@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.
- package/dist/app.js +27 -20
- package/dist/core.d.ts +33 -9
- package/dist/core.js +279 -147
- package/dist/mcp/UserContextCache.js +18 -0
- package/dist/mcp/hailer-clients.d.ts +9 -1
- package/dist/mcp/hailer-clients.js +13 -3
- package/dist/mcp/signal-handler.js +1 -1
- package/dist/mcp/tool-registry.d.ts +3 -1
- package/dist/mcp/tool-registry.js +4 -1
- package/dist/mcp/tools/activity.js +43 -34
- package/dist/mcp/tools/bot-config/constants.d.ts +23 -0
- package/dist/mcp/tools/bot-config/constants.js +94 -0
- package/dist/mcp/tools/{bot-config.d.ts → bot-config/core.d.ts} +6 -6
- package/dist/mcp/tools/{bot-config.js → bot-config/core.js} +15 -15
- package/dist/mcp/tools/bot-config/index.d.ts +10 -0
- package/dist/mcp/tools/bot-config/index.js +59 -0
- package/dist/mcp/tools/bot-config/tools.d.ts +7 -0
- package/dist/mcp/tools/bot-config/tools.js +15 -0
- package/dist/mcp/tools/bot-config/types.d.ts +50 -0
- package/dist/mcp/tools/bot-config/types.js +6 -0
- package/dist/mcp/tools/bug-fixer-tools.d.ts +21 -0
- package/dist/mcp/tools/{giuseppe-tools.js → bug-fixer-tools.js} +61 -61
- package/dist/mcp/tools/user.js +10 -29
- package/dist/mcp/tools/workflow.js +36 -2
- package/dist/mcp/utils/data-transformers.d.ts +0 -8
- package/dist/mcp/utils/data-transformers.js +0 -28
- package/dist/mcp/utils/index.d.ts +4 -1
- package/dist/mcp/utils/index.js +17 -3
- package/dist/mcp/utils/pagination.d.ts +40 -0
- package/dist/mcp/utils/pagination.js +55 -0
- package/dist/mcp/utils/response-builder.d.ts +53 -0
- package/dist/mcp/utils/response-builder.js +110 -0
- package/dist/mcp/utils/tool-helpers.d.ts +0 -8
- package/dist/mcp/utils/tool-helpers.js +0 -24
- package/dist/mcp/utils/types.d.ts +1 -33
- package/dist/mcp-server.d.ts +2 -2
- package/dist/mcp-server.js +161 -139
- package/package.json +1 -1
- package/REFACTOR_STATUS.md +0 -127
- package/dist/agents/bot-manager.d.ts +0 -48
- package/dist/agents/bot-manager.js +0 -254
- package/dist/agents/factory.d.ts +0 -150
- package/dist/agents/factory.js +0 -650
- package/dist/agents/giuseppe/ai.d.ts +0 -83
- package/dist/agents/giuseppe/ai.js +0 -466
- package/dist/agents/giuseppe/bot.d.ts +0 -110
- package/dist/agents/giuseppe/bot.js +0 -780
- package/dist/agents/giuseppe/config.d.ts +0 -25
- package/dist/agents/giuseppe/config.js +0 -227
- package/dist/agents/giuseppe/files.d.ts +0 -52
- package/dist/agents/giuseppe/files.js +0 -338
- package/dist/agents/giuseppe/git.d.ts +0 -48
- package/dist/agents/giuseppe/git.js +0 -298
- package/dist/agents/giuseppe/index.d.ts +0 -97
- package/dist/agents/giuseppe/index.js +0 -258
- package/dist/agents/giuseppe/lsp.d.ts +0 -113
- package/dist/agents/giuseppe/lsp.js +0 -485
- package/dist/agents/giuseppe/monitor.d.ts +0 -118
- package/dist/agents/giuseppe/monitor.js +0 -621
- package/dist/agents/giuseppe/prompt.d.ts +0 -5
- package/dist/agents/giuseppe/prompt.js +0 -94
- package/dist/agents/giuseppe/registries/pending-classification.d.ts +0 -28
- package/dist/agents/giuseppe/registries/pending-classification.js +0 -50
- package/dist/agents/giuseppe/registries/pending-fix.d.ts +0 -30
- package/dist/agents/giuseppe/registries/pending-fix.js +0 -42
- package/dist/agents/giuseppe/registries/pending.d.ts +0 -27
- package/dist/agents/giuseppe/registries/pending.js +0 -49
- package/dist/agents/giuseppe/specialist.d.ts +0 -47
- package/dist/agents/giuseppe/specialist.js +0 -237
- package/dist/agents/giuseppe/types.d.ts +0 -123
- package/dist/agents/giuseppe/types.js +0 -9
- package/dist/agents/hailer-expert/index.d.ts +0 -8
- package/dist/agents/hailer-expert/index.js +0 -14
- package/dist/agents/hal/daemon.d.ts +0 -142
- package/dist/agents/hal/daemon.js +0 -1103
- package/dist/agents/hal/definitions.d.ts +0 -55
- package/dist/agents/hal/definitions.js +0 -263
- package/dist/agents/hal/index.d.ts +0 -3
- package/dist/agents/hal/index.js +0 -8
- package/dist/agents/index.d.ts +0 -18
- package/dist/agents/index.js +0 -48
- package/dist/agents/shared/base.d.ts +0 -216
- package/dist/agents/shared/base.js +0 -846
- package/dist/agents/shared/services/agent-registry.d.ts +0 -107
- package/dist/agents/shared/services/agent-registry.js +0 -629
- package/dist/agents/shared/services/conversation-manager.d.ts +0 -50
- package/dist/agents/shared/services/conversation-manager.js +0 -136
- package/dist/agents/shared/services/mcp-client.d.ts +0 -56
- package/dist/agents/shared/services/mcp-client.js +0 -124
- package/dist/agents/shared/services/message-classifier.d.ts +0 -37
- package/dist/agents/shared/services/message-classifier.js +0 -187
- package/dist/agents/shared/services/message-formatter.d.ts +0 -89
- package/dist/agents/shared/services/message-formatter.js +0 -371
- package/dist/agents/shared/services/session-logger.d.ts +0 -106
- package/dist/agents/shared/services/session-logger.js +0 -446
- package/dist/agents/shared/services/tool-executor.d.ts +0 -41
- package/dist/agents/shared/services/tool-executor.js +0 -169
- package/dist/agents/shared/services/workspace-schema-cache.d.ts +0 -125
- package/dist/agents/shared/services/workspace-schema-cache.js +0 -578
- package/dist/agents/shared/specialist.d.ts +0 -91
- package/dist/agents/shared/specialist.js +0 -399
- package/dist/agents/shared/tool-schema-loader.d.ts +0 -62
- package/dist/agents/shared/tool-schema-loader.js +0 -232
- package/dist/agents/shared/types.d.ts +0 -327
- package/dist/agents/shared/types.js +0 -121
- package/dist/client/agents/base.d.ts +0 -207
- package/dist/client/agents/base.js +0 -744
- package/dist/client/agents/definitions.d.ts +0 -53
- package/dist/client/agents/definitions.js +0 -263
- package/dist/client/agents/orchestrator.d.ts +0 -141
- package/dist/client/agents/orchestrator.js +0 -1062
- package/dist/client/agents/specialist.d.ts +0 -86
- package/dist/client/agents/specialist.js +0 -340
- package/dist/client/bot-entrypoint.d.ts +0 -7
- package/dist/client/bot-entrypoint.js +0 -103
- package/dist/client/bot-manager.d.ts +0 -44
- package/dist/client/bot-manager.js +0 -173
- package/dist/client/bot-runner.d.ts +0 -35
- package/dist/client/bot-runner.js +0 -188
- package/dist/client/chat-agent-daemon.d.ts +0 -464
- package/dist/client/chat-agent-daemon.js +0 -1774
- package/dist/client/daemon-factory.d.ts +0 -106
- package/dist/client/daemon-factory.js +0 -301
- package/dist/client/factory.d.ts +0 -111
- package/dist/client/factory.js +0 -314
- package/dist/client/index.d.ts +0 -17
- package/dist/client/index.js +0 -38
- package/dist/client/multi-bot-manager.d.ts +0 -42
- package/dist/client/multi-bot-manager.js +0 -161
- package/dist/client/orchestrator-daemon.d.ts +0 -87
- package/dist/client/orchestrator-daemon.js +0 -444
- package/dist/client/server.d.ts +0 -8
- package/dist/client/server.js +0 -251
- package/dist/client/services/agent-registry.d.ts +0 -108
- package/dist/client/services/agent-registry.js +0 -630
- package/dist/client/services/conversation-manager.d.ts +0 -50
- package/dist/client/services/conversation-manager.js +0 -136
- package/dist/client/services/mcp-client.d.ts +0 -48
- package/dist/client/services/mcp-client.js +0 -105
- package/dist/client/services/message-classifier.d.ts +0 -37
- package/dist/client/services/message-classifier.js +0 -187
- package/dist/client/services/message-formatter.d.ts +0 -84
- package/dist/client/services/message-formatter.js +0 -353
- package/dist/client/services/session-logger.d.ts +0 -106
- package/dist/client/services/session-logger.js +0 -446
- package/dist/client/services/tool-executor.d.ts +0 -41
- package/dist/client/services/tool-executor.js +0 -169
- package/dist/client/services/workspace-schema-cache.d.ts +0 -149
- package/dist/client/services/workspace-schema-cache.js +0 -732
- package/dist/client/specialist-daemon.d.ts +0 -77
- package/dist/client/specialist-daemon.js +0 -197
- package/dist/client/specialists.d.ts +0 -53
- package/dist/client/specialists.js +0 -178
- package/dist/client/tool-schema-loader.d.ts +0 -62
- package/dist/client/tool-schema-loader.js +0 -232
- package/dist/client/types.d.ts +0 -327
- package/dist/client/types.js +0 -121
- package/dist/commands/seed-config.d.ts +0 -9
- package/dist/commands/seed-config.js +0 -372
- package/dist/lib/context-manager.d.ts +0 -111
- package/dist/lib/context-manager.js +0 -431
- package/dist/lib/prompt-length-manager.d.ts +0 -81
- package/dist/lib/prompt-length-manager.js +0 -457
- package/dist/mcp/tools/giuseppe-tools.d.ts +0 -21
- package/dist/modules/bug-reports/bug-config.d.ts +0 -25
- package/dist/modules/bug-reports/bug-config.js +0 -187
- package/dist/modules/bug-reports/bug-monitor.d.ts +0 -108
- package/dist/modules/bug-reports/bug-monitor.js +0 -510
- package/dist/modules/bug-reports/giuseppe-agent.d.ts +0 -58
- package/dist/modules/bug-reports/giuseppe-agent.js +0 -467
- package/dist/modules/bug-reports/giuseppe-ai.d.ts +0 -83
- package/dist/modules/bug-reports/giuseppe-ai.js +0 -466
- package/dist/modules/bug-reports/giuseppe-bot.d.ts +0 -110
- package/dist/modules/bug-reports/giuseppe-bot.js +0 -804
- package/dist/modules/bug-reports/giuseppe-daemon.d.ts +0 -80
- package/dist/modules/bug-reports/giuseppe-daemon.js +0 -617
- package/dist/modules/bug-reports/giuseppe-files.d.ts +0 -64
- package/dist/modules/bug-reports/giuseppe-files.js +0 -375
- package/dist/modules/bug-reports/giuseppe-git.d.ts +0 -48
- package/dist/modules/bug-reports/giuseppe-git.js +0 -298
- package/dist/modules/bug-reports/giuseppe-lsp.d.ts +0 -113
- package/dist/modules/bug-reports/giuseppe-lsp.js +0 -485
- package/dist/modules/bug-reports/giuseppe-prompt.d.ts +0 -5
- package/dist/modules/bug-reports/giuseppe-prompt.js +0 -94
- package/dist/modules/bug-reports/index.d.ts +0 -77
- package/dist/modules/bug-reports/index.js +0 -215
- package/dist/modules/bug-reports/pending-classification-registry.d.ts +0 -28
- package/dist/modules/bug-reports/pending-classification-registry.js +0 -50
- package/dist/modules/bug-reports/pending-fix-registry.d.ts +0 -30
- package/dist/modules/bug-reports/pending-fix-registry.js +0 -42
- package/dist/modules/bug-reports/pending-registry.d.ts +0 -27
- package/dist/modules/bug-reports/pending-registry.js +0 -49
- package/dist/modules/bug-reports/types.d.ts +0 -123
- package/dist/modules/bug-reports/types.js +0 -9
- package/dist/routes/agents.d.ts +0 -44
- package/dist/routes/agents.js +0 -311
- package/dist/services/agent-credential-store.d.ts +0 -73
- package/dist/services/agent-credential-store.js +0 -212
- package/dist/services/bug-monitor.d.ts +0 -23
- 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
|