@hailer/mcp 0.1.15 → 0.1.17

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 (112) hide show
  1. package/.claude/agents/agent-giuseppe-app-builder.md +7 -6
  2. package/.claude/agents/agent-lars-code-inspector.md +26 -14
  3. package/dist/agents/bot-manager.d.ts +48 -0
  4. package/dist/agents/bot-manager.js +254 -0
  5. package/dist/agents/factory.d.ts +150 -0
  6. package/dist/agents/factory.js +650 -0
  7. package/dist/agents/giuseppe/ai.d.ts +83 -0
  8. package/dist/agents/giuseppe/ai.js +466 -0
  9. package/dist/agents/giuseppe/bot.d.ts +110 -0
  10. package/dist/agents/giuseppe/bot.js +780 -0
  11. package/dist/agents/giuseppe/config.d.ts +25 -0
  12. package/dist/agents/giuseppe/config.js +227 -0
  13. package/dist/agents/giuseppe/files.d.ts +52 -0
  14. package/dist/agents/giuseppe/files.js +338 -0
  15. package/dist/agents/giuseppe/git.d.ts +48 -0
  16. package/dist/agents/giuseppe/git.js +298 -0
  17. package/dist/agents/giuseppe/index.d.ts +97 -0
  18. package/dist/agents/giuseppe/index.js +258 -0
  19. package/dist/agents/giuseppe/lsp.d.ts +113 -0
  20. package/dist/agents/giuseppe/lsp.js +485 -0
  21. package/dist/agents/giuseppe/monitor.d.ts +118 -0
  22. package/dist/agents/giuseppe/monitor.js +621 -0
  23. package/dist/agents/giuseppe/prompt.d.ts +5 -0
  24. package/dist/agents/giuseppe/prompt.js +94 -0
  25. package/dist/agents/giuseppe/registries/pending-classification.d.ts +28 -0
  26. package/dist/agents/giuseppe/registries/pending-classification.js +50 -0
  27. package/dist/agents/giuseppe/registries/pending-fix.d.ts +30 -0
  28. package/dist/agents/giuseppe/registries/pending-fix.js +42 -0
  29. package/dist/agents/giuseppe/registries/pending.d.ts +27 -0
  30. package/dist/agents/giuseppe/registries/pending.js +49 -0
  31. package/dist/agents/giuseppe/specialist.d.ts +47 -0
  32. package/dist/agents/giuseppe/specialist.js +237 -0
  33. package/dist/agents/giuseppe/types.d.ts +123 -0
  34. package/dist/agents/giuseppe/types.js +9 -0
  35. package/dist/agents/hailer-expert/index.d.ts +8 -0
  36. package/dist/agents/hailer-expert/index.js +14 -0
  37. package/dist/agents/hal/daemon.d.ts +142 -0
  38. package/dist/agents/hal/daemon.js +1103 -0
  39. package/dist/agents/hal/definitions.d.ts +55 -0
  40. package/dist/agents/hal/definitions.js +263 -0
  41. package/dist/agents/hal/index.d.ts +3 -0
  42. package/dist/agents/hal/index.js +8 -0
  43. package/dist/agents/index.d.ts +18 -0
  44. package/dist/agents/index.js +48 -0
  45. package/dist/agents/shared/base.d.ts +216 -0
  46. package/dist/agents/shared/base.js +846 -0
  47. package/dist/agents/shared/services/agent-registry.d.ts +107 -0
  48. package/dist/agents/shared/services/agent-registry.js +629 -0
  49. package/dist/agents/shared/services/conversation-manager.d.ts +50 -0
  50. package/dist/agents/shared/services/conversation-manager.js +136 -0
  51. package/dist/agents/shared/services/mcp-client.d.ts +56 -0
  52. package/dist/agents/shared/services/mcp-client.js +124 -0
  53. package/dist/agents/shared/services/message-classifier.d.ts +37 -0
  54. package/dist/agents/shared/services/message-classifier.js +187 -0
  55. package/dist/agents/shared/services/message-formatter.d.ts +89 -0
  56. package/dist/agents/shared/services/message-formatter.js +371 -0
  57. package/dist/agents/shared/services/session-logger.d.ts +106 -0
  58. package/dist/agents/shared/services/session-logger.js +446 -0
  59. package/dist/agents/shared/services/tool-executor.d.ts +41 -0
  60. package/dist/agents/shared/services/tool-executor.js +169 -0
  61. package/dist/agents/shared/services/workspace-schema-cache.d.ts +125 -0
  62. package/dist/agents/shared/services/workspace-schema-cache.js +578 -0
  63. package/dist/agents/shared/specialist.d.ts +91 -0
  64. package/dist/agents/shared/specialist.js +399 -0
  65. package/dist/agents/shared/tool-schema-loader.d.ts +62 -0
  66. package/dist/agents/shared/tool-schema-loader.js +232 -0
  67. package/dist/agents/shared/types.d.ts +327 -0
  68. package/dist/agents/shared/types.js +121 -0
  69. package/dist/app.js +21 -4
  70. package/dist/cli.js +0 -0
  71. package/dist/client/agents/orchestrator.d.ts +1 -0
  72. package/dist/client/agents/orchestrator.js +12 -1
  73. package/dist/commands/seed-config.d.ts +9 -0
  74. package/dist/commands/seed-config.js +372 -0
  75. package/dist/config.d.ts +10 -0
  76. package/dist/config.js +61 -1
  77. package/dist/core.d.ts +8 -0
  78. package/dist/core.js +137 -6
  79. package/dist/lib/discussion-lock.d.ts +42 -0
  80. package/dist/lib/discussion-lock.js +110 -0
  81. package/dist/mcp/UserContextCache.js +2 -2
  82. package/dist/mcp/hailer-clients.d.ts +15 -0
  83. package/dist/mcp/hailer-clients.js +100 -6
  84. package/dist/mcp/signal-handler.d.ts +16 -5
  85. package/dist/mcp/signal-handler.js +173 -122
  86. package/dist/mcp/tools/activity.js +9 -1
  87. package/dist/mcp/tools/bot-config.d.ts +184 -9
  88. package/dist/mcp/tools/bot-config.js +2177 -163
  89. package/dist/mcp/tools/giuseppe-tools.d.ts +21 -0
  90. package/dist/mcp/tools/giuseppe-tools.js +525 -0
  91. package/dist/mcp/utils/hailer-api-client.d.ts +42 -1
  92. package/dist/mcp/utils/hailer-api-client.js +128 -2
  93. package/dist/mcp/webhook-handler.d.ts +87 -0
  94. package/dist/mcp/webhook-handler.js +345 -0
  95. package/dist/mcp/workspace-cache.d.ts +5 -0
  96. package/dist/mcp/workspace-cache.js +11 -0
  97. package/dist/mcp-server.js +60 -5
  98. package/dist/modules/bug-reports/giuseppe-agent.d.ts +58 -0
  99. package/dist/modules/bug-reports/giuseppe-agent.js +467 -0
  100. package/dist/modules/bug-reports/giuseppe-ai.d.ts +25 -1
  101. package/dist/modules/bug-reports/giuseppe-ai.js +133 -2
  102. package/dist/modules/bug-reports/giuseppe-bot.d.ts +2 -2
  103. package/dist/modules/bug-reports/giuseppe-bot.js +66 -42
  104. package/dist/modules/bug-reports/giuseppe-daemon.d.ts +80 -0
  105. package/dist/modules/bug-reports/giuseppe-daemon.js +617 -0
  106. package/dist/modules/bug-reports/giuseppe-files.d.ts +12 -0
  107. package/dist/modules/bug-reports/giuseppe-files.js +37 -0
  108. package/dist/modules/bug-reports/giuseppe-lsp.d.ts +84 -13
  109. package/dist/modules/bug-reports/giuseppe-lsp.js +403 -61
  110. package/dist/modules/bug-reports/index.d.ts +1 -0
  111. package/dist/modules/bug-reports/index.js +31 -29
  112. package/package.json +3 -2
