@hailer/mcp 0.1.14 → 0.1.16
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/.claude/agents/agent-giuseppe-app-builder.md +7 -6
- package/.claude/agents/agent-lars-code-inspector.md +26 -14
- package/dist/agents/bot-manager.d.ts +48 -0
- package/dist/agents/bot-manager.js +254 -0
- package/dist/agents/factory.d.ts +150 -0
- package/dist/agents/factory.js +650 -0
- package/dist/agents/giuseppe/ai.d.ts +83 -0
- package/dist/agents/giuseppe/ai.js +466 -0
- package/dist/agents/giuseppe/bot.d.ts +110 -0
- package/dist/agents/giuseppe/bot.js +780 -0
- package/dist/agents/giuseppe/config.d.ts +25 -0
- package/dist/agents/giuseppe/config.js +227 -0
- package/dist/agents/giuseppe/files.d.ts +52 -0
- package/dist/agents/giuseppe/files.js +338 -0
- package/dist/agents/giuseppe/git.d.ts +48 -0
- package/dist/agents/giuseppe/git.js +298 -0
- package/dist/agents/giuseppe/index.d.ts +97 -0
- package/dist/agents/giuseppe/index.js +258 -0
- package/dist/agents/giuseppe/lsp.d.ts +113 -0
- package/dist/agents/giuseppe/lsp.js +485 -0
- package/dist/agents/giuseppe/monitor.d.ts +118 -0
- package/dist/agents/giuseppe/monitor.js +621 -0
- package/dist/agents/giuseppe/prompt.d.ts +5 -0
- package/dist/agents/giuseppe/prompt.js +94 -0
- package/dist/agents/giuseppe/registries/pending-classification.d.ts +28 -0
- package/dist/agents/giuseppe/registries/pending-classification.js +50 -0
- package/dist/agents/giuseppe/registries/pending-fix.d.ts +30 -0
- package/dist/agents/giuseppe/registries/pending-fix.js +42 -0
- package/dist/agents/giuseppe/registries/pending.d.ts +27 -0
- package/dist/agents/giuseppe/registries/pending.js +49 -0
- package/dist/agents/giuseppe/specialist.d.ts +47 -0
- package/dist/agents/giuseppe/specialist.js +237 -0
- package/dist/agents/giuseppe/types.d.ts +123 -0
- package/dist/agents/giuseppe/types.js +9 -0
- package/dist/agents/hailer-expert/index.d.ts +8 -0
- package/dist/agents/hailer-expert/index.js +14 -0
- package/dist/agents/hal/daemon.d.ts +142 -0
- package/dist/agents/hal/daemon.js +1103 -0
- package/dist/agents/hal/definitions.d.ts +55 -0
- package/dist/agents/hal/definitions.js +263 -0
- package/dist/agents/hal/index.d.ts +3 -0
- package/dist/agents/hal/index.js +8 -0
- package/dist/agents/index.d.ts +18 -0
- package/dist/agents/index.js +48 -0
- package/dist/agents/shared/base.d.ts +216 -0
- package/dist/agents/shared/base.js +846 -0
- package/dist/agents/shared/services/agent-registry.d.ts +107 -0
- package/dist/agents/shared/services/agent-registry.js +629 -0
- package/dist/agents/shared/services/conversation-manager.d.ts +50 -0
- package/dist/agents/shared/services/conversation-manager.js +136 -0
- package/dist/agents/shared/services/mcp-client.d.ts +56 -0
- package/dist/agents/shared/services/mcp-client.js +124 -0
- package/dist/agents/shared/services/message-classifier.d.ts +37 -0
- package/dist/agents/shared/services/message-classifier.js +187 -0
- package/dist/agents/shared/services/message-formatter.d.ts +89 -0
- package/dist/agents/shared/services/message-formatter.js +371 -0
- package/dist/agents/shared/services/session-logger.d.ts +106 -0
- package/dist/agents/shared/services/session-logger.js +446 -0
- package/dist/agents/shared/services/tool-executor.d.ts +41 -0
- package/dist/agents/shared/services/tool-executor.js +169 -0
- package/dist/agents/shared/services/workspace-schema-cache.d.ts +125 -0
- package/dist/agents/shared/services/workspace-schema-cache.js +578 -0
- package/dist/agents/shared/specialist.d.ts +91 -0
- package/dist/agents/shared/specialist.js +399 -0
- package/dist/agents/shared/tool-schema-loader.d.ts +62 -0
- package/dist/agents/shared/tool-schema-loader.js +232 -0
- package/dist/agents/shared/types.d.ts +327 -0
- package/dist/agents/shared/types.js +121 -0
- package/dist/app.js +21 -4
- package/dist/cli.js +0 -0
- package/dist/client/agents/orchestrator.d.ts +1 -0
- package/dist/client/agents/orchestrator.js +12 -1
- package/dist/commands/seed-config.d.ts +9 -0
- package/dist/commands/seed-config.js +372 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +61 -1
- package/dist/core.d.ts +8 -0
- package/dist/core.js +137 -6
- package/dist/lib/discussion-lock.d.ts +42 -0
- package/dist/lib/discussion-lock.js +110 -0
- package/dist/mcp/UserContextCache.js +2 -2
- package/dist/mcp/hailer-clients.d.ts +15 -0
- package/dist/mcp/hailer-clients.js +100 -6
- package/dist/mcp/signal-handler.d.ts +16 -5
- package/dist/mcp/signal-handler.js +173 -122
- package/dist/mcp/tools/activity.js +9 -1
- package/dist/mcp/tools/bot-config.d.ts +184 -9
- package/dist/mcp/tools/bot-config.js +2177 -163
- package/dist/mcp/tools/giuseppe-tools.d.ts +21 -0
- package/dist/mcp/tools/giuseppe-tools.js +525 -0
- package/dist/mcp/utils/hailer-api-client.d.ts +42 -1
- package/dist/mcp/utils/hailer-api-client.js +128 -2
- package/dist/mcp/webhook-handler.d.ts +87 -0
- package/dist/mcp/webhook-handler.js +343 -0
- package/dist/mcp/workspace-cache.d.ts +5 -0
- package/dist/mcp/workspace-cache.js +11 -0
- package/dist/mcp-server.js +55 -5
- package/dist/modules/bug-reports/giuseppe-agent.d.ts +58 -0
- package/dist/modules/bug-reports/giuseppe-agent.js +467 -0
- package/dist/modules/bug-reports/giuseppe-ai.d.ts +25 -1
- package/dist/modules/bug-reports/giuseppe-ai.js +133 -2
- package/dist/modules/bug-reports/giuseppe-bot.d.ts +3 -2
- package/dist/modules/bug-reports/giuseppe-bot.js +75 -36
- package/dist/modules/bug-reports/giuseppe-daemon.d.ts +80 -0
- package/dist/modules/bug-reports/giuseppe-daemon.js +617 -0
- package/dist/modules/bug-reports/giuseppe-files.d.ts +12 -0
- package/dist/modules/bug-reports/giuseppe-files.js +37 -0
- package/dist/modules/bug-reports/giuseppe-lsp.d.ts +113 -0
- package/dist/modules/bug-reports/giuseppe-lsp.js +485 -0
- package/dist/modules/bug-reports/index.d.ts +1 -0
- package/dist/modules/bug-reports/index.js +31 -29
- package/package.json +5 -4
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Session Logger Service
|
|
4
|
+
*
|
|
5
|
+
* Manages per-activity session tracking and logging to Hailer workflows:
|
|
6
|
+
* - Tracks metrics (tokens, tool calls, messages)
|
|
7
|
+
* - Logs completed sessions to SESSION_LOG workflow
|
|
8
|
+
* - Manages idle session detection and flushing
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.SessionLoggerService = void 0;
|
|
12
|
+
const types_1 = require("../../shared/types");
|
|
13
|
+
class SessionLoggerService {
|
|
14
|
+
agentDirectoryId;
|
|
15
|
+
logger;
|
|
16
|
+
callMcpTool;
|
|
17
|
+
getDefaultTeamId;
|
|
18
|
+
activitySessions = new Map();
|
|
19
|
+
lastGlobalLogId = null;
|
|
20
|
+
idleCheckTimer = null;
|
|
21
|
+
anthropicClient = null;
|
|
22
|
+
schemaCache = null;
|
|
23
|
+
_currentWorkspaceId = null;
|
|
24
|
+
constructor(agentDirectoryId, logger, callMcpTool, getDefaultTeamId) {
|
|
25
|
+
this.agentDirectoryId = agentDirectoryId;
|
|
26
|
+
this.logger = logger;
|
|
27
|
+
this.callMcpTool = callMcpTool;
|
|
28
|
+
this.getDefaultTeamId = getDefaultTeamId;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Set the workspace schema cache for dynamic ID lookup
|
|
32
|
+
*/
|
|
33
|
+
setSchemaCache(cache, workspaceId) {
|
|
34
|
+
this.schemaCache = cache;
|
|
35
|
+
this._currentWorkspaceId = workspaceId;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get Session Log schema from cache for a specific workspace
|
|
39
|
+
*/
|
|
40
|
+
getSessionLogSchema(workspaceId) {
|
|
41
|
+
if (!this.schemaCache || !workspaceId) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return this.schemaCache.getSessionLogSchema(workspaceId) || null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Set Anthropic client for generating summaries
|
|
48
|
+
*/
|
|
49
|
+
setAnthropicClient(client) {
|
|
50
|
+
this.anthropicClient = client;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get or create an activity session for tracking
|
|
54
|
+
* Multi-tenant: workspaceId is required for proper schema lookup
|
|
55
|
+
*/
|
|
56
|
+
getOrCreateActivitySession(message) {
|
|
57
|
+
// Use linked activity ID as key, fallback to discussion ID
|
|
58
|
+
const sessionKey = message.linkedActivityId || message.discussionId;
|
|
59
|
+
let session = this.activitySessions.get(sessionKey);
|
|
60
|
+
if (!session) {
|
|
61
|
+
session = {
|
|
62
|
+
activityId: message.linkedActivityId || "",
|
|
63
|
+
activityName: message.linkedActivityName || `Discussion ${message.discussionId.substring(0, 8)}`,
|
|
64
|
+
discussionId: message.discussionId,
|
|
65
|
+
workspaceId: message.workspaceId, // Multi-tenant: store workspace for this session
|
|
66
|
+
startTime: Date.now(),
|
|
67
|
+
lastActivityTime: Date.now(),
|
|
68
|
+
metrics: {
|
|
69
|
+
inputTokens: 0,
|
|
70
|
+
outputTokens: 0,
|
|
71
|
+
toolCalls: 0,
|
|
72
|
+
writeOperations: 0,
|
|
73
|
+
messagesProcessed: 0,
|
|
74
|
+
responsesPosted: 0,
|
|
75
|
+
},
|
|
76
|
+
actions: [],
|
|
77
|
+
previousLogId: this.lastGlobalLogId,
|
|
78
|
+
conversation: [],
|
|
79
|
+
writeDetails: [],
|
|
80
|
+
triggerRequest: null,
|
|
81
|
+
requestedBy: null,
|
|
82
|
+
requestedById: null,
|
|
83
|
+
};
|
|
84
|
+
this.activitySessions.set(sessionKey, session);
|
|
85
|
+
this.logger.debug("Created new activity session", {
|
|
86
|
+
sessionKey,
|
|
87
|
+
workspaceId: message.workspaceId,
|
|
88
|
+
activityName: session.activityName,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return session;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get session by key
|
|
95
|
+
*/
|
|
96
|
+
getSession(key) {
|
|
97
|
+
return this.activitySessions.get(key);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Get all active sessions
|
|
101
|
+
*/
|
|
102
|
+
getActiveSessions() {
|
|
103
|
+
return new Map(this.activitySessions);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Check for idle sessions and flush them
|
|
107
|
+
* Called periodically by timer
|
|
108
|
+
*/
|
|
109
|
+
async checkAndFlushIdleSessions() {
|
|
110
|
+
const now = Date.now();
|
|
111
|
+
const sessionsToFlush = [];
|
|
112
|
+
for (const [key, session] of this.activitySessions) {
|
|
113
|
+
const idleTime = now - session.lastActivityTime;
|
|
114
|
+
if (idleTime >= types_1.SESSION_IDLE_TIMEOUT) {
|
|
115
|
+
sessionsToFlush.push(key);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const key of sessionsToFlush) {
|
|
119
|
+
const session = this.activitySessions.get(key);
|
|
120
|
+
if (session) {
|
|
121
|
+
await this.flushActivitySession(key, session);
|
|
122
|
+
this.activitySessions.delete(key);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (sessionsToFlush.length > 0) {
|
|
126
|
+
this.logger.debug("Flushed idle sessions", { count: sessionsToFlush.length });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Flush a single activity session to the session log
|
|
131
|
+
*/
|
|
132
|
+
async flushActivitySession(_key, session) {
|
|
133
|
+
// Only log if WRITE operations occurred (skip read-only sessions)
|
|
134
|
+
if (session.metrics.writeOperations === 0) {
|
|
135
|
+
this.logger.debug("Skipping session log - no write operations", {
|
|
136
|
+
activity: session.activityName,
|
|
137
|
+
toolCalls: session.metrics.toolCalls,
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// Get dynamic schema for this session's workspace
|
|
142
|
+
const schema = this.getSessionLogSchema(session.workspaceId);
|
|
143
|
+
if (!schema) {
|
|
144
|
+
this.logger.debug("Skipping session log - no Session Log workflow in workspace", {
|
|
145
|
+
workspaceId: session.workspaceId,
|
|
146
|
+
});
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (!this.agentDirectoryId) {
|
|
150
|
+
this.logger.debug("Skipping session log - no agent directory ID");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
// Build descriptive session name from first write operation
|
|
155
|
+
let sessionName;
|
|
156
|
+
if (session.writeDetails.length > 0) {
|
|
157
|
+
sessionName = session.writeDetails[0].what;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
sessionName = session.activityName || `Session ${new Date().toISOString()}`;
|
|
161
|
+
}
|
|
162
|
+
// Build context summary (now async - includes conversation summary)
|
|
163
|
+
const summary = await this.buildActivitySessionSummary(session);
|
|
164
|
+
// Calculate total tokens
|
|
165
|
+
const totalTokens = session.metrics.inputTokens + session.metrics.outputTokens;
|
|
166
|
+
// Get field IDs from schema (use our standard keys that were mapped)
|
|
167
|
+
const contextSummaryFieldId = schema.fields["contextSummary"] || schema.fields["Context summary"];
|
|
168
|
+
const madeByFieldId = schema.fields["madeBy"] || schema.fields["Log entry made by"];
|
|
169
|
+
const costFieldId = schema.fields["cost"] || schema.fields["Tokens Used"];
|
|
170
|
+
const previousLogFieldId = schema.fields["previousLog"] || schema.fields["Previus log entry"];
|
|
171
|
+
const linkedWorkFieldId = schema.fields["linkedWork"] || schema.fields["Most relevant link to work"];
|
|
172
|
+
// Build fields
|
|
173
|
+
const fields = {};
|
|
174
|
+
if (contextSummaryFieldId) {
|
|
175
|
+
fields[contextSummaryFieldId] = summary;
|
|
176
|
+
}
|
|
177
|
+
if (madeByFieldId && this.agentDirectoryId) {
|
|
178
|
+
fields[madeByFieldId] = this.agentDirectoryId;
|
|
179
|
+
}
|
|
180
|
+
if (costFieldId) {
|
|
181
|
+
fields[costFieldId] = totalTokens;
|
|
182
|
+
}
|
|
183
|
+
// Chain to previous log
|
|
184
|
+
if (session.previousLogId && previousLogFieldId) {
|
|
185
|
+
fields[previousLogFieldId] = session.previousLogId;
|
|
186
|
+
}
|
|
187
|
+
// Link to activity being worked on
|
|
188
|
+
if (session.activityId && linkedWorkFieldId) {
|
|
189
|
+
fields[linkedWorkFieldId] = session.activityId;
|
|
190
|
+
}
|
|
191
|
+
// Get phase ID (try "active" first, then first available)
|
|
192
|
+
const phaseId = schema.phases["active"] || Object.values(schema.phases)[0];
|
|
193
|
+
const createParams = {
|
|
194
|
+
workflowId: schema.workflowId,
|
|
195
|
+
name: sessionName,
|
|
196
|
+
phaseId,
|
|
197
|
+
fields,
|
|
198
|
+
};
|
|
199
|
+
const wsTeamId = this.getDefaultTeamId();
|
|
200
|
+
if (wsTeamId) {
|
|
201
|
+
createParams.teamId = wsTeamId;
|
|
202
|
+
}
|
|
203
|
+
const result = await this.callMcpTool("create_activity", createParams);
|
|
204
|
+
// Extract created ID for chaining
|
|
205
|
+
const resultText = result?.content?.[0]?.text;
|
|
206
|
+
if (resultText) {
|
|
207
|
+
const idMatch = resultText.match(/"_id":\s*"([a-f0-9]{24})"/i) ||
|
|
208
|
+
resultText.match(/`([a-f0-9]{24})`/);
|
|
209
|
+
if (idMatch) {
|
|
210
|
+
this.lastGlobalLogId = idMatch[1];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const durationSec = Math.round((Date.now() - session.startTime) / 1000);
|
|
214
|
+
this.logger.info("Activity session logged", {
|
|
215
|
+
activity: session.activityName,
|
|
216
|
+
durationSec,
|
|
217
|
+
messages: session.metrics.messagesProcessed,
|
|
218
|
+
toolCalls: session.metrics.toolCalls,
|
|
219
|
+
tokens: totalTokens,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
this.logger.warn("Failed to flush activity session", {
|
|
224
|
+
activity: session.activityName,
|
|
225
|
+
error: error instanceof Error ? error.message : String(error),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Flush all active sessions (called on shutdown)
|
|
231
|
+
*/
|
|
232
|
+
async flushAllSessions() {
|
|
233
|
+
for (const [key, session] of this.activitySessions) {
|
|
234
|
+
await this.flushActivitySession(key, session);
|
|
235
|
+
}
|
|
236
|
+
this.activitySessions.clear();
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Build summary for an activity session
|
|
240
|
+
* Now includes conversation summary for memory recall
|
|
241
|
+
*/
|
|
242
|
+
async buildActivitySessionSummary(session) {
|
|
243
|
+
const lines = [];
|
|
244
|
+
// Who requested this
|
|
245
|
+
if (session.requestedBy) {
|
|
246
|
+
lines.push(`**Requested by:** ${session.requestedBy}`);
|
|
247
|
+
}
|
|
248
|
+
// What they asked for
|
|
249
|
+
if (session.triggerRequest) {
|
|
250
|
+
lines.push(`**Request:** "${session.triggerRequest}"`);
|
|
251
|
+
}
|
|
252
|
+
// What was changed (write operations only)
|
|
253
|
+
if (session.writeDetails.length > 0) {
|
|
254
|
+
lines.push("");
|
|
255
|
+
lines.push(`**Changes made:**`);
|
|
256
|
+
for (const detail of session.writeDetails) {
|
|
257
|
+
lines.push(`- ${detail.what}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
// Generate and include conversation summary for memory
|
|
261
|
+
const conversationSummary = await this.generateConversationSummary(session);
|
|
262
|
+
if (conversationSummary) {
|
|
263
|
+
lines.push("");
|
|
264
|
+
lines.push(`**Conversation:** ${conversationSummary}`);
|
|
265
|
+
}
|
|
266
|
+
lines.push("");
|
|
267
|
+
lines.push("---");
|
|
268
|
+
lines.push("");
|
|
269
|
+
// Metrics (compact)
|
|
270
|
+
const duration = Math.round((Date.now() - session.startTime) / 1000);
|
|
271
|
+
const totalTokens = session.metrics.inputTokens + session.metrics.outputTokens;
|
|
272
|
+
lines.push(`**Stats:** ${session.metrics.writeOperations} writes, ${totalTokens} tokens, ${duration}s`);
|
|
273
|
+
return lines.join("\n");
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Start periodic idle session checking
|
|
277
|
+
*/
|
|
278
|
+
startIdleCheckTimer(intervalMs = 30_000) {
|
|
279
|
+
if (this.idleCheckTimer) {
|
|
280
|
+
clearInterval(this.idleCheckTimer);
|
|
281
|
+
}
|
|
282
|
+
this.idleCheckTimer = setInterval(() => {
|
|
283
|
+
this.checkAndFlushIdleSessions().catch(err => {
|
|
284
|
+
this.logger.warn("Error checking idle sessions", { error: err });
|
|
285
|
+
});
|
|
286
|
+
}, intervalMs);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Stop idle check timer
|
|
290
|
+
*/
|
|
291
|
+
stopIdleCheckTimer() {
|
|
292
|
+
if (this.idleCheckTimer) {
|
|
293
|
+
clearInterval(this.idleCheckTimer);
|
|
294
|
+
this.idleCheckTimer = null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// ===== MEMORY SYSTEM =====
|
|
298
|
+
/**
|
|
299
|
+
* Generate a conversation summary using LLM
|
|
300
|
+
* Creates a condensed memory of what was discussed
|
|
301
|
+
*/
|
|
302
|
+
async generateConversationSummary(session) {
|
|
303
|
+
if (!this.anthropicClient || session.conversation.length === 0) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const response = await this.anthropicClient.messages.create({
|
|
308
|
+
model: "claude-haiku-4-5-20251001",
|
|
309
|
+
max_tokens: 500,
|
|
310
|
+
messages: [
|
|
311
|
+
{
|
|
312
|
+
role: "user",
|
|
313
|
+
content: `Summarize this conversation in 2-3 sentences for future context. Focus on:
|
|
314
|
+
- What the user wanted to accomplish
|
|
315
|
+
- Key decisions or preferences expressed
|
|
316
|
+
- Important context for future interactions
|
|
317
|
+
|
|
318
|
+
Conversation:
|
|
319
|
+
${session.conversation.join("\n")}
|
|
320
|
+
|
|
321
|
+
Summary:`,
|
|
322
|
+
},
|
|
323
|
+
],
|
|
324
|
+
});
|
|
325
|
+
const textBlock = response.content.find((b) => b.type === "text");
|
|
326
|
+
return textBlock?.text || null;
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
this.logger.warn("Failed to generate conversation summary", {
|
|
330
|
+
error: error instanceof Error ? error.message : String(error),
|
|
331
|
+
});
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Load memory entries for an activity from SESSION_LOG
|
|
337
|
+
* Returns past conversation summaries for context injection
|
|
338
|
+
*
|
|
339
|
+
* NOTE: ActivityLink fields store objects {_id, name}, so we fetch recent
|
|
340
|
+
* session logs and filter client-side by checking the nested _id.
|
|
341
|
+
*/
|
|
342
|
+
async loadMemoryForActivity(activityId, workspaceId, limit = 5) {
|
|
343
|
+
if (!activityId || !workspaceId) {
|
|
344
|
+
return [];
|
|
345
|
+
}
|
|
346
|
+
// Get dynamic schema for this workspace
|
|
347
|
+
const schema = this.getSessionLogSchema(workspaceId);
|
|
348
|
+
if (!schema) {
|
|
349
|
+
this.logger.debug("Cannot load memory - no Session Log workflow in workspace", { workspaceId });
|
|
350
|
+
return [];
|
|
351
|
+
}
|
|
352
|
+
// Get field IDs from schema
|
|
353
|
+
const contextSummaryFieldId = schema.fields["contextSummary"] || schema.fields["Context summary"];
|
|
354
|
+
const linkedWorkFieldId = schema.fields["linkedWork"] || schema.fields["Most relevant link to work"];
|
|
355
|
+
const phaseId = schema.phases["active"] || Object.values(schema.phases)[0];
|
|
356
|
+
if (!contextSummaryFieldId || !linkedWorkFieldId || !phaseId) {
|
|
357
|
+
this.logger.debug("Cannot load memory - missing required fields in schema");
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
// Query SESSION_LOG for recent sessions (fetch more to filter client-side)
|
|
362
|
+
// ActivityLink filters don't work with simple equals, so we filter after
|
|
363
|
+
const result = await this.callMcpTool("list_activities", {
|
|
364
|
+
workflowId: schema.workflowId,
|
|
365
|
+
phaseId,
|
|
366
|
+
fields: [contextSummaryFieldId, linkedWorkFieldId],
|
|
367
|
+
limit: 50, // Fetch more, filter client-side
|
|
368
|
+
sortBy: "created",
|
|
369
|
+
sortOrder: "desc",
|
|
370
|
+
});
|
|
371
|
+
const resultText = result?.content?.[0]?.text;
|
|
372
|
+
if (!resultText) {
|
|
373
|
+
return [];
|
|
374
|
+
}
|
|
375
|
+
// Parse the response to extract memory entries
|
|
376
|
+
const entries = [];
|
|
377
|
+
// Try to parse as JSON array
|
|
378
|
+
try {
|
|
379
|
+
const parsed = JSON.parse(resultText);
|
|
380
|
+
const activities = parsed.activities || parsed.data || parsed;
|
|
381
|
+
if (Array.isArray(activities)) {
|
|
382
|
+
for (const activity of activities) {
|
|
383
|
+
// Get linkedWork field - it's an ActivityLink with nested {_id, name}
|
|
384
|
+
const linkedWorkField = activity.fields?.[linkedWorkFieldId];
|
|
385
|
+
const linkedWorkId = linkedWorkField?.value?._id || linkedWorkField?._id;
|
|
386
|
+
// Filter: only include entries linked to our target activity
|
|
387
|
+
if (linkedWorkId !== activityId) {
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
// Get context summary
|
|
391
|
+
const summaryField = activity.fields?.[contextSummaryFieldId];
|
|
392
|
+
const summary = summaryField?.value || summaryField;
|
|
393
|
+
if (summary) {
|
|
394
|
+
// Extract conversation summary section if present
|
|
395
|
+
const conversationMatch = summary.match(/\*\*Conversation:\*\*\s*([\s\S]*?)(?=\n\n\*\*|---\n|$)/);
|
|
396
|
+
const memorySummary = conversationMatch?.[1]?.trim() || summary;
|
|
397
|
+
entries.push({
|
|
398
|
+
sessionId: activity._id,
|
|
399
|
+
timestamp: activity.created || Date.now(),
|
|
400
|
+
summary: memorySummary,
|
|
401
|
+
linkedActivityId: activityId,
|
|
402
|
+
});
|
|
403
|
+
// Stop once we have enough
|
|
404
|
+
if (entries.length >= limit) {
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
// Not JSON, try regex extraction (fallback)
|
|
413
|
+
this.logger.debug("Failed to parse as JSON, using regex fallback");
|
|
414
|
+
}
|
|
415
|
+
this.logger.debug("Loaded memory entries", {
|
|
416
|
+
activityId,
|
|
417
|
+
count: entries.length,
|
|
418
|
+
});
|
|
419
|
+
return entries;
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
this.logger.warn("Failed to load memory for activity", {
|
|
423
|
+
activityId,
|
|
424
|
+
error: error instanceof Error ? error.message : String(error),
|
|
425
|
+
});
|
|
426
|
+
return [];
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Format memory entries for injection into conversation context
|
|
431
|
+
*/
|
|
432
|
+
formatMemoryForContext(entries) {
|
|
433
|
+
if (entries.length === 0) {
|
|
434
|
+
return "";
|
|
435
|
+
}
|
|
436
|
+
const lines = ["<previous_memory>", "Past interactions with this activity:"];
|
|
437
|
+
for (const entry of entries) {
|
|
438
|
+
const date = new Date(entry.timestamp).toLocaleDateString();
|
|
439
|
+
lines.push(`- [${date}] ${entry.summary}`);
|
|
440
|
+
}
|
|
441
|
+
lines.push("</previous_memory>");
|
|
442
|
+
return lines.join("\n");
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
exports.SessionLoggerService = SessionLoggerService;
|
|
446
|
+
//# sourceMappingURL=session-logger.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Executor Service
|
|
3
|
+
*
|
|
4
|
+
* Handles MCP tool execution:
|
|
5
|
+
* - Executes tool calls from LLM
|
|
6
|
+
* - Tracks write operations
|
|
7
|
+
* - Extracts write details for logging
|
|
8
|
+
*/
|
|
9
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
10
|
+
import { Logger } from "../../../lib/logger";
|
|
11
|
+
import { McpClientService } from "./mcp-client";
|
|
12
|
+
import { WriteDetail, ActivitySession, ToolInput, McpToolResult } from "../../shared/types";
|
|
13
|
+
export interface ToolExecutionResult {
|
|
14
|
+
type: "tool_result";
|
|
15
|
+
tool_use_id: string;
|
|
16
|
+
content: string;
|
|
17
|
+
is_error?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface ToolExecutionContext {
|
|
20
|
+
session?: ActivitySession;
|
|
21
|
+
preprocessToolInput?: (toolName: string, input: ToolInput) => ToolInput;
|
|
22
|
+
}
|
|
23
|
+
export declare class ToolExecutor {
|
|
24
|
+
private mcpClient;
|
|
25
|
+
private logger;
|
|
26
|
+
private static WRITE_TOOLS;
|
|
27
|
+
constructor(mcpClient: McpClientService, logger: Logger);
|
|
28
|
+
/**
|
|
29
|
+
* Check if a tool is a write operation
|
|
30
|
+
*/
|
|
31
|
+
isWriteTool(toolName: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Execute a list of tool calls and return results
|
|
34
|
+
*/
|
|
35
|
+
executeTools(toolUseBlocks: Anthropic.ToolUseBlock[], context?: ToolExecutionContext): Promise<ToolExecutionResult[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Extract details about a write operation for logging
|
|
38
|
+
*/
|
|
39
|
+
extractWriteDetail(toolName: string, input: ToolInput, result: McpToolResult): WriteDetail | null;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=tool-executor.d.ts.map
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Tool Executor Service
|
|
4
|
+
*
|
|
5
|
+
* Handles MCP tool execution:
|
|
6
|
+
* - Executes tool calls from LLM
|
|
7
|
+
* - Tracks write operations
|
|
8
|
+
* - Extracts write details for logging
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.ToolExecutor = void 0;
|
|
12
|
+
class ToolExecutor {
|
|
13
|
+
mcpClient;
|
|
14
|
+
logger;
|
|
15
|
+
// Tools that modify data in Hailer (vs read-only tools)
|
|
16
|
+
static WRITE_TOOLS = new Set([
|
|
17
|
+
"create_activity",
|
|
18
|
+
"update_activity",
|
|
19
|
+
"add_discussion_message",
|
|
20
|
+
"invite_discussion_members",
|
|
21
|
+
"join_discussion",
|
|
22
|
+
"create_insight",
|
|
23
|
+
"update_insight",
|
|
24
|
+
"remove_insight",
|
|
25
|
+
"create_app",
|
|
26
|
+
"update_app",
|
|
27
|
+
"remove_app",
|
|
28
|
+
"install_workflow",
|
|
29
|
+
"remove_workflow",
|
|
30
|
+
"update_workflow_field",
|
|
31
|
+
"update_workflow_phase",
|
|
32
|
+
"upload_files",
|
|
33
|
+
]);
|
|
34
|
+
constructor(mcpClient, logger) {
|
|
35
|
+
this.mcpClient = mcpClient;
|
|
36
|
+
this.logger = logger;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Check if a tool is a write operation
|
|
40
|
+
*/
|
|
41
|
+
isWriteTool(toolName) {
|
|
42
|
+
return ToolExecutor.WRITE_TOOLS.has(toolName);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Execute a list of tool calls and return results
|
|
46
|
+
*/
|
|
47
|
+
async executeTools(toolUseBlocks, context = {}) {
|
|
48
|
+
const { session, preprocessToolInput } = context;
|
|
49
|
+
const toolResults = [];
|
|
50
|
+
for (const toolUse of toolUseBlocks) {
|
|
51
|
+
this.logger.info("Executing tool", { tool: toolUse.name });
|
|
52
|
+
// Track tool call in session
|
|
53
|
+
if (session) {
|
|
54
|
+
session.metrics.toolCalls++;
|
|
55
|
+
session.actions.push(`Tool: ${toolUse.name}`);
|
|
56
|
+
session.lastActivityTime = Date.now();
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
// Load full schema if needed (uses service's cache)
|
|
60
|
+
await this.mcpClient.loadToolSchemaIfNeeded(toolUse.name);
|
|
61
|
+
// Allow preprocessing of tool input (cast from Anthropic's unknown type)
|
|
62
|
+
const rawInput = toolUse.input;
|
|
63
|
+
const toolInput = preprocessToolInput
|
|
64
|
+
? preprocessToolInput(toolUse.name, rawInput)
|
|
65
|
+
: rawInput;
|
|
66
|
+
const result = await this.mcpClient.callMcpTool(toolUse.name, toolInput);
|
|
67
|
+
const resultContent = JSON.stringify(result?.content || {});
|
|
68
|
+
// Track write operations for session logging
|
|
69
|
+
if (session && this.isWriteTool(toolUse.name)) {
|
|
70
|
+
session.metrics.writeOperations++;
|
|
71
|
+
const writeDetail = this.extractWriteDetail(toolUse.name, toolInput, result);
|
|
72
|
+
if (writeDetail) {
|
|
73
|
+
session.writeDetails.push(writeDetail);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
toolResults.push({
|
|
77
|
+
type: "tool_result",
|
|
78
|
+
tool_use_id: toolUse.id,
|
|
79
|
+
content: resultContent,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
84
|
+
this.logger.warn("Tool execution failed", {
|
|
85
|
+
tool: toolUse.name,
|
|
86
|
+
error: errorMessage,
|
|
87
|
+
});
|
|
88
|
+
toolResults.push({
|
|
89
|
+
type: "tool_result",
|
|
90
|
+
tool_use_id: toolUse.id,
|
|
91
|
+
content: `Error: ${errorMessage}`,
|
|
92
|
+
is_error: true,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return toolResults;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Extract details about a write operation for logging
|
|
100
|
+
*/
|
|
101
|
+
extractWriteDetail(toolName, input, result) {
|
|
102
|
+
try {
|
|
103
|
+
const resultText = result?.content?.[0]?.text || "";
|
|
104
|
+
// Extract activity ID from result if present
|
|
105
|
+
const idMatch = resultText.match(/"_id":\s*"([a-f0-9]{24})"/i) ||
|
|
106
|
+
resultText.match(/`([a-f0-9]{24})`/);
|
|
107
|
+
const targetId = idMatch?.[1] || input.activityId || input.discussionId;
|
|
108
|
+
switch (toolName) {
|
|
109
|
+
case "create_activity":
|
|
110
|
+
return {
|
|
111
|
+
tool: toolName,
|
|
112
|
+
what: `Created activity "${input.name || 'unnamed'}"`,
|
|
113
|
+
targetId,
|
|
114
|
+
};
|
|
115
|
+
case "update_activity": {
|
|
116
|
+
const fieldCount = input.fields ? Object.keys(input.fields).length : 0;
|
|
117
|
+
return {
|
|
118
|
+
tool: toolName,
|
|
119
|
+
what: `Updated ${fieldCount} field(s)${input.name ? ` on "${input.name}"` : ''}`,
|
|
120
|
+
targetId: input.activityId,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
case "add_discussion_message": {
|
|
124
|
+
const msgPreview = input.content?.substring(0, 50) || "";
|
|
125
|
+
return {
|
|
126
|
+
tool: toolName,
|
|
127
|
+
what: `Posted message: "${msgPreview}${msgPreview.length >= 50 ? '...' : ''}"`,
|
|
128
|
+
targetId: input.discussionId,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
case "invite_discussion_members":
|
|
132
|
+
return {
|
|
133
|
+
tool: toolName,
|
|
134
|
+
what: `Invited ${input.userIds?.length || 0} member(s) to discussion`,
|
|
135
|
+
targetId: input.discussionId,
|
|
136
|
+
};
|
|
137
|
+
case "join_discussion":
|
|
138
|
+
return {
|
|
139
|
+
tool: toolName,
|
|
140
|
+
what: input.inviteUserId ? "Invited user to discussion" : "Joined discussion",
|
|
141
|
+
targetId: input.activityId || input.discussionId,
|
|
142
|
+
};
|
|
143
|
+
case "create_insight":
|
|
144
|
+
return {
|
|
145
|
+
tool: toolName,
|
|
146
|
+
what: `Created insight "${input.name || 'unnamed'}"`,
|
|
147
|
+
targetId,
|
|
148
|
+
};
|
|
149
|
+
case "update_insight":
|
|
150
|
+
return {
|
|
151
|
+
tool: toolName,
|
|
152
|
+
what: `Updated insight`,
|
|
153
|
+
targetId: input.insightId,
|
|
154
|
+
};
|
|
155
|
+
default:
|
|
156
|
+
return {
|
|
157
|
+
tool: toolName,
|
|
158
|
+
what: `Executed ${toolName}`,
|
|
159
|
+
targetId,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
exports.ToolExecutor = ToolExecutor;
|
|
169
|
+
//# sourceMappingURL=tool-executor.js.map
|