@@ -0,0 +1,1103 @@
1
+ "use strict";
2
+ /**
3
+ * Orchestrator Daemon (HAL)
4
+ *
5
+ * The main conversational bot that handles general chat and coordinates
6
+ * with specialist bots when tasks are too complex.
7
+ *
8
+ * HAL can:
9
+ * - Handle general conversation and simple queries
10
+ * - Detect when a task needs specialist help
11
+ * - Invite specialist bots to the discussion
12
+ * - Hand off context to specialists
13
+ * - Summarize specialist responses for users
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.OrchestratorDaemon = void 0;
17
+ const base_1 = require("../shared/base");
18
+ const definitions_1 = require("./definitions");
19
+ const logger_1 = require("../../lib/logger");
20
+ const pending_fix_1 = require("../giuseppe/registries/pending-fix");
21
+ const pending_classification_1 = require("../giuseppe/registries/pending-classification");
22
+ class OrchestratorDaemon extends base_1.ChatAgentDaemon {
23
+ orchestratorLogger;
24
+ specialists = new Map();
25
+ activeSpecialistsInDiscussion = new Map(); // discussionId -> Set<specialistUserId>
26
+ specialistUserIds = new Map(); // specialistKey -> userId
27
+ toolsUsedInCurrentMessage = false; // Track if tools were used in current message processing
28
+ lastToolsUsed = []; // Track which tools were used (for silent success detection)
29
+ lastToolsFailed = false; // Track if any tool failed (to allow error reporting)
30
+ // Tools that should NOT trigger confirmation messages back to source chat (on SUCCESS only)
31
+ static SILENT_SUCCESS_TOOLS = new Set(['join_discussion']);
32
+ // Cross-discussion memory - remembers recent activity context
33
+ lastKnownActivityId = null;
34
+ lastKnownActivityName = null;
35
+ lastKnownActivityTime = 0;
36
+ static CONTEXT_MEMORY_TIMEOUT = 5 * 60 * 1000; // 5 minutes
37
+ constructor(config) {
38
+ super(config);
39
+ this.orchestratorLogger = (0, logger_1.createLogger)({
40
+ component: "OrchestratorDaemon",
41
+ botId: config.botClient.userId,
42
+ });
43
+ // Register specialists from config (make copies to avoid cross-workspace contamination)
44
+ for (const [key, specialist] of Object.entries(definitions_1.SPECIALISTS)) {
45
+ // Clone the specialist so each orchestrator has its own instance
46
+ this.specialists.set(key, { ...specialist });
47
+ }
48
+ // Set specialist user IDs if provided
49
+ if (config.specialistUserIds) {
50
+ this.specialistUserIds = config.specialistUserIds;
51
+ }
52
+ }
53
+ /**
54
+ * Trigger HAL to respond in a discussion with specific context
55
+ * Used when bug monitor needs HAL to naturally inform users about Giuseppe being disabled
56
+ */
57
+ async respondWithContext(discussionId, activityId, contextMessage) {
58
+ this.orchestratorLogger.info('Triggering contextual response', { discussionId, activityId });
59
+ try {
60
+ // Join the discussion first using MCP tool
61
+ await this.callMcpTool("join_discussion", { activityId });
62
+ // Get workspace ID from bot client cache
63
+ const workspaceId = this.config.botClient.workspaceCache?.currentWorkspace?._id || '';
64
+ // Create a synthetic incoming message with the context
65
+ const syntheticMessage = {
66
+ id: `synthetic-${Date.now()}`,
67
+ discussionId,
68
+ linkedActivityId: activityId,
69
+ workspaceId,
70
+ content: contextMessage,
71
+ senderName: 'System',
72
+ senderId: 'system',
73
+ timestamp: Date.now(),
74
+ priority: 'high',
75
+ priorityReason: 'system_notification',
76
+ isReplyToBot: false,
77
+ isMention: false,
78
+ isDirectMessage: false,
79
+ };
80
+ // Process through normal LLM pipeline
81
+ // This will make HAL respond naturally with the context
82
+ await this['processMessage'](syntheticMessage);
83
+ }
84
+ catch (error) {
85
+ this.orchestratorLogger.error('Failed to respond with context', { error });
86
+ }
87
+ }
88
+ // ===== AGENT DIRECTORY OVERRIDES =====
89
+ /**
90
+ * Override agent name for Agent Directory
91
+ * Uses the actual Hailer user name from BotClient (set in workspace)
92
+ */
93
+ getAgentName() {
94
+ // Use parent implementation which gets name from BotClient
95
+ return super.getAgentName();
96
+ }
97
+ /**
98
+ * Override agent description for Agent Directory
99
+ */
100
+ getAgentDescription() {
101
+ return "HAL - the main Hailer Assistant orchestrator. Handles general conversation and coordinates specialist bots for complex tasks.";
102
+ }
103
+ /**
104
+ * Override Position details for Orchestrator
105
+ */
106
+ getPositionDetails() {
107
+ return {
108
+ name: "HAL Orchestrator",
109
+ purpose: "Main point of contact for users. Handles general conversation, triages requests, and coordinates specialist bots for complex tasks.",
110
+ personaTone: "Sharp, efficient, and helpful. Professional but approachable. Uses clear, concise language.",
111
+ coreCapabilities: "- Monitor all workspace discussions\n- Respond to general queries and greetings\n- Detect when specialist help is needed\n- Invite specialists to discussions\n- Coordinate multi-step workflows\n- Execute MCP tools for data operations",
112
+ boundaries: "- Never fabricate data - always use tools\n- Don't attempt complex technical tasks alone\n- Hand off to specialists for: bulk operations, report creation, workflow setup\n- Don't share credentials or sensitive config",
113
+ };
114
+ }
115
+ /**
116
+ * Orchestrator only needs basic tools - complex ops go to specialists
117
+ */
118
+ getToolWhitelist() {
119
+ return [
120
+ // Workflow discovery
121
+ "list_workflows",
122
+ "list_workflow_phases",
123
+ "get_workflow_schema",
124
+ "list_workflows_minimal",
125
+ // Activity operations
126
+ "list_activities",
127
+ "show_activity_by_id",
128
+ "count_activities",
129
+ "create_activity",
130
+ "update_activity",
131
+ // Discussion tools
132
+ "join_discussion",
133
+ "add_discussion_message",
134
+ "invite_discussion_members",
135
+ "fetch_discussion_messages",
136
+ "get_activity_from_discussion",
137
+ "list_my_discussions",
138
+ // User lookup
139
+ "search_workspace_users",
140
+ // App tools (read-only)
141
+ "list_apps",
142
+ ];
143
+ }
144
+ /**
145
+ * Preprocess tool input - inject context for certain tools
146
+ * Uses cross-discussion memory to maintain context awareness
147
+ */
148
+ preprocessToolInput(toolName, input) {
149
+ // Force compact output for schema tools (avoid verbose field dumps in discussions)
150
+ if (toolName === "get_workflow_schema") {
151
+ return { ...input, compact: true };
152
+ }
153
+ // Auto-inject sourceActivityId for join_discussion
154
+ if (toolName === "join_discussion") {
155
+ // Debug: Log current context state
156
+ this.orchestratorLogger.debug("join_discussion context check", {
157
+ currentLinkedActivityId: this.currentLinkedActivityId || "none",
158
+ lastKnownActivityId: this.lastKnownActivityId || "none",
159
+ inputSourceActivityId: input.sourceActivityId || "none",
160
+ currentDiscussionId: this.currentDiscussionId || "none",
161
+ });
162
+ if (!input.sourceActivityId) {
163
+ // First try current message's linked activity
164
+ const currentActivityId = this.currentLinkedActivityId;
165
+ // Fall back to recent memory if within timeout
166
+ const memoryStillValid = Date.now() - this.lastKnownActivityTime < OrchestratorDaemon.CONTEXT_MEMORY_TIMEOUT;
167
+ const sourceActivityId = currentActivityId || (memoryStillValid ? this.lastKnownActivityId : null);
168
+ if (sourceActivityId) {
169
+ this.orchestratorLogger.info("Auto-injected sourceActivityId", {
170
+ sourceActivityId,
171
+ fromCurrentMessage: !!currentActivityId,
172
+ fromMemory: !currentActivityId && !!sourceActivityId,
173
+ });
174
+ input = { ...input, sourceActivityId };
175
+ }
176
+ else {
177
+ this.orchestratorLogger.debug("No sourceActivityId available to inject", {
178
+ currentLinkedActivityId: this.currentLinkedActivityId,
179
+ lastKnownActivityId: this.lastKnownActivityId,
180
+ memoryStillValid,
181
+ });
182
+ }
183
+ }
184
+ // Always inject a default welcomeReason if user is being invited
185
+ // This ensures welcome messages are always posted
186
+ if (input.inviteUserId && !input.welcomeReason) {
187
+ const reason = this.lastKnownActivityName
188
+ ? `Added from ${this.lastKnownActivityName} discussion`
189
+ : "Added to this discussion";
190
+ this.orchestratorLogger.debug("Auto-injected welcomeReason", { reason });
191
+ input = { ...input, welcomeReason: reason };
192
+ }
193
+ }
194
+ return input;
195
+ }
196
+ /**
197
+ * Override to detect tool failures for silent success feature
198
+ */
199
+ async executeToolsAndContinue(toolUseBlocks, originalMessage) {
200
+ // Reset failure flag before executing tools
201
+ this.lastToolsFailed = false;
202
+ // Handle local tools (not sent to MCP)
203
+ const localToolResults = [];
204
+ const mcpToolBlocks = [];
205
+ for (const toolBlock of toolUseBlocks) {
206
+ if (toolBlock.name === 'trigger_giuseppe_retry') {
207
+ // Handle locally - trigger Giuseppe retry
208
+ const input = toolBlock.input;
209
+ const explanation = input.explanation || '';
210
+ const discussionId = originalMessage.discussionId;
211
+ const pendingFix = pending_fix_1.pendingFixRegistry.getPendingFix(discussionId);
212
+ if (!pendingFix) {
213
+ localToolResults.push({
214
+ type: 'tool_result',
215
+ tool_use_id: toolBlock.id,
216
+ content: 'No pending fix found for this discussion. The fix may have already been approved or cancelled.',
217
+ is_error: true,
218
+ });
219
+ }
220
+ else {
221
+ // Trigger the retry
222
+ const success = await pending_fix_1.pendingFixRegistry.triggerRetry(discussionId, explanation);
223
+ localToolResults.push({
224
+ type: 'tool_result',
225
+ tool_use_id: toolBlock.id,
226
+ content: success
227
+ ? `✅ Triggered Giuseppe retry with explanation: "${explanation.substring(0, 100)}...". Giuseppe will analyze and apply a new fix.`
228
+ : '❌ Failed to trigger retry. Giuseppe may not be available.',
229
+ is_error: !success,
230
+ });
231
+ }
232
+ }
233
+ else if (toolBlock.name === 'confirm_bug_fix') {
234
+ // Handle locally - trigger Giuseppe fix confirmation
235
+ const discussionId = originalMessage.discussionId;
236
+ const hasPending = pending_classification_1.pendingClassificationRegistry.has(discussionId);
237
+ if (!hasPending) {
238
+ localToolResults.push({
239
+ type: 'tool_result',
240
+ tool_use_id: toolBlock.id,
241
+ content: 'No pending classification found for this discussion. Giuseppe may have already processed it.',
242
+ is_error: true,
243
+ });
244
+ }
245
+ else {
246
+ const success = await pending_classification_1.pendingClassificationRegistry.triggerFixIt(discussionId);
247
+ localToolResults.push({
248
+ type: 'tool_result',
249
+ tool_use_id: toolBlock.id,
250
+ content: success
251
+ ? '✅ Triggered Giuseppe to proceed with bug fix.'
252
+ : '❌ Failed to trigger fix. Giuseppe may not be available.',
253
+ is_error: !success,
254
+ });
255
+ }
256
+ }
257
+ else if (toolBlock.name === 'decline_bug_report') {
258
+ // Handle locally - trigger Giuseppe decline
259
+ const discussionId = originalMessage.discussionId;
260
+ const hasPending = pending_classification_1.pendingClassificationRegistry.has(discussionId);
261
+ if (!hasPending) {
262
+ localToolResults.push({
263
+ type: 'tool_result',
264
+ tool_use_id: toolBlock.id,
265
+ content: 'No pending classification found for this discussion. Giuseppe may have already processed it.',
266
+ is_error: true,
267
+ });
268
+ }
269
+ else {
270
+ const success = await pending_classification_1.pendingClassificationRegistry.triggerNotABug(discussionId);
271
+ localToolResults.push({
272
+ type: 'tool_result',
273
+ tool_use_id: toolBlock.id,
274
+ content: success
275
+ ? '✅ Marked as not a bug. Report moved to Declined.'
276
+ : '❌ Failed to decline. Giuseppe may not be available.',
277
+ is_error: !success,
278
+ });
279
+ }
280
+ }
281
+ else {
282
+ // Send to MCP
283
+ mcpToolBlocks.push(toolBlock);
284
+ }
285
+ }
286
+ // If we handled all tools locally, skip MCP execution
287
+ if (mcpToolBlocks.length === 0 && localToolResults.length > 0) {
288
+ const conversation = this.conversationManager.getConversation(originalMessage.discussionId);
289
+ conversation.push({
290
+ role: 'user',
291
+ content: localToolResults,
292
+ });
293
+ // Continue with LLM
294
+ const response = await this.client.messages.create({
295
+ model: this.config.model || 'claude-haiku-4-5-20251001',
296
+ max_tokens: 2000,
297
+ system: this.getSystemPrompt(),
298
+ messages: conversation,
299
+ tools: this.getToolsWithGiuseppeRetry(),
300
+ });
301
+ await this.handleLlmResponse(response, originalMessage);
302
+ return;
303
+ }
304
+ // If we have local results, add them to be processed together with MCP results
305
+ // (for now, just continue with original flow for MCP tools)
306
+ // Get current activity session
307
+ const sessionKey = this.currentLinkedActivityId || this.currentDiscussionId || "default";
308
+ const session = this.sessionLogger.getSession(sessionKey);
309
+ // Store the user's request that triggered these tool calls (for context)
310
+ if (session && !session.triggerRequest) {
311
+ session.triggerRequest = originalMessage.content.substring(0, 500);
312
+ session.requestedBy = originalMessage.senderName;
313
+ session.requestedById = originalMessage.senderId;
314
+ }
315
+ // Execute tools using the tool executor service
316
+ const toolResults = await this.toolExecutor.executeTools(toolUseBlocks, {
317
+ session,
318
+ preprocessToolInput: this.preprocessToolInput.bind(this),
319
+ });
320
+ // Check if any tool failed (detect various error patterns)
321
+ const anyToolFailed = toolResults.some(r => {
322
+ if (r.is_error)
323
+ return true;
324
+ // Check for error patterns in content (MCP tools return error text)
325
+ const content = r.content.toLowerCase();
326
+ return content.includes('error') || content.includes('failed') || content.includes('❌');
327
+ });
328
+ if (anyToolFailed) {
329
+ this.lastToolsFailed = true;
330
+ this.orchestratorLogger.debug("Tool failure detected", {
331
+ tools: this.lastToolsUsed,
332
+ failedCount: toolResults.filter(r => r.is_error).length,
333
+ });
334
+ }
335
+ // Get conversation for this discussion
336
+ const conversation = this.conversationManager.getConversation(originalMessage.discussionId);
337
+ // Add tool results to conversation
338
+ conversation.push({
339
+ role: "user",
340
+ content: toolResults,
341
+ });
342
+ // Continue with LLM
343
+ const response = await this.client.messages.create({
344
+ model: this.config.model || "claude-haiku-4-5-20251001",
345
+ max_tokens: 2000,
346
+ system: this.getSystemPrompt(),
347
+ messages: conversation,
348
+ tools: this.minimalTools,
349
+ });
350
+ // Track token usage in session
351
+ if (session && response.usage) {
352
+ session.metrics.inputTokens += response.usage.input_tokens;
353
+ session.metrics.outputTokens += response.usage.output_tokens;
354
+ session.lastActivityTime = Date.now();
355
+ }
356
+ // Recursively handle (might need more tools or finally respond)
357
+ await this.handleLlmResponse(response, originalMessage);
358
+ }
359
+ /**
360
+ * Update cross-discussion memory when processing messages
361
+ * Call this when entering an activity discussion to remember context
362
+ */
363
+ updateContextMemory(activityId, activityName) {
364
+ if (activityId) {
365
+ this.lastKnownActivityId = activityId;
366
+ this.lastKnownActivityName = activityName;
367
+ this.lastKnownActivityTime = Date.now();
368
+ this.orchestratorLogger.debug("Updated context memory", { activityId, activityName });
369
+ }
370
+ }
371
+ /**
372
+ * Override to update cross-discussion memory when entering activity discussions
373
+ */
374
+ async extractIncomingMessage(signal) {
375
+ const message = await super.extractIncomingMessage(signal);
376
+ // Update context memory if this message is from an activity discussion
377
+ if (message?.linkedActivityId) {
378
+ this.updateContextMemory(message.linkedActivityId, message.linkedActivityName || null);
379
+ }
380
+ return message;
381
+ }
382
+ /**
383
+ * Register a specialist's Hailer user ID
384
+ * Called during initialization when we know the specialist bot's user ID
385
+ */
386
+ registerSpecialistUserId(specialistKey, userId, displayName) {
387
+ this.specialistUserIds.set(specialistKey, userId);
388
+ const specialist = this.specialists.get(specialistKey);
389
+ if (specialist) {
390
+ specialist.botUserId = userId;
391
+ if (displayName) {
392
+ specialist.displayName = displayName;
393
+ }
394
+ this.orchestratorLogger.info("Specialist registered", {
395
+ key: specialistKey,
396
+ name: specialist.name,
397
+ displayName: specialist.displayName || specialist.name,
398
+ userId,
399
+ });
400
+ }
401
+ }
402
+ /**
403
+ * Unregister a specialist (called when specialist is disabled/stopped)
404
+ * This prevents HAL from trying to invite a disabled specialist
405
+ */
406
+ unregisterSpecialist(specialistKey) {
407
+ const specialist = this.specialists.get(specialistKey);
408
+ if (specialist) {
409
+ specialist.botUserId = undefined; // Mark as unavailable
410
+ this.orchestratorLogger.info("Specialist unregistered", {
411
+ key: specialistKey,
412
+ name: specialist.name,
413
+ });
414
+ }
415
+ this.specialistUserIds.delete(specialistKey);
416
+ }
417
+ /**
418
+ * Mark a specialist as active in a discussion
419
+ */
420
+ markSpecialistActive(discussionId, specialistUserId) {
421
+ if (!this.activeSpecialistsInDiscussion.has(discussionId)) {
422
+ this.activeSpecialistsInDiscussion.set(discussionId, new Set());
423
+ }
424
+ this.activeSpecialistsInDiscussion.get(discussionId).add(specialistUserId);
425
+ }
426
+ /**
427
+ * Invite a specialist to a discussion
428
+ * Always invites first (tool handles membership check), then tags
429
+ */
430
+ async inviteSpecialist(specialist, discussionId, handoffContext) {
431
+ const specialistUserId = specialist.botUserId;
432
+ const displayName = specialist.displayName || specialist.name;
433
+ if (!specialistUserId) {
434
+ this.orchestratorLogger.warn("Specialist has no user ID", {
435
+ name: specialist.name,
436
+ });
437
+ return false;
438
+ }
439
+ try {
440
+ this.orchestratorLogger.info("Inviting specialist to discussion", {
441
+ name: specialist.name,
442
+ displayName,
443
+ userId: specialistUserId,
444
+ discussionId,
445
+ });
446
+ // Always call invite - the tool checks if already a member and handles gracefully
447
+ await this.callMcpTool("invite_discussion_members", {
448
+ discussionId,
449
+ userIds: [specialistUserId],
450
+ });
451
+ this.markSpecialistActive(discussionId, specialistUserId);
452
+ // Post handoff message with Hailer tag format using actual display name
453
+ await this.postResponse(discussionId, `[hailerTag|${displayName}](${specialistUserId}) - ${handoffContext}`);
454
+ this.orchestratorLogger.info("Specialist invited successfully", {
455
+ name: specialist.name,
456
+ displayName,
457
+ discussionId,
458
+ });
459
+ return true;
460
+ }
461
+ catch (error) {
462
+ this.orchestratorLogger.error("Failed to invite specialist", {
463
+ name: specialist.name,
464
+ displayName,
465
+ discussionId,
466
+ error: error instanceof Error ? error.message : String(error),
467
+ });
468
+ return false;
469
+ }
470
+ }
471
+ /**
472
+ * Override getTools to include Giuseppe retry tool when there's a pending fix
473
+ */
474
+ getTools() {
475
+ // Always include the Giuseppe retry tool so LLM knows about it
476
+ return this.getToolsWithGiuseppeRetry();
477
+ }
478
+ /**
479
+ * Get tools list with Giuseppe retry tool added
480
+ */
481
+ getToolsWithGiuseppeRetry() {
482
+ const giuseppeRetryTool = {
483
+ name: 'trigger_giuseppe_retry',
484
+ description: 'Trigger Giuseppe to retry fixing a bug with new explanation. Use this after gathering enough info about what went wrong with the previous fix. Only works in discussions with a pending bug fix.',
485
+ input_schema: {
486
+ type: 'object',
487
+ properties: {
488
+ explanation: {
489
+ type: 'string',
490
+ description: 'Clear explanation of what is wrong with the current fix and what the expected behavior should be. Include: (1) what is broken, (2) expected vs actual behavior, (3) any specific details the user mentioned.',
491
+ },
492
+ },
493
+ required: ['explanation'],
494
+ },
495
+ };
496
+ const confirmBugFixTool = {
497
+ name: 'confirm_bug_fix',
498
+ description: 'Confirm that a bug report should be fixed. Use when user indicates they want the bug fixed (variations of "fix it", "yes fix", "please fix", etc.). Only works in discussions with a pending classification.',
499
+ input_schema: {
500
+ type: 'object',
501
+ properties: {},
502
+ required: [],
503
+ },
504
+ };
505
+ const declineBugReportTool = {
506
+ name: 'decline_bug_report',
507
+ description: 'Decline a bug report as not actually a bug. Use when user indicates it\'s not a bug (variations of "not a bug", "feature request", "not actually broken", etc.). Only works in discussions with a pending classification.',
508
+ input_schema: {
509
+ type: 'object',
510
+ properties: {},
511
+ required: [],
512
+ },
513
+ };
514
+ return [...this.minimalTools, giuseppeRetryTool, confirmBugFixTool, declineBugReportTool];
515
+ }
516
+ /**
517
+ * Get pending fix context for a discussion (if any)
518
+ */
519
+ getPendingFixContext(discussionId) {
520
+ const pendingFix = pending_fix_1.pendingFixRegistry.getPendingFix(discussionId);
521
+ if (!pendingFix) {
522
+ return '';
523
+ }
524
+ return `
525
+ <pending_bug_fix>
526
+ **IMPORTANT: This discussion has a pending bug fix from Giuseppe!**
527
+
528
+ - Bug ID: ${pendingFix.bugId}
529
+ - State: ${pendingFix.state}
530
+ - Fix Summary: ${pendingFix.fixSummary}
531
+
532
+ **Your role:**
533
+ - If user says "approved" → Giuseppe handles it automatically (you do nothing)
534
+ - If user expresses ANY dissatisfaction (denied, not working, still broken, etc.):
535
+ 1. Ask what's wrong (if not clear)
536
+ 2. Gather: what's broken + expected behavior
537
+ 3. Use trigger_giuseppe_retry tool with the explanation
538
+
539
+ **Remember:** You're gathering info for Giuseppe. Be concise, ask ONE question at a time.
540
+ </pending_bug_fix>
541
+ `;
542
+ }
543
+ /**
544
+ * Get pending classification context for a discussion (if any)
545
+ */
546
+ getPendingClassificationContext(discussionId) {
547
+ const pending = pending_classification_1.pendingClassificationRegistry.getPendingClassification(discussionId);
548
+ if (!pending) {
549
+ return '';
550
+ }
551
+ const classificationLabel = pending.classification === 'bug' ? 'Bug' :
552
+ pending.classification === 'feature_request' ? 'Feature Request' : 'Unclear';
553
+ return `
554
+ <pending_classification>
555
+ **IMPORTANT: Giuseppe just classified this report and is waiting for user decision!**
556
+
557
+ - Bug: ${pending.bugName}
558
+ - App: ${pending.appName || pending.appId || 'Unknown'}
559
+ - Classification: ${classificationLabel}
560
+ - Reason: ${pending.reason}
561
+
562
+ **User options:**
563
+ - "fix it" → Giuseppe will proceed to fix the bug
564
+ - "not a bug" → Report will be moved to Declined
565
+
566
+ **Your role - Use tools for user intent:**
567
+ - User wants to fix the bug → Call confirm_bug_fix tool
568
+ - User says it's not a bug / feature request → Call decline_bug_report tool
569
+ - User has questions → Help explain the classification
570
+
571
+ **Detecting intent (use tools, don't wait for exact words):**
572
+ - "fix it", "yes fix", "go ahead", "please fix", "fix this" → confirm_bug_fix
573
+ - "not a bug", "feature request", "not broken", "close it", "decline" → decline_bug_report
574
+
575
+ **Remember:** Use the tools to trigger actions. Don't tell user to type magic words.
576
+ </pending_classification>
577
+ `;
578
+ }
579
+ /**
580
+ * Override system prompt to include orchestrator capabilities
581
+ */
582
+ getSystemPrompt() {
583
+ const now = new Date();
584
+ const { firstName, lastName } = this.getAgentName();
585
+ const fullName = `${firstName} ${lastName}`.trim();
586
+ // Build specialist info for prompt - ONLY show available specialists
587
+ const availableSpecialists = Array.from(this.specialists.entries())
588
+ .filter(([_, spec]) => !!spec.botUserId);
589
+ const specialistInfo = availableSpecialists.length > 0
590
+ ? availableSpecialists
591
+ .map(([key, spec]) => {
592
+ const mentionName = spec.displayName || spec.name;
593
+ return `- **${spec.name}** (available)
594
+ Tag with: [hailerTag|${mentionName}](${spec.botUserId})
595
+ Key for invite tag: ${key}
596
+ Expertise: ${spec.expertise.join(", ")}
597
+ Triggers: ${spec.triggerKeywords.slice(0, 5).join(", ")}`;
598
+ })
599
+ .join("\n\n")
600
+ : `**NO SPECIALISTS ARE CURRENTLY ENABLED.**
601
+ DO NOT invent, fabricate, or mention any specialist names.
602
+ DO NOT use <invite> tags. DO NOT claim to "hand off" to anyone.
603
+ Handle all requests yourself or tell the user: "No specialists are enabled. You can enable them in AI Hub."`;
604
+ return `<identity>
605
+ You are ${fullName} - the Hailer Assistant. Sharp, efficient, and helpful.
606
+ Bot ID: ${this.botClient.userId}
607
+
608
+ You're the main point of contact for users. You handle general conversation
609
+ and simple tasks yourself, but can bring in specialist bots for complex work.
610
+ </identity>
611
+
612
+ <current_time>${now.toISOString()}</current_time>
613
+
614
+ <personality>
615
+ **BUSINESS MODE** (default):
616
+ - Professional, direct, competent
617
+ - Get things done efficiently
618
+ - Provide accurate information
619
+
620
+ **SARCASM MODE** (for ridiculous requests):
621
+ - Dry wit, not mean-spirited
622
+ - Still help after the gentle mockery
623
+ </personality>
624
+
625
+ <decision_framework>
626
+ For each message, decide:
627
+
628
+ 1. **HIGH PRIORITY** (priority="high") - ALWAYS RESPOND:
629
+ - Direct messages (1:1) -> respond helpfully
630
+ - @mentions -> respond (even if just a greeting - acknowledge and ask how you can help)
631
+ - Replies to your messages -> respond
632
+ - **NEVER use <ignore/> for HIGH priority. Always acknowledge the user.**
633
+
634
+ 2. **NORMAL PRIORITY** (general chat) - STRICT FILTERING:
635
+
636
+ **RESPOND ONLY IF the message:**
637
+ - Explicitly asks about Hailer (workflows, activities, insights, apps, discussions)
638
+ - Requests to find/list/create/update workspace data
639
+ - Discusses a specific activity, customer, project, or workflow by name
640
+ - You can genuinely help with workspace-related context
641
+ - Is a complex task needing specialist help
642
+
643
+ **IGNORE (output <ignore/>) for NORMAL priority if:**
644
+ - Random characters, gibberish, keyboard mashing (no real words, repeated patterns)
645
+ - General chit-chat unrelated to workspace ("how are you", jokes)
646
+ - Conversations between other users that don't need you
647
+ - Bare greetings without a question ("hi", "hey") - but NOT if HIGH priority!
648
+ - Off-topic discussions (sports, weather, personal chat)
649
+ - Anything you're uncertain about
650
+
651
+ **CRITICAL:** For NORMAL priority only - if no clear workspace-related question/task, output <ignore/>.
652
+ When in doubt about NORMAL priority, IGNORE. Your DEFAULT for normal priority is <ignore/>.
653
+
654
+ 3. **RESPOND FORMAT** (only when you have something helpful):
655
+ <respond discussion="DISCUSSION_ID">
656
+ Your response
657
+ </respond>
658
+
659
+ 4. **IGNORE FORMAT** (use liberally - this is your DEFAULT):
660
+ <ignore/>
661
+ </decision_framework>
662
+
663
+ <specialists>
664
+ ${specialistInfo}
665
+ ${availableSpecialists.length > 0 ? `
666
+ **When to invite a specialist:**
667
+ - Creating new workflows/pipelines
668
+ - Setting up reports/insights/dashboards
669
+ - Bulk operations (10+ items)
670
+ - Complex multi-step data tasks
671
+ - Workflow configuration changes
672
+
673
+ **When to handle yourself:**
674
+ - General chat, greetings
675
+ - Simple queries (list activities, show details)
676
+ - Single create/update operations
677
+ - Questions about the conversation
678
+ - Clarifying user requirements
679
+
680
+ <invite_syntax>
681
+ <invite specialist="specialistKey">
682
+ Clear description of what you need done.
683
+ Include relevant context from the conversation.
684
+ </invite>
685
+ </invite_syntax>
686
+
687
+ <invite_rules>
688
+ **CRITICAL: NEVER use tools to invite or tag specialists directly!**
689
+ - DO NOT use invite_discussion_members for specialists
690
+ - DO NOT use add_discussion_message with [hailerTag|...] for specialists
691
+ - ALWAYS use the <invite specialist="key">...</invite> XML pattern
692
+
693
+ The system will handle inviting the specialist to the discussion and tagging them.
694
+ If you try to invite/tag specialists directly via tools, they may not receive the message.
695
+ </invite_rules>
696
+
697
+ <handoff_rules>
698
+ CRITICAL: When you've already gathered IDs (workflows, fields, phases), include them formatted in the handoff.
699
+
700
+ <bad_handoff>
701
+ "Create an insight for the Injuries workflow showing player data"
702
+ </bad_handoff>
703
+
704
+ <good_handoff>
705
+ "Create insight for Injuries workflow:
706
+ - workflowId: 691ffdf84217e9e8434e56ad
707
+ - Fields: { name: 'playerName', fieldId: '691ffdf84217e9e8434e56b1' }, { name: 'injuryType', fieldId: '691ffdf84217e9e8434e56b2' }
708
+ - Query: SELECT name as \\"Activity\\", playerName, injuryType FROM injuries ORDER BY name"
709
+ </good_handoff>
710
+
711
+ <insight_field_format>
712
+ Always format fields as: { name: 'Column Name', fieldId: 'FIELD_ID' }
713
+ Use 'fieldId' not 'id' for the field identifier.
714
+ </insight_field_format>
715
+ </handoff_rules>
716
+
717
+ <after_invite>
718
+ 1. I invite them to the discussion
719
+ 2. I post your handoff message mentioning them
720
+ 3. They see it and take action
721
+ </after_invite>` : ''}
722
+ </specialists>
723
+
724
+ <tagging_syntax>
725
+ To @mention someone in Hailer, use this format:
726
+ [hailerTag|Display Name](userId)
727
+
728
+ ${availableSpecialists.length > 0 ? `Example: [hailerTag|${availableSpecialists[0][1].displayName || availableSpecialists[0][1].name}](${availableSpecialists[0][1].botUserId})` : 'Example: [hailerTag|User Name](userId)'}
729
+
730
+ For specialists, use their display name and userId from the list above.
731
+ Do NOT use plain @mentions like "@hailerExpert" - they won't work.
732
+ </tagging_syntax>
733
+
734
+ <giuseppe_autonomous>
735
+ **Giuseppe - Autonomous Bug Fixer**
736
+
737
+ Giuseppe is SPECIAL - he works AUTONOMOUSLY, not through invites!
738
+
739
+ **How Giuseppe works:**
740
+ 1. Users create a bug report in the "Bug Reports" workflow
741
+ 2. Giuseppe automatically detects new bugs and starts fixing them
742
+ 3. He analyzes the code, generates a fix, tests it
743
+ 4. He asks the user to verify with "approved" or "denied"
744
+ 5. On "approved" → Giuseppe publishes to production automatically
745
+ 6. On "denied" or complaint → YOU (HAL) gather info, then trigger Giuseppe retry
746
+
747
+ **YOUR ROLE IN BUG FIX DENIALS:**
748
+ When a discussion has a pending bug fix and the user expresses dissatisfaction:
749
+ 1. YOU handle the conversation - ask what's wrong, what behavior they expected
750
+ 2. Gather enough context: what's broken, expected vs actual behavior
751
+ 3. Once you have clear info, use trigger_giuseppe_retry tool to pass the explanation
752
+ 4. Giuseppe will receive your gathered info and retry the fix
753
+
754
+ **Detecting denial signals:**
755
+ - "denied", "not working", "still broken", "wrong", "doesn't work"
756
+ - Any complaint about the fix behavior
757
+ - User describing unexpected behavior
758
+
759
+ **Gathering info (ask ONE question at a time):**
760
+ - "What behavior are you seeing?"
761
+ - "What did you expect to happen instead?"
762
+ - "Is the issue consistent or intermittent?"
763
+
764
+ **When to trigger retry:**
765
+ Once you have: (1) what's wrong, (2) expected behavior → trigger the retry
766
+
767
+ **When someone mentions bugs (NEW bugs):**
768
+ - Tell them to create a bug report in "Bug Reports" workflow
769
+ - Explain Giuseppe will pick it up automatically
770
+ - Offer to help create the bug report for them
771
+ </giuseppe_autonomous>
772
+
773
+ <your_tools>
774
+ You have access to basic MCP tools for simple operations:
775
+ - list_workflows, list_workflow_phases, get_workflow_schema
776
+ - list_activities, show_activity_by_id, count_activities
777
+ - create_activity, update_activity (single operations)
778
+ - search_workspace_users
779
+ - Discussion tools (join_discussion, add_discussion_message, invite_discussion_members)
780
+
781
+ For complex operations (workflow creation, insights, bulk ops), invite a specialist.
782
+
783
+ **CRITICAL for join_discussion when inviting users:**
784
+ ALWAYS pass these parameters from the incoming message:
785
+ - inviteUserId = user_id attribute
786
+ - sourceActivityId = activity_id attribute (creates "came from" link!)
787
+ - welcomeReason = why they're being invited
788
+
789
+ **join_discussion ID types:**
790
+ - HailerTags like [hailerTag|Name](ID) usually contain ACTIVITY IDs, not discussion IDs
791
+ - When user references an activity/player/customer by tag, use: activityId parameter
792
+ - Only use discussionId for direct discussion links (rare)
793
+ - The tool auto-detects: if discussionId fails, it tries as activityId
794
+
795
+ **SILENT SUCCESS for join_discussion:**
796
+ After successfully joining a discussion, do NOT post a confirmation message back to the source chat.
797
+ The action is self-evident (bot appears in target discussion). Only report ERRORS back to source chat.
798
+
799
+ **CRITICAL - Extract IDs from context:**
800
+ The <incoming> tag contains IDs. ALWAYS use them:
801
+ - activityId attribute → pass to show_activity_by_id, update_activity
802
+ - discussionId attribute → pass to get_activity_from_discussion, add_discussion_message
803
+
804
+ **NEVER call tools with empty parameters {}** - always extract from context first.
805
+
806
+ **update_activity formats:**
807
+ - Single mode: use \`activityId\` parameter
808
+ - Bulk mode (activities array): use \`_id\` inside each object, NOT \`activityId\`
809
+
810
+ **Field values must be plain:**
811
+ - numericunit: just the number → \`78\` (NOT \`{"type":"numericunit","value":78}\`)
812
+ - text: just the string → \`"hello"\`
813
+ - activitylink: just the ID → \`"abc123..."\`
814
+ </your_tools>
815
+
816
+ <tagging>
817
+ **Activity Tags:** #ACTIVITY_ID (24-char hex)
818
+ - Correct: "Check out #691ffe874217e9e8434e57fc"
819
+ - Wrong: "Check out #691ffe874217e9e8434e57fc (Name)" - name auto-displays
820
+
821
+ **User Mentions:** @"Full Name" or @userId
822
+ </tagging>
823
+
824
+ <agentic_loop>
825
+ **CRITICAL: Complete work before responding!**
826
+
827
+ You work in a single-turn loop. When you respond with <respond>, your turn ENDS.
828
+ There is NO "next turn" unless a new message arrives.
829
+
830
+ **WRONG - promising future work:**
831
+ <respond discussion="...">
832
+ I found 29 players. Let me check which ones are injured...
833
+ </respond>
834
+ → This posts the message but NEVER calls any tools!
835
+
836
+ **RIGHT - complete the work first:**
837
+ 1. Call list_activities with injury filter
838
+ 2. Get results
839
+ 3. THEN respond with the actual answer:
840
+ <respond discussion="...">
841
+ Found 3 injured players: #player1, #player2, #player3
842
+ </respond>
843
+
844
+ **RULE: Never say "let me check" or "give me a second" - just DO IT by calling tools.**
845
+ Only use <respond> when you have the FINAL answer to share.
846
+ </agentic_loop>
847
+
848
+ <rules>
849
+ - Be concise - snappy responses, not essays
850
+ - Use your memory - reference past conversations
851
+ - For HIGH priority: respond immediately
852
+ - For NORMAL priority: your DEFAULT is <ignore/>. Only break if you see a clear workspace question.
853
+ - **Gibberish test:** No recognizable words, no vowels, repeated patterns = <ignore/>
854
+ - **Relevance test:** Is this about workflows/activities/insights? If not = <ignore/>
855
+ - When inviting specialists, explain to the user what's happening
856
+ - After specialist responds, summarize for the user if needed
857
+ - **Complete work before responding** - Never say "I'm going to do X", just DO X with tool calls
858
+ </rules>
859
+
860
+ ${this.currentDiscussionId ? this.getPendingFixContext(this.currentDiscussionId) : ''}
861
+ ${this.currentDiscussionId ? this.getPendingClassificationContext(this.currentDiscussionId) : ''}`;
862
+ }
863
+ /**
864
+ * Override response handling to detect specialist invitations
865
+ */
866
+ async handleLlmResponse(response, originalMessage) {
867
+ // Get conversation for this discussion
868
+ const conversation = this.conversationManager.getConversation(originalMessage.discussionId);
869
+ // Add assistant response to conversation
870
+ // Cast response content to MessageParam content type (ContentBlock[] → ContentBlockParam[])
871
+ conversation.push({
872
+ role: "assistant",
873
+ content: response.content,
874
+ });
875
+ // Check for specialist invitation in text content
876
+ const textBlocks = response.content.filter((block) => block.type === "text");
877
+ const textContent = textBlocks.map((b) => b.text).join("\n");
878
+ // Look for <invite specialist="...">...</invite> pattern
879
+ const inviteMatch = textContent.match(/<invite specialist="(\w+)">([\s\S]*?)<\/invite>/);
880
+ if (inviteMatch) {
881
+ const [_fullMatch, specialistKey, handoffContext] = inviteMatch;
882
+ const specialist = this.specialists.get(specialistKey);
883
+ if (specialist && specialist.botUserId) {
884
+ this.orchestratorLogger.info("LLM requested specialist invitation", {
885
+ specialist: specialistKey,
886
+ discussionId: originalMessage.discussionId,
887
+ });
888
+ // Extract any text before the invite tag to post as acknowledgment
889
+ const preInviteText = textContent
890
+ .substring(0, textContent.indexOf("<invite"))
891
+ .trim();
892
+ // Check for respond wrapper
893
+ const respondMatch = preInviteText.match(/<respond discussion="([^"]+)">([\s\S]*)/);
894
+ if (respondMatch) {
895
+ const [, discussionId, content] = respondMatch;
896
+ const cleanContent = content.replace(/<\/respond>.*$/s, "").trim();
897
+ if (cleanContent) {
898
+ await this.postResponse(discussionId, cleanContent);
899
+ }
900
+ }
901
+ else if (preInviteText) {
902
+ // No respond wrapper, but there's text - might be for high priority
903
+ if (originalMessage.priority === "high") {
904
+ await this.postResponse(originalMessage.discussionId, preInviteText);
905
+ }
906
+ }
907
+ // Invite the specialist
908
+ const invited = await this.inviteSpecialist(specialist, originalMessage.discussionId, handoffContext.trim());
909
+ if (!invited) {
910
+ // Failed to invite - let user know
911
+ await this.postResponse(originalMessage.discussionId, `I tried to bring in ${specialist.name} but couldn't reach them. Let me try handling this myself...`);
912
+ // Continue with normal handling
913
+ }
914
+ return; // Don't continue with normal response handling
915
+ }
916
+ else {
917
+ // Specialist is disabled - feed back to LLM for natural response
918
+ this.orchestratorLogger.info("Specialist disabled, asking LLM to respond naturally", {
919
+ key: specialistKey,
920
+ name: specialist?.name || specialistKey,
921
+ });
922
+ // Add system feedback to conversation so LLM knows the specialist is unavailable
923
+ const conversation = this.conversationManager.getConversation(originalMessage.discussionId);
924
+ conversation.push({
925
+ role: 'user',
926
+ content: `[SYSTEM: The specialist "${specialist?.name || specialistKey}" you tried to invite is currently disabled. Please inform the user naturally that this specialist is not available and they can enable it in the AI Hub settings if needed. Do NOT use XML tags in your response.]`,
927
+ });
928
+ // Get LLM to respond naturally about the unavailability
929
+ const retryResponse = await this.client.messages.create({
930
+ model: this.config.model || 'claude-haiku-4-5-20251001',
931
+ max_tokens: 1000,
932
+ system: this.getSystemPrompt(),
933
+ messages: conversation,
934
+ tools: this.getTools(),
935
+ });
936
+ // Process the retry response (should be a simple text response)
937
+ await this.handleLlmResponse(retryResponse, originalMessage);
938
+ return;
939
+ }
940
+ }
941
+ // Check for tool calls
942
+ const toolUseBlocks = response.content.filter((block) => block.type === "tool_use");
943
+ if (toolUseBlocks.length > 0) {
944
+ // Mark that tools were used - subsequent responses should be posted
945
+ this.toolsUsedInCurrentMessage = true;
946
+ // Track which tools were used (for silent success detection)
947
+ this.lastToolsUsed = toolUseBlocks.map(b => b.name);
948
+ // Execute tools and continue conversation
949
+ await this.executeToolsAndContinue(toolUseBlocks, originalMessage);
950
+ return;
951
+ }
952
+ // Check for regular response (no invite, no tools)
953
+ if (textBlocks.length > 0) {
954
+ const responseText = textContent.trim();
955
+ this.orchestratorLogger.info("LLM raw response", {
956
+ discussion: originalMessage.discussionId,
957
+ priority: originalMessage.priority,
958
+ responseLength: responseText.length,
959
+ fullResponse: responseText.substring(0, 500),
960
+ hasIgnoreTag: responseText.includes("<ignore"),
961
+ hasRespondTag: responseText.includes("<respond"),
962
+ });
963
+ // Check for IGNORE decision - remove from context to keep it clean
964
+ if (responseText.includes("<decision>IGNORE</decision>") ||
965
+ responseText.includes("<ignore")) {
966
+ this.orchestratorLogger.debug("LLM decided to ignore - removing from context", {
967
+ discussion: originalMessage.discussionId,
968
+ });
969
+ // Stop typing indicator
970
+ this.stopTypingIndicator();
971
+ // Remove both the assistant's response AND the incoming message from context
972
+ conversation.pop(); // Remove assistant response we just added
973
+ conversation.pop(); // Remove the incoming message
974
+ this.toolsUsedInCurrentMessage = false; // Reset for next message
975
+ return;
976
+ }
977
+ // Check for explicit response directive
978
+ const responseMatch = responseText.match(/<respond discussion="([^"]+)">([\s\S]*?)<\/respond>/);
979
+ if (responseMatch) {
980
+ const targetDiscussion = responseMatch[1];
981
+ const content = responseMatch[2].trim();
982
+ // SILENT SUCCESS: Skip confirmation if we just ran a silent tool successfully (not on error)
983
+ const usedSilentTool = this.lastToolsUsed.some(t => OrchestratorDaemon.SILENT_SUCCESS_TOOLS.has(t));
984
+ if (usedSilentTool && !this.lastToolsFailed) {
985
+ this.orchestratorLogger.debug("Suppressing confirmation for silent success tool", {
986
+ discussion: originalMessage.discussionId,
987
+ tools: this.lastToolsUsed,
988
+ });
989
+ // Stop typing indicator
990
+ this.stopTypingIndicator();
991
+ // NOTE: Don't pop from context - keep conversation valid for future messages
992
+ this.toolsUsedInCurrentMessage = false;
993
+ this.lastToolsUsed = [];
994
+ this.lastToolsFailed = false;
995
+ return;
996
+ }
997
+ await this.postResponse(targetDiscussion, content);
998
+ this.toolsUsedInCurrentMessage = false; // Reset for next message
999
+ this.lastToolsUsed = [];
1000
+ this.lastToolsFailed = false;
1001
+ return;
1002
+ }
1003
+ // For high priority messages, always send substantive responses
1004
+ // (unless we just ran a silent success tool)
1005
+ if (originalMessage.priority === "high" &&
1006
+ responseText &&
1007
+ !responseText.includes("<thinking>") &&
1008
+ !responseText.startsWith("I'll") &&
1009
+ responseText.length > 10) {
1010
+ // SILENT SUCCESS: Skip confirmation if we just ran a silent tool successfully (not on error)
1011
+ const usedSilentTool = this.lastToolsUsed.some(t => OrchestratorDaemon.SILENT_SUCCESS_TOOLS.has(t));
1012
+ if (usedSilentTool && !this.lastToolsFailed) {
1013
+ this.orchestratorLogger.debug("Suppressing high-priority confirmation for silent success tool", {
1014
+ discussion: originalMessage.discussionId,
1015
+ tools: this.lastToolsUsed,
1016
+ });
1017
+ // Stop typing indicator
1018
+ this.stopTypingIndicator();
1019
+ // NOTE: Don't pop from context - keep conversation valid for future messages
1020
+ this.toolsUsedInCurrentMessage = false;
1021
+ this.lastToolsUsed = [];
1022
+ this.lastToolsFailed = false;
1023
+ return;
1024
+ }
1025
+ await this.postResponse(originalMessage.discussionId, responseText);
1026
+ this.toolsUsedInCurrentMessage = false; // Reset for next message
1027
+ this.lastToolsUsed = [];
1028
+ this.lastToolsFailed = false;
1029
+ return;
1030
+ }
1031
+ // For normal priority:
1032
+ // - If tools were used, post substantive responses (LLM was doing real work)
1033
+ // - Otherwise, require explicit <respond> tag
1034
+ if (originalMessage.priority === "normal") {
1035
+ // SILENT SUCCESS: Skip confirmation if we just ran a silent tool successfully (not on error)
1036
+ const usedSilentTool = this.lastToolsUsed.some(t => OrchestratorDaemon.SILENT_SUCCESS_TOOLS.has(t));
1037
+ const shouldSuppressSilent = usedSilentTool && !this.lastToolsFailed;
1038
+ if (this.toolsUsedInCurrentMessage && responseText.length > 50 && !responseText.includes("<ignore") && !shouldSuppressSilent) {
1039
+ // Tools were used and we have a substantive response - post it
1040
+ this.orchestratorLogger.info("Posting tool-assisted response", {
1041
+ discussion: originalMessage.discussionId,
1042
+ responseLength: responseText.length,
1043
+ });
1044
+ await this.postResponse(originalMessage.discussionId, responseText);
1045
+ this.toolsUsedInCurrentMessage = false; // Reset for next message
1046
+ this.lastToolsUsed = [];
1047
+ this.lastToolsFailed = false;
1048
+ return;
1049
+ }
1050
+ if (shouldSuppressSilent) {
1051
+ this.orchestratorLogger.debug("Suppressing normal-priority confirmation for silent success tool", {
1052
+ discussion: originalMessage.discussionId,
1053
+ tools: this.lastToolsUsed,
1054
+ });
1055
+ // Stop typing indicator
1056
+ this.stopTypingIndicator();
1057
+ // NOTE: Don't pop from context when tools were used - keep conversation valid
1058
+ this.toolsUsedInCurrentMessage = false;
1059
+ this.lastToolsUsed = [];
1060
+ this.lastToolsFailed = false;
1061
+ return;
1062
+ }
1063
+ this.orchestratorLogger.debug("Normal priority without <respond> tag - removing from context", {
1064
+ discussion: originalMessage.discussionId,
1065
+ responsePreview: responseText.substring(0, 100),
1066
+ toolsUsed: this.toolsUsedInCurrentMessage,
1067
+ });
1068
+ // Stop typing indicator
1069
+ this.stopTypingIndicator();
1070
+ // Remove from context - we didn't respond (only safe when no tools were used)
1071
+ if (!this.toolsUsedInCurrentMessage) {
1072
+ conversation.pop(); // Remove assistant response
1073
+ conversation.pop(); // Remove incoming message
1074
+ }
1075
+ this.toolsUsedInCurrentMessage = false; // Reset for next message
1076
+ this.lastToolsUsed = [];
1077
+ this.lastToolsFailed = false;
1078
+ }
1079
+ }
1080
+ }
1081
+ /**
1082
+ * Get orchestrator status including specialist info
1083
+ */
1084
+ getOrchestratorStatus() {
1085
+ const specialists = Array.from(this.specialists.entries()).map(([key, spec]) => ({
1086
+ key,
1087
+ name: spec.name,
1088
+ available: !!spec.botUserId,
1089
+ userId: spec.botUserId,
1090
+ }));
1091
+ const activeInDiscussions = {};
1092
+ for (const [discussionId, userIds] of this.activeSpecialistsInDiscussion) {
1093
+ activeInDiscussions[discussionId] = Array.from(userIds);
1094
+ }
1095
+ return {
1096
+ conversationState: this.getConversationState(),
1097
+ specialists,
1098
+ activeInDiscussions,
1099
+ };
1100
+ }
1101
+ }
1102
+ exports.OrchestratorDaemon = OrchestratorDaemon;
1103
+ //# sourceMappingURL=daemon.js.map