@inkeep/agents-run-api 0.0.0-dev-20250910232631
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/LICENSE.md +49 -0
- package/README.md +117 -0
- package/dist/__tests__/setup.d.ts +4 -0
- package/dist/__tests__/setup.d.ts.map +1 -0
- package/dist/__tests__/setup.js +80 -0
- package/dist/__tests__/utils/testProject.d.ts +18 -0
- package/dist/__tests__/utils/testProject.d.ts.map +1 -0
- package/dist/__tests__/utils/testProject.js +26 -0
- package/dist/__tests__/utils/testRequest.d.ts +8 -0
- package/dist/__tests__/utils/testRequest.d.ts.map +1 -0
- package/dist/__tests__/utils/testRequest.js +32 -0
- package/dist/__tests__/utils/testTenant.d.ts +64 -0
- package/dist/__tests__/utils/testTenant.d.ts.map +1 -0
- package/dist/__tests__/utils/testTenant.js +71 -0
- package/dist/a2a/client.d.ts +182 -0
- package/dist/a2a/client.d.ts.map +1 -0
- package/dist/a2a/client.js +645 -0
- package/dist/a2a/handlers.d.ts +4 -0
- package/dist/a2a/handlers.d.ts.map +1 -0
- package/dist/a2a/handlers.js +656 -0
- package/dist/a2a/transfer.d.ts +18 -0
- package/dist/a2a/transfer.d.ts.map +1 -0
- package/dist/a2a/transfer.js +22 -0
- package/dist/a2a/types.d.ts +63 -0
- package/dist/a2a/types.d.ts.map +1 -0
- package/dist/a2a/types.js +1 -0
- package/dist/agents/Agent.d.ts +151 -0
- package/dist/agents/Agent.d.ts.map +1 -0
- package/dist/agents/Agent.js +1164 -0
- package/dist/agents/ModelFactory.d.ts +62 -0
- package/dist/agents/ModelFactory.d.ts.map +1 -0
- package/dist/agents/ModelFactory.js +208 -0
- package/dist/agents/SystemPromptBuilder.d.ts +14 -0
- package/dist/agents/SystemPromptBuilder.d.ts.map +1 -0
- package/dist/agents/SystemPromptBuilder.js +62 -0
- package/dist/agents/ToolSessionManager.d.ts +53 -0
- package/dist/agents/ToolSessionManager.d.ts.map +1 -0
- package/dist/agents/ToolSessionManager.js +106 -0
- package/dist/agents/artifactTools.d.ts +30 -0
- package/dist/agents/artifactTools.d.ts.map +1 -0
- package/dist/agents/artifactTools.js +463 -0
- package/dist/agents/generateTaskHandler.d.ts +41 -0
- package/dist/agents/generateTaskHandler.d.ts.map +1 -0
- package/dist/agents/generateTaskHandler.js +350 -0
- package/dist/agents/relationTools.d.ts +35 -0
- package/dist/agents/relationTools.d.ts.map +1 -0
- package/dist/agents/relationTools.js +246 -0
- package/dist/agents/types.d.ts +23 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +1 -0
- package/dist/agents/versions/V1Config.d.ts +21 -0
- package/dist/agents/versions/V1Config.d.ts.map +1 -0
- package/dist/agents/versions/V1Config.js +285 -0
- package/dist/app.d.ts +5 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +219 -0
- package/dist/data/agentGraph.d.ts +4 -0
- package/dist/data/agentGraph.d.ts.map +1 -0
- package/dist/data/agentGraph.js +73 -0
- package/dist/data/agents.d.ts +4 -0
- package/dist/data/agents.d.ts.map +1 -0
- package/dist/data/agents.js +78 -0
- package/dist/data/conversations.d.ts +59 -0
- package/dist/data/conversations.d.ts.map +1 -0
- package/dist/data/conversations.js +216 -0
- package/dist/data/db/clean.d.ts +6 -0
- package/dist/data/db/clean.d.ts.map +1 -0
- package/dist/data/db/clean.js +77 -0
- package/dist/data/db/dbClient.d.ts +3 -0
- package/dist/data/db/dbClient.d.ts.map +1 -0
- package/dist/data/db/dbClient.js +13 -0
- package/dist/env.d.ts +45 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +64 -0
- package/dist/handlers/executionHandler.d.ts +36 -0
- package/dist/handlers/executionHandler.d.ts.map +1 -0
- package/dist/handlers/executionHandler.js +415 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +28 -0
- package/dist/instrumentation.d.ts +13 -0
- package/dist/instrumentation.d.ts.map +1 -0
- package/dist/instrumentation.js +66 -0
- package/dist/logger.d.ts +4 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +32 -0
- package/dist/middleware/api-key-auth.d.ts +22 -0
- package/dist/middleware/api-key-auth.d.ts.map +1 -0
- package/dist/middleware/api-key-auth.js +139 -0
- package/dist/middleware/index.d.ts +2 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +1 -0
- package/dist/openapi.d.ts +2 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +36 -0
- package/dist/routes/agents.d.ts +10 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +158 -0
- package/dist/routes/chat.d.ts +10 -0
- package/dist/routes/chat.d.ts.map +1 -0
- package/dist/routes/chat.js +307 -0
- package/dist/routes/chatDataStream.d.ts +10 -0
- package/dist/routes/chatDataStream.d.ts.map +1 -0
- package/dist/routes/chatDataStream.js +185 -0
- package/dist/routes/mcp.d.ts +10 -0
- package/dist/routes/mcp.d.ts.map +1 -0
- package/dist/routes/mcp.js +500 -0
- package/dist/tracer.d.ts +24 -0
- package/dist/tracer.d.ts.map +1 -0
- package/dist/tracer.js +107 -0
- package/dist/types/chat.d.ts +25 -0
- package/dist/types/chat.d.ts.map +1 -0
- package/dist/types/chat.js +1 -0
- package/dist/types/execution-context.d.ts +14 -0
- package/dist/types/execution-context.d.ts.map +1 -0
- package/dist/types/execution-context.js +14 -0
- package/dist/utils/agent-operations.d.ts +93 -0
- package/dist/utils/agent-operations.d.ts.map +1 -0
- package/dist/utils/agent-operations.js +78 -0
- package/dist/utils/artifact-component-schema.d.ts +29 -0
- package/dist/utils/artifact-component-schema.d.ts.map +1 -0
- package/dist/utils/artifact-component-schema.js +119 -0
- package/dist/utils/artifact-parser.d.ts +71 -0
- package/dist/utils/artifact-parser.d.ts.map +1 -0
- package/dist/utils/artifact-parser.js +253 -0
- package/dist/utils/cleanup.d.ts +19 -0
- package/dist/utils/cleanup.d.ts.map +1 -0
- package/dist/utils/cleanup.js +66 -0
- package/dist/utils/data-component-schema.d.ts +6 -0
- package/dist/utils/data-component-schema.d.ts.map +1 -0
- package/dist/utils/data-component-schema.js +43 -0
- package/dist/utils/graph-session.d.ts +230 -0
- package/dist/utils/graph-session.d.ts.map +1 -0
- package/dist/utils/graph-session.js +1199 -0
- package/dist/utils/incremental-stream-parser.d.ts +62 -0
- package/dist/utils/incremental-stream-parser.d.ts.map +1 -0
- package/dist/utils/incremental-stream-parser.js +330 -0
- package/dist/utils/response-formatter.d.ts +26 -0
- package/dist/utils/response-formatter.d.ts.map +1 -0
- package/dist/utils/response-formatter.js +158 -0
- package/dist/utils/stream-helpers.d.ts +186 -0
- package/dist/utils/stream-helpers.d.ts.map +1 -0
- package/dist/utils/stream-helpers.js +603 -0
- package/dist/utils/stream-registry.d.ts +18 -0
- package/dist/utils/stream-registry.d.ts.map +1 -0
- package/dist/utils/stream-registry.js +33 -0
- package/package.json +95 -0
- package/templates/v1/artifact.xml +7 -0
- package/templates/v1/data-component.xml +9 -0
- package/templates/v1/system-prompt.xml +52 -0
- package/templates/v1/thinking-preparation.xml +34 -0
- package/templates/v1/tool.xml +12 -0
|
@@ -0,0 +1,1199 @@
|
|
|
1
|
+
import { SpanStatusCode } from '@opentelemetry/api';
|
|
2
|
+
import { generateObject, generateText } from 'ai';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { ModelFactory } from '../agents/ModelFactory';
|
|
5
|
+
import { getFormattedConversationHistory } from '../data/conversations';
|
|
6
|
+
import dbClient from '../data/db/dbClient';
|
|
7
|
+
import { getLogger } from '../logger';
|
|
8
|
+
import { createSpanName, getGlobalTracer, handleSpanError } from '../tracer';
|
|
9
|
+
import { statusUpdateOp } from './agent-operations';
|
|
10
|
+
import { getStreamHelper } from './stream-registry';
|
|
11
|
+
const logger = getLogger('GraphSession');
|
|
12
|
+
const tracer = getGlobalTracer();
|
|
13
|
+
/**
|
|
14
|
+
* Tracks all agent operations and interactions for a single message
|
|
15
|
+
* Now includes intelligent status update functionality
|
|
16
|
+
*/
|
|
17
|
+
export class GraphSession {
|
|
18
|
+
sessionId;
|
|
19
|
+
messageId;
|
|
20
|
+
graphId;
|
|
21
|
+
tenantId;
|
|
22
|
+
projectId;
|
|
23
|
+
events = [];
|
|
24
|
+
statusUpdateState;
|
|
25
|
+
statusUpdateTimer;
|
|
26
|
+
previousSummaries = [];
|
|
27
|
+
isEnded = false;
|
|
28
|
+
isTextStreaming = false;
|
|
29
|
+
isGeneratingUpdate = false;
|
|
30
|
+
pendingArtifacts = new Set(); // Track pending artifact processing
|
|
31
|
+
artifactProcessingErrors = new Map(); // Track errors per artifact
|
|
32
|
+
MAX_ARTIFACT_RETRIES = 3;
|
|
33
|
+
MAX_PENDING_ARTIFACTS = 100; // Prevent unbounded growth
|
|
34
|
+
scheduledTimeouts; // Track scheduled timeouts for cleanup
|
|
35
|
+
constructor(sessionId, messageId, graphId, tenantId, projectId) {
|
|
36
|
+
this.sessionId = sessionId;
|
|
37
|
+
this.messageId = messageId;
|
|
38
|
+
this.graphId = graphId;
|
|
39
|
+
this.tenantId = tenantId;
|
|
40
|
+
this.projectId = projectId;
|
|
41
|
+
logger.debug({ sessionId, messageId, graphId }, 'GraphSession created');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Initialize status updates for this session
|
|
45
|
+
*/
|
|
46
|
+
initializeStatusUpdates(config, summarizerModel) {
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
this.statusUpdateState = {
|
|
49
|
+
lastUpdateTime: now,
|
|
50
|
+
lastEventCount: 0,
|
|
51
|
+
startTime: now,
|
|
52
|
+
summarizerModel,
|
|
53
|
+
config: {
|
|
54
|
+
numEvents: config.numEvents || 10,
|
|
55
|
+
timeInSeconds: config.timeInSeconds || 30,
|
|
56
|
+
...config,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
// Set up time-based updates if configured
|
|
60
|
+
if (this.statusUpdateState.config.timeInSeconds) {
|
|
61
|
+
this.statusUpdateTimer = setInterval(async () => {
|
|
62
|
+
// Guard against cleanup race condition
|
|
63
|
+
if (!this.statusUpdateState || this.isEnded) {
|
|
64
|
+
logger.debug({ sessionId: this.sessionId }, 'Timer triggered but session already cleaned up or ended');
|
|
65
|
+
if (this.statusUpdateTimer) {
|
|
66
|
+
clearInterval(this.statusUpdateTimer);
|
|
67
|
+
this.statusUpdateTimer = undefined;
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
await this.checkAndSendTimeBasedUpdate();
|
|
72
|
+
}, this.statusUpdateState.config.timeInSeconds * 1000);
|
|
73
|
+
logger.info({
|
|
74
|
+
sessionId: this.sessionId,
|
|
75
|
+
intervalMs: this.statusUpdateState.config.timeInSeconds * 1000,
|
|
76
|
+
}, 'Time-based status update timer started');
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Record an event in the session and trigger status updates if configured
|
|
81
|
+
*/
|
|
82
|
+
recordEvent(eventType, agentId, data) {
|
|
83
|
+
// Don't record events or trigger updates if session has ended
|
|
84
|
+
if (this.isEnded) {
|
|
85
|
+
logger.debug({
|
|
86
|
+
sessionId: this.sessionId,
|
|
87
|
+
eventType,
|
|
88
|
+
agentId,
|
|
89
|
+
}, 'Event received after session ended - ignoring');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const event = {
|
|
93
|
+
timestamp: Date.now(),
|
|
94
|
+
eventType,
|
|
95
|
+
agentId,
|
|
96
|
+
data,
|
|
97
|
+
};
|
|
98
|
+
this.events.push(event);
|
|
99
|
+
// Process artifact if it's pending generation
|
|
100
|
+
if (eventType === 'artifact_saved' && data.pendingGeneration) {
|
|
101
|
+
const artifactId = data.artifactId;
|
|
102
|
+
// Check for backpressure - prevent unbounded growth of pending artifacts
|
|
103
|
+
if (this.pendingArtifacts.size >= this.MAX_PENDING_ARTIFACTS) {
|
|
104
|
+
logger.warn({
|
|
105
|
+
sessionId: this.sessionId,
|
|
106
|
+
artifactId,
|
|
107
|
+
pendingCount: this.pendingArtifacts.size,
|
|
108
|
+
maxAllowed: this.MAX_PENDING_ARTIFACTS
|
|
109
|
+
}, 'Too many pending artifacts, skipping processing');
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Track this artifact as pending
|
|
113
|
+
this.pendingArtifacts.add(artifactId);
|
|
114
|
+
// Fire and forget - process artifact completely asynchronously without any blocking
|
|
115
|
+
setImmediate(() => {
|
|
116
|
+
// No await, no spans at trigger level - truly fire and forget
|
|
117
|
+
this.processArtifact(data)
|
|
118
|
+
.then(() => {
|
|
119
|
+
// Remove from pending on success
|
|
120
|
+
this.pendingArtifacts.delete(artifactId);
|
|
121
|
+
this.artifactProcessingErrors.delete(artifactId);
|
|
122
|
+
})
|
|
123
|
+
.catch((error) => {
|
|
124
|
+
// Track error count
|
|
125
|
+
const errorCount = (this.artifactProcessingErrors.get(artifactId) || 0) + 1;
|
|
126
|
+
this.artifactProcessingErrors.set(artifactId, errorCount);
|
|
127
|
+
// Remove from pending after max retries
|
|
128
|
+
if (errorCount >= this.MAX_ARTIFACT_RETRIES) {
|
|
129
|
+
this.pendingArtifacts.delete(artifactId);
|
|
130
|
+
logger.error({
|
|
131
|
+
sessionId: this.sessionId,
|
|
132
|
+
artifactId,
|
|
133
|
+
errorCount,
|
|
134
|
+
maxRetries: this.MAX_ARTIFACT_RETRIES,
|
|
135
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
136
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
137
|
+
}, 'Artifact processing failed after max retries, giving up');
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
// Keep in pending for potential retry
|
|
141
|
+
logger.warn({
|
|
142
|
+
sessionId: this.sessionId,
|
|
143
|
+
artifactId,
|
|
144
|
+
errorCount,
|
|
145
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
146
|
+
}, 'Artifact processing failed, may retry');
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
// Trigger status updates check (only sends if thresholds met)
|
|
152
|
+
if (!this.isEnded) {
|
|
153
|
+
this.checkStatusUpdates();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Check and send status updates if configured (async, non-blocking)
|
|
158
|
+
*/
|
|
159
|
+
checkStatusUpdates() {
|
|
160
|
+
if (this.isEnded) {
|
|
161
|
+
logger.debug({ sessionId: this.sessionId }, 'Session has ended - skipping status update check');
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (!this.statusUpdateState) {
|
|
165
|
+
logger.debug({ sessionId: this.sessionId }, 'No status update state - skipping check');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
// Status updates are enabled by having statusUpdateState
|
|
169
|
+
// Store reference to prevent race condition during async execution
|
|
170
|
+
const statusUpdateState = this.statusUpdateState;
|
|
171
|
+
// Schedule async update check with proper error handling
|
|
172
|
+
this.scheduleStatusUpdateCheck(statusUpdateState);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Check and send time-based status updates
|
|
176
|
+
*/
|
|
177
|
+
async checkAndSendTimeBasedUpdate() {
|
|
178
|
+
if (this.isEnded) {
|
|
179
|
+
logger.debug({ sessionId: this.sessionId }, 'Session has ended - skipping time-based update');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (!this.statusUpdateState) {
|
|
183
|
+
logger.debug({ sessionId: this.sessionId }, 'No status updates configured for time-based check');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// Only send if we have new events since last update
|
|
187
|
+
const newEventCount = this.events.length - this.statusUpdateState.lastEventCount;
|
|
188
|
+
if (newEventCount === 0) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
// Always send time-based updates regardless of event count
|
|
193
|
+
await this.generateAndSendUpdate();
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
logger.error({
|
|
197
|
+
sessionId: this.sessionId,
|
|
198
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
199
|
+
}, 'Failed to send time-based status update');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Get all events in chronological order
|
|
204
|
+
*/
|
|
205
|
+
getEvents() {
|
|
206
|
+
return [...this.events];
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Get events filtered by type
|
|
210
|
+
*/
|
|
211
|
+
getEventsByType(eventType) {
|
|
212
|
+
return this.events.filter((event) => event.eventType === eventType);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Get events filtered by agent
|
|
216
|
+
*/
|
|
217
|
+
getEventsByAgent(agentId) {
|
|
218
|
+
return this.events.filter((event) => event.agentId === agentId);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Get summary of session activity
|
|
222
|
+
*/
|
|
223
|
+
getSummary() {
|
|
224
|
+
const eventCounts = this.events.reduce((counts, event) => {
|
|
225
|
+
counts[event.eventType] = (counts[event.eventType] || 0) + 1;
|
|
226
|
+
return counts;
|
|
227
|
+
}, {});
|
|
228
|
+
const agentCounts = this.events.reduce((counts, event) => {
|
|
229
|
+
counts[event.agentId] = (counts[event.agentId] || 0) + 1;
|
|
230
|
+
return counts;
|
|
231
|
+
}, {});
|
|
232
|
+
return {
|
|
233
|
+
sessionId: this.sessionId,
|
|
234
|
+
messageId: this.messageId,
|
|
235
|
+
graphId: this.graphId,
|
|
236
|
+
totalEvents: this.events.length,
|
|
237
|
+
eventCounts,
|
|
238
|
+
agentCounts,
|
|
239
|
+
startTime: this.events[0]?.timestamp,
|
|
240
|
+
endTime: this.events[this.events.length - 1]?.timestamp,
|
|
241
|
+
duration: this.events.length > 0
|
|
242
|
+
? this.events[this.events.length - 1].timestamp - this.events[0].timestamp
|
|
243
|
+
: 0,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Mark that text streaming has started (to suppress status updates)
|
|
248
|
+
*/
|
|
249
|
+
setTextStreaming(isStreaming) {
|
|
250
|
+
this.isTextStreaming = isStreaming;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Check if text is currently being streamed
|
|
254
|
+
*/
|
|
255
|
+
isCurrentlyStreaming() {
|
|
256
|
+
return this.isTextStreaming;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Clean up status update resources when session ends
|
|
260
|
+
*/
|
|
261
|
+
cleanup() {
|
|
262
|
+
// Mark session as ended
|
|
263
|
+
this.isEnded = true;
|
|
264
|
+
if (this.statusUpdateTimer) {
|
|
265
|
+
clearInterval(this.statusUpdateTimer);
|
|
266
|
+
this.statusUpdateTimer = undefined;
|
|
267
|
+
}
|
|
268
|
+
this.statusUpdateState = undefined;
|
|
269
|
+
// Clean up artifact tracking maps to prevent memory leaks
|
|
270
|
+
this.pendingArtifacts.clear();
|
|
271
|
+
this.artifactProcessingErrors.clear();
|
|
272
|
+
// Clear any scheduled timeouts to prevent race conditions
|
|
273
|
+
if (this.scheduledTimeouts) {
|
|
274
|
+
for (const timeoutId of this.scheduledTimeouts) {
|
|
275
|
+
clearTimeout(timeoutId);
|
|
276
|
+
}
|
|
277
|
+
this.scheduledTimeouts.clear();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Generate and send a status update using graph-level summarizer
|
|
282
|
+
*/
|
|
283
|
+
async generateAndSendUpdate() {
|
|
284
|
+
if (this.isEnded) {
|
|
285
|
+
logger.debug({ sessionId: this.sessionId }, 'Session has ended - not generating update');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (this.isTextStreaming) {
|
|
289
|
+
logger.debug({ sessionId: this.sessionId }, 'Text is currently streaming - skipping status update');
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (this.isGeneratingUpdate) {
|
|
293
|
+
logger.debug({ sessionId: this.sessionId }, 'Update already in progress - skipping duplicate generation');
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (!this.statusUpdateState) {
|
|
297
|
+
logger.warn({ sessionId: this.sessionId }, 'No status update state - cannot generate update');
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (!this.graphId) {
|
|
301
|
+
logger.warn({ sessionId: this.sessionId }, 'No graph ID - cannot generate update');
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
// Only send if we have new events since last update
|
|
305
|
+
const newEventCount = this.events.length - this.statusUpdateState.lastEventCount;
|
|
306
|
+
if (newEventCount === 0) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
// Set flag to prevent concurrent updates
|
|
310
|
+
this.isGeneratingUpdate = true;
|
|
311
|
+
// Store references at start to prevent race conditions
|
|
312
|
+
const statusUpdateState = this.statusUpdateState;
|
|
313
|
+
const graphId = this.graphId;
|
|
314
|
+
try {
|
|
315
|
+
const streamHelper = getStreamHelper(this.sessionId);
|
|
316
|
+
if (!streamHelper) {
|
|
317
|
+
logger.warn({ sessionId: this.sessionId }, 'No stream helper found - cannot send status update');
|
|
318
|
+
this.isGeneratingUpdate = false;
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
const now = Date.now();
|
|
322
|
+
const elapsedTime = now - statusUpdateState.startTime;
|
|
323
|
+
// Generate status update - either structured or text summary
|
|
324
|
+
let operation;
|
|
325
|
+
if (statusUpdateState.config.statusComponents &&
|
|
326
|
+
statusUpdateState.config.statusComponents.length > 0) {
|
|
327
|
+
// Use generateObject to intelligently select relevant data components
|
|
328
|
+
const result = await this.generateStructuredStatusUpdate(this.events.slice(statusUpdateState.lastEventCount), elapsedTime, statusUpdateState.config.statusComponents, statusUpdateState.summarizerModel, this.previousSummaries);
|
|
329
|
+
if (result.operations && result.operations.length > 0) {
|
|
330
|
+
// Send each operation separately using writeData for dynamic types
|
|
331
|
+
for (const op of result.operations) {
|
|
332
|
+
// Guard against empty/invalid operations
|
|
333
|
+
if (!op || !op.type || !op.data || Object.keys(op.data).length === 0) {
|
|
334
|
+
logger.warn({
|
|
335
|
+
sessionId: this.sessionId,
|
|
336
|
+
operation: op,
|
|
337
|
+
}, 'Skipping empty or invalid structured operation');
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const operationToSend = {
|
|
341
|
+
type: 'status_update',
|
|
342
|
+
ctx: {
|
|
343
|
+
operationType: op.type,
|
|
344
|
+
label: op.data.label,
|
|
345
|
+
data: Object.fromEntries(Object.entries(op.data).filter(([key]) => !['label', 'type'].includes(key))),
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
await streamHelper.writeOperation(operationToSend);
|
|
349
|
+
}
|
|
350
|
+
// Store summaries for next time - use full JSON for better comparison
|
|
351
|
+
const summaryTexts = result.operations.map((op) => JSON.stringify({ type: op.type, data: op.data }));
|
|
352
|
+
this.previousSummaries.push(...summaryTexts);
|
|
353
|
+
// Update state after sending all operations
|
|
354
|
+
if (this.statusUpdateState) {
|
|
355
|
+
this.statusUpdateState.lastUpdateTime = now;
|
|
356
|
+
this.statusUpdateState.lastEventCount = this.events.length;
|
|
357
|
+
}
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
// Fall through to regular text summary if no structured updates
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
// Use regular text generation for simple summaries
|
|
366
|
+
const summary = await this.generateProgressSummary(this.events.slice(statusUpdateState.lastEventCount), elapsedTime, statusUpdateState.summarizerModel, this.previousSummaries);
|
|
367
|
+
// Store this summary for next time
|
|
368
|
+
this.previousSummaries.push(summary);
|
|
369
|
+
// Create standard status update operation
|
|
370
|
+
operation = statusUpdateOp({
|
|
371
|
+
summary,
|
|
372
|
+
eventCount: this.events.length,
|
|
373
|
+
elapsedTime,
|
|
374
|
+
currentPhase: 'processing',
|
|
375
|
+
activeAgent: 'system',
|
|
376
|
+
graphId,
|
|
377
|
+
sessionId: this.sessionId,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
// Keep only last 3 summaries to avoid context getting too large
|
|
381
|
+
if (this.previousSummaries.length > 3) {
|
|
382
|
+
this.previousSummaries.shift();
|
|
383
|
+
}
|
|
384
|
+
// Guard against sending empty/undefined operations that break streams
|
|
385
|
+
if (!operation || !operation.type || !operation.ctx) {
|
|
386
|
+
logger.warn({
|
|
387
|
+
sessionId: this.sessionId,
|
|
388
|
+
operation,
|
|
389
|
+
}, 'Skipping empty or invalid status update operation');
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
await streamHelper.writeOperation(operation);
|
|
393
|
+
// Update state - check if still exists (could be cleaned up during async operation)
|
|
394
|
+
if (this.statusUpdateState) {
|
|
395
|
+
this.statusUpdateState.lastUpdateTime = now;
|
|
396
|
+
this.statusUpdateState.lastEventCount = this.events.length;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
logger.error({
|
|
401
|
+
sessionId: this.sessionId,
|
|
402
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
403
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
404
|
+
}, '❌ Failed to generate status update');
|
|
405
|
+
}
|
|
406
|
+
finally {
|
|
407
|
+
// Clear the flag to allow future updates
|
|
408
|
+
this.isGeneratingUpdate = false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Schedule status update check without setImmediate race conditions
|
|
413
|
+
*/
|
|
414
|
+
scheduleStatusUpdateCheck(statusUpdateState) {
|
|
415
|
+
// Use setTimeout with 0 delay instead of setImmediate for better control
|
|
416
|
+
const timeoutId = setTimeout(async () => {
|
|
417
|
+
try {
|
|
418
|
+
// Double-check session is still valid before proceeding
|
|
419
|
+
if (this.isEnded || !this.statusUpdateState) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
// Acquire update lock with atomic check
|
|
423
|
+
if (!this.acquireUpdateLock()) {
|
|
424
|
+
return; // Another update is in progress
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
// Final validation before processing
|
|
428
|
+
if (this.isEnded || !statusUpdateState || this.isTextStreaming) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const currentEventCount = this.events.length;
|
|
432
|
+
const numEventsThreshold = statusUpdateState.config.numEvents;
|
|
433
|
+
const shouldUpdateByEvents = numEventsThreshold &&
|
|
434
|
+
currentEventCount >= statusUpdateState.lastEventCount + numEventsThreshold;
|
|
435
|
+
if (shouldUpdateByEvents) {
|
|
436
|
+
await this.generateAndSendUpdate();
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
finally {
|
|
440
|
+
// Always release the lock
|
|
441
|
+
this.releaseUpdateLock();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
catch (error) {
|
|
445
|
+
logger.error({
|
|
446
|
+
sessionId: this.sessionId,
|
|
447
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
448
|
+
}, 'Failed to check status updates during event recording');
|
|
449
|
+
// Ensure lock is released on error
|
|
450
|
+
this.releaseUpdateLock();
|
|
451
|
+
}
|
|
452
|
+
}, 0);
|
|
453
|
+
// Track timeout for cleanup if session ends
|
|
454
|
+
if (!this.scheduledTimeouts) {
|
|
455
|
+
this.scheduledTimeouts = new Set();
|
|
456
|
+
}
|
|
457
|
+
this.scheduledTimeouts.add(timeoutId);
|
|
458
|
+
// Auto-cleanup timeout reference
|
|
459
|
+
setTimeout(() => {
|
|
460
|
+
if (this.scheduledTimeouts) {
|
|
461
|
+
this.scheduledTimeouts.delete(timeoutId);
|
|
462
|
+
}
|
|
463
|
+
}, 1000);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Acquire update lock with atomic check
|
|
467
|
+
*/
|
|
468
|
+
acquireUpdateLock() {
|
|
469
|
+
// Atomic check-and-set
|
|
470
|
+
if (this.statusUpdateState?.updateLock) {
|
|
471
|
+
return false; // Already locked
|
|
472
|
+
}
|
|
473
|
+
if (this.statusUpdateState) {
|
|
474
|
+
this.statusUpdateState.updateLock = true;
|
|
475
|
+
}
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Release update lock
|
|
480
|
+
*/
|
|
481
|
+
releaseUpdateLock() {
|
|
482
|
+
if (this.statusUpdateState) {
|
|
483
|
+
this.statusUpdateState.updateLock = false;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Generate user-focused progress summary hiding internal operations
|
|
488
|
+
*/
|
|
489
|
+
async generateProgressSummary(newEvents, elapsedTime, summarizerModel, previousSummaries = []) {
|
|
490
|
+
return tracer.startActiveSpan(createSpanName('graph_session.generate_progress_summary'), {
|
|
491
|
+
attributes: {
|
|
492
|
+
'graph_session.id': this.sessionId,
|
|
493
|
+
'events.count': newEvents.length,
|
|
494
|
+
'elapsed_time.seconds': Math.round(elapsedTime / 1000),
|
|
495
|
+
'llm.model': summarizerModel?.model || 'openai/gpt-4.1-nano-2025-04-14',
|
|
496
|
+
'previous_summaries.count': previousSummaries.length,
|
|
497
|
+
},
|
|
498
|
+
}, async (span) => {
|
|
499
|
+
try {
|
|
500
|
+
// Extract user-visible activities (hide internal agent operations)
|
|
501
|
+
const userVisibleActivities = this.extractUserVisibleActivities(newEvents);
|
|
502
|
+
// Get conversation history to understand user's context and question
|
|
503
|
+
let conversationContext = '';
|
|
504
|
+
if (this.tenantId && this.projectId) {
|
|
505
|
+
try {
|
|
506
|
+
const conversationHistory = await getFormattedConversationHistory({
|
|
507
|
+
tenantId: this.tenantId,
|
|
508
|
+
projectId: this.projectId,
|
|
509
|
+
conversationId: this.sessionId,
|
|
510
|
+
options: {
|
|
511
|
+
limit: 10, // Get recent conversation context
|
|
512
|
+
maxOutputTokens: 2000,
|
|
513
|
+
},
|
|
514
|
+
filters: {},
|
|
515
|
+
});
|
|
516
|
+
conversationContext = conversationHistory.trim()
|
|
517
|
+
? `\nUser's Question/Context:\n${conversationHistory}\n`
|
|
518
|
+
: '';
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
logger.warn({ sessionId: this.sessionId, error }, 'Failed to fetch conversation history for status update');
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
const previousSummaryContext = previousSummaries.length > 0
|
|
525
|
+
? `\nPrevious updates provided to user:\n${previousSummaries.map((s, i) => `${i + 1}. ${s}`).join('\n')}\n`
|
|
526
|
+
: '';
|
|
527
|
+
// Use custom prompt if provided, otherwise use default
|
|
528
|
+
const basePrompt = `Generate a meaningful status update that tells the user what specific information or result was just found/achieved.${conversationContext}${previousSummaries.length > 0 ? `\n${previousSummaryContext}` : ''}
|
|
529
|
+
|
|
530
|
+
Activities:\n${userVisibleActivities.join('\n') || 'No New Activities'}
|
|
531
|
+
|
|
532
|
+
Describe the ACTUAL finding, result, or specific information discovered (e.g., "Found Slack bot requires admin permissions", "Identified 3 channel types for ingestion", "Configuration requires OAuth token").
|
|
533
|
+
|
|
534
|
+
${this.statusUpdateState?.config.prompt?.trim() || ''}`;
|
|
535
|
+
const prompt = basePrompt;
|
|
536
|
+
const model = ModelFactory.createModel(summarizerModel?.model?.trim()
|
|
537
|
+
? summarizerModel
|
|
538
|
+
: { model: 'openai/gpt-4.1-nano-2025-04-14' });
|
|
539
|
+
const { text } = await generateText({
|
|
540
|
+
model,
|
|
541
|
+
prompt,
|
|
542
|
+
experimental_telemetry: {
|
|
543
|
+
isEnabled: true,
|
|
544
|
+
functionId: `status_update_${this.sessionId}`,
|
|
545
|
+
recordInputs: true,
|
|
546
|
+
recordOutputs: true,
|
|
547
|
+
metadata: {
|
|
548
|
+
operation: 'progress_summary_generation',
|
|
549
|
+
sessionId: this.sessionId,
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
});
|
|
553
|
+
span.setAttributes({
|
|
554
|
+
'summary.length': text.trim().length,
|
|
555
|
+
'user_activities.count': userVisibleActivities.length,
|
|
556
|
+
});
|
|
557
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
558
|
+
return text.trim();
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
handleSpanError(span, error);
|
|
562
|
+
logger.error({ error }, 'Failed to generate summary, using fallback');
|
|
563
|
+
return this.generateFallbackSummary(newEvents, elapsedTime);
|
|
564
|
+
}
|
|
565
|
+
finally {
|
|
566
|
+
span.end();
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Generate structured status update using configured data components
|
|
572
|
+
*/
|
|
573
|
+
async generateStructuredStatusUpdate(newEvents, elapsedTime, statusComponents, summarizerModel, previousSummaries = []) {
|
|
574
|
+
return tracer.startActiveSpan(createSpanName('graph_session.generate_structured_update'), {
|
|
575
|
+
attributes: {
|
|
576
|
+
'graph_session.id': this.sessionId,
|
|
577
|
+
'events.count': newEvents.length,
|
|
578
|
+
'elapsed_time.seconds': Math.round(elapsedTime / 1000),
|
|
579
|
+
'llm.model': summarizerModel?.model || 'openai/gpt-4.1-nano-2025-04-14',
|
|
580
|
+
'status_components.count': statusComponents.length,
|
|
581
|
+
'previous_summaries.count': previousSummaries.length,
|
|
582
|
+
},
|
|
583
|
+
}, async (span) => {
|
|
584
|
+
try {
|
|
585
|
+
// Extract user-visible activities
|
|
586
|
+
const userVisibleActivities = this.extractUserVisibleActivities(newEvents);
|
|
587
|
+
// Get conversation history to understand user's context and question
|
|
588
|
+
let conversationContext = '';
|
|
589
|
+
if (this.tenantId && this.projectId) {
|
|
590
|
+
try {
|
|
591
|
+
const conversationHistory = await getFormattedConversationHistory({
|
|
592
|
+
tenantId: this.tenantId,
|
|
593
|
+
projectId: this.projectId,
|
|
594
|
+
conversationId: this.sessionId,
|
|
595
|
+
options: {
|
|
596
|
+
limit: 10, // Get recent conversation context
|
|
597
|
+
maxOutputTokens: 2000,
|
|
598
|
+
},
|
|
599
|
+
filters: {},
|
|
600
|
+
});
|
|
601
|
+
conversationContext = conversationHistory.trim()
|
|
602
|
+
? `\nUser's Question/Context:\n${conversationHistory}\n`
|
|
603
|
+
: '';
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
logger.warn({ sessionId: this.sessionId, error }, 'Failed to fetch conversation history for structured status update');
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const previousSummaryContext = previousSummaries.length > 0
|
|
610
|
+
? `\nPrevious updates sent to user:\n${previousSummaries.map((s, i) => `${i + 1}. ${s}`).join('\n')}\n`
|
|
611
|
+
: '';
|
|
612
|
+
// Build schema for data components and no_relevant_updates option
|
|
613
|
+
const selectionSchema = z.object(Object.fromEntries([
|
|
614
|
+
// Add no_relevant_updates schema
|
|
615
|
+
[
|
|
616
|
+
'no_relevant_updates',
|
|
617
|
+
z
|
|
618
|
+
.object({
|
|
619
|
+
no_updates: z.boolean().default(true),
|
|
620
|
+
})
|
|
621
|
+
.optional()
|
|
622
|
+
.describe('Use when nothing substantially new to report. Should only use on its own.'),
|
|
623
|
+
],
|
|
624
|
+
// Add all other component schemas
|
|
625
|
+
...statusComponents.map((component) => [
|
|
626
|
+
component.type,
|
|
627
|
+
this.getComponentSchema(component)
|
|
628
|
+
.optional()
|
|
629
|
+
.describe(component.description || component.type),
|
|
630
|
+
]),
|
|
631
|
+
]));
|
|
632
|
+
// Use custom prompt if provided, otherwise use default
|
|
633
|
+
const basePrompt = `Generate status updates for relevant components based on what the user has asked for.${conversationContext}${previousSummaries.length > 0 ? `\n${previousSummaryContext}` : ''}
|
|
634
|
+
|
|
635
|
+
Activities:\n${userVisibleActivities.join('\n') || 'No New Activities'}
|
|
636
|
+
|
|
637
|
+
Available components: no_relevant_updates, ${statusComponents.map((c) => c.type).join(', ')}
|
|
638
|
+
|
|
639
|
+
Rules:
|
|
640
|
+
- Fill in data for relevant components only
|
|
641
|
+
- Use 'no_relevant_updates' if nothing substantially new to report. DO NOT WRITE LABELS OR USE OTHER COMPONENTS IF YOU USE THIS COMPONENT.
|
|
642
|
+
- Never repeat previous values, make every update EXTREMELY unique. If you cannot do that the update is not worth mentioning.
|
|
643
|
+
- Labels MUST contain the ACTUAL information discovered ("Found X", "Learned Y", "Discovered Z requires A")
|
|
644
|
+
- DO NOT use action words like "Searching", "Processing", "Analyzing" - state what was FOUND
|
|
645
|
+
- Include specific details, numbers, requirements, or insights discovered
|
|
646
|
+
- You are ONE AI (no agents/delegations)
|
|
647
|
+
- Anonymize all internal operations so that the information appears descriptive and USER FRIENDLY. HIDE INTERNAL OPERATIONS!
|
|
648
|
+
- Bad examples: "Searching docs", "Processing request", "Status update", or not using the no_relevant_updates: e.g. "No New Updates", "No new info to report"
|
|
649
|
+
- Good examples: "Slack bot needs admin privileges", "Found 3-step OAuth flow required", "Channel limit is 500 per workspace", or use the no_relevant_updates component if nothing new to report.
|
|
650
|
+
|
|
651
|
+
REMEMBER YOU CAN ONLY USE 'no_relevant_updates' ALONE! IT CANNOT BE CONCATENATED WITH OTHER STATUS UPDATES!
|
|
652
|
+
|
|
653
|
+
${this.statusUpdateState?.config.prompt?.trim() || ''}`;
|
|
654
|
+
const prompt = basePrompt;
|
|
655
|
+
const model = ModelFactory.createModel(summarizerModel?.model?.trim()
|
|
656
|
+
? summarizerModel
|
|
657
|
+
: { model: 'openai/gpt-4.1-nano-2025-04-14' });
|
|
658
|
+
const { object } = await generateObject({
|
|
659
|
+
model,
|
|
660
|
+
prompt,
|
|
661
|
+
schema: selectionSchema,
|
|
662
|
+
experimental_telemetry: {
|
|
663
|
+
isEnabled: true,
|
|
664
|
+
functionId: `structured_update_${this.sessionId}`,
|
|
665
|
+
recordInputs: true,
|
|
666
|
+
recordOutputs: true,
|
|
667
|
+
metadata: {
|
|
668
|
+
operation: 'structured_status_update_generation',
|
|
669
|
+
sessionId: this.sessionId,
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
});
|
|
673
|
+
const result = object;
|
|
674
|
+
// Extract components that have data (skip no_relevant_updates and empty components)
|
|
675
|
+
const operations = [];
|
|
676
|
+
for (const [componentId, data] of Object.entries(result)) {
|
|
677
|
+
// Skip no_relevant_updates - we don't send any operation for this
|
|
678
|
+
if (componentId === 'no_relevant_updates') {
|
|
679
|
+
continue;
|
|
680
|
+
}
|
|
681
|
+
// Only include components that have actual data
|
|
682
|
+
if (data && typeof data === 'object' && Object.keys(data).length > 0) {
|
|
683
|
+
operations.push({
|
|
684
|
+
type: componentId,
|
|
685
|
+
data: data,
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
span.setAttributes({
|
|
690
|
+
'operations.count': operations.length,
|
|
691
|
+
'user_activities.count': userVisibleActivities.length,
|
|
692
|
+
'result_keys.count': Object.keys(result).length,
|
|
693
|
+
});
|
|
694
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
695
|
+
return { operations };
|
|
696
|
+
}
|
|
697
|
+
catch (error) {
|
|
698
|
+
handleSpanError(span, error);
|
|
699
|
+
logger.error({ error }, 'Failed to generate structured update, using fallback');
|
|
700
|
+
return { operations: [] };
|
|
701
|
+
}
|
|
702
|
+
finally {
|
|
703
|
+
span.end();
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Build Zod schema from JSON schema configuration or use pre-defined schemas
|
|
709
|
+
*/
|
|
710
|
+
getComponentSchema(component) {
|
|
711
|
+
// Check if we have a JSON schema to convert
|
|
712
|
+
if (component.detailsSchema && 'properties' in component.detailsSchema) {
|
|
713
|
+
return this.buildZodSchemaFromJson(component.detailsSchema);
|
|
714
|
+
}
|
|
715
|
+
// Fallback to a simple object with just label if no schema provided
|
|
716
|
+
return z.object({
|
|
717
|
+
label: z
|
|
718
|
+
.string()
|
|
719
|
+
.describe('A short 3-5 word phrase, that is a descriptive label for the update component. This Label must be EXTREMELY unique to represent the UNIQUE update we are providing. The ACTUAL finding or result, not the action. What specific information was discovered? (e.g., "Slack requires OAuth 2.0 setup", "Found 5 integration methods", "API rate limit is 100/minute"). Include the actual detail or insight, not just that you searched or processed.'),
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Build Zod schema from JSON schema with improved type handling
|
|
724
|
+
*/
|
|
725
|
+
buildZodSchemaFromJson(jsonSchema) {
|
|
726
|
+
const properties = {};
|
|
727
|
+
// Always add label field
|
|
728
|
+
properties['label'] = z
|
|
729
|
+
.string()
|
|
730
|
+
.describe('A short 3-5 word phrase, that is a descriptive label for the update component. This Label must be EXTREMELY unique to represent the UNIQUE update we are providing. The SPECIFIC finding, result, or insight discovered (e.g., "Slack bot needs workspace admin role", "Found ingestion requires 3 steps", "Channel history limited to 10k messages"). State the ACTUAL information found, not that you searched. What did you LEARN or DISCOVER? What specific detail is now known?');
|
|
731
|
+
for (const [key, value] of Object.entries(jsonSchema.properties)) {
|
|
732
|
+
let zodType;
|
|
733
|
+
// Check for enum first
|
|
734
|
+
if (value.enum && Array.isArray(value.enum)) {
|
|
735
|
+
// Handle enum types
|
|
736
|
+
if (value.enum.length === 1) {
|
|
737
|
+
zodType = z.literal(value.enum[0]);
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
const [first, ...rest] = value.enum;
|
|
741
|
+
zodType = z.enum([first, ...rest]);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
else if (value.type === 'string') {
|
|
745
|
+
zodType = z.string();
|
|
746
|
+
// Add string-specific validations if present
|
|
747
|
+
if (value.minLength)
|
|
748
|
+
zodType = zodType.min(value.minLength);
|
|
749
|
+
if (value.maxLength)
|
|
750
|
+
zodType = zodType.max(value.maxLength);
|
|
751
|
+
if (value.format === 'email')
|
|
752
|
+
zodType = zodType.email();
|
|
753
|
+
if (value.format === 'url' || value.format === 'uri')
|
|
754
|
+
zodType = zodType.url();
|
|
755
|
+
}
|
|
756
|
+
else if (value.type === 'number' || value.type === 'integer') {
|
|
757
|
+
zodType = value.type === 'integer' ? z.number().int() : z.number();
|
|
758
|
+
// Add number-specific validations if present
|
|
759
|
+
if (value.minimum !== undefined)
|
|
760
|
+
zodType = zodType.min(value.minimum);
|
|
761
|
+
if (value.maximum !== undefined)
|
|
762
|
+
zodType = zodType.max(value.maximum);
|
|
763
|
+
}
|
|
764
|
+
else if (value.type === 'boolean') {
|
|
765
|
+
zodType = z.boolean();
|
|
766
|
+
}
|
|
767
|
+
else if (value.type === 'array') {
|
|
768
|
+
// Handle array items if specified
|
|
769
|
+
if (value.items) {
|
|
770
|
+
if (value.items.enum && Array.isArray(value.items.enum)) {
|
|
771
|
+
// Array of enum values
|
|
772
|
+
const [first, ...rest] = value.items.enum;
|
|
773
|
+
zodType = z.array(z.enum([first, ...rest]));
|
|
774
|
+
}
|
|
775
|
+
else if (value.items.type === 'string') {
|
|
776
|
+
zodType = z.array(z.string());
|
|
777
|
+
}
|
|
778
|
+
else if (value.items.type === 'number') {
|
|
779
|
+
zodType = z.array(z.number());
|
|
780
|
+
}
|
|
781
|
+
else if (value.items.type === 'boolean') {
|
|
782
|
+
zodType = z.array(z.boolean());
|
|
783
|
+
}
|
|
784
|
+
else if (value.items.type === 'object') {
|
|
785
|
+
zodType = z.array(z.record(z.string(), z.any()));
|
|
786
|
+
}
|
|
787
|
+
else {
|
|
788
|
+
zodType = z.array(z.any());
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
zodType = z.array(z.any());
|
|
793
|
+
}
|
|
794
|
+
// Add array-specific validations
|
|
795
|
+
if (value.minItems)
|
|
796
|
+
zodType = zodType.min(value.minItems);
|
|
797
|
+
if (value.maxItems)
|
|
798
|
+
zodType = zodType.max(value.maxItems);
|
|
799
|
+
}
|
|
800
|
+
else if (value.type === 'object') {
|
|
801
|
+
zodType = z.record(z.string(), z.any());
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
zodType = z.any();
|
|
805
|
+
}
|
|
806
|
+
// Add description if present in JSON schema
|
|
807
|
+
if (value.description) {
|
|
808
|
+
zodType = zodType.describe(value.description);
|
|
809
|
+
}
|
|
810
|
+
// Make optional if not in required array OR if marked as optional
|
|
811
|
+
if (!jsonSchema.required?.includes(key) || value.optional === true) {
|
|
812
|
+
zodType = zodType.optional();
|
|
813
|
+
}
|
|
814
|
+
properties[key] = zodType;
|
|
815
|
+
}
|
|
816
|
+
return z.object(properties);
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* Extract user-visible activities with rich formatting and complete information
|
|
820
|
+
*/
|
|
821
|
+
extractUserVisibleActivities(events) {
|
|
822
|
+
const activities = [];
|
|
823
|
+
for (const event of events) {
|
|
824
|
+
switch (event.eventType) {
|
|
825
|
+
case 'tool_execution': {
|
|
826
|
+
const data = event.data;
|
|
827
|
+
const resultStr = JSON.stringify(data.result);
|
|
828
|
+
activities.push(`🔧 **${data.toolName}** ${data.duration ? `(${data.duration}ms)` : ''}\n` +
|
|
829
|
+
` 📥 Input: ${JSON.stringify(data.args)}\n` +
|
|
830
|
+
` 📤 Output: ${resultStr}`);
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
case 'transfer': {
|
|
834
|
+
const data = event.data;
|
|
835
|
+
activities.push(`🔄 **Transfer**: ${data.fromAgent} → ${data.targetAgent}\n` +
|
|
836
|
+
` ${data.reason ? `Reason: ${data.reason}` : 'Control transfer'}\n` +
|
|
837
|
+
` ${data.context ? `Context: ${JSON.stringify(data.context, null, 2)}` : ''}`);
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
case 'delegation_sent': {
|
|
841
|
+
const data = event.data;
|
|
842
|
+
activities.push(`📤 **Delegation Sent** [${data.delegationId}]: ${data.fromAgent} → ${data.targetAgent}\n` +
|
|
843
|
+
` Task: ${data.taskDescription}\n` +
|
|
844
|
+
` ${data.context ? `Context: ${JSON.stringify(data.context, null, 2)}` : ''}`);
|
|
845
|
+
break;
|
|
846
|
+
}
|
|
847
|
+
case 'delegation_returned': {
|
|
848
|
+
const data = event.data;
|
|
849
|
+
activities.push(`📥 **Delegation Returned** [${data.delegationId}]: ${data.fromAgent} ← ${data.targetAgent}\n` +
|
|
850
|
+
` Result: ${JSON.stringify(data.result, null, 2)}`);
|
|
851
|
+
break;
|
|
852
|
+
}
|
|
853
|
+
case 'artifact_saved': {
|
|
854
|
+
const data = event.data;
|
|
855
|
+
activities.push(`💾 **Artifact Saved**: ${data.artifactType}\n` +
|
|
856
|
+
` ID: ${data.artifactId}\n` +
|
|
857
|
+
` Task: ${data.taskId}\n` +
|
|
858
|
+
` ${data.summaryData ? `Summary: ${data.summaryData}` : ''}\n` +
|
|
859
|
+
` ${data.fullData ? `Full Data: ${data.fullData}` : ''}`);
|
|
860
|
+
break;
|
|
861
|
+
}
|
|
862
|
+
case 'agent_reasoning': {
|
|
863
|
+
const data = event.data;
|
|
864
|
+
activities.push(`⚙️ **Reasoning**: reasoning\n` +
|
|
865
|
+
` Full Details: ${JSON.stringify(data.parts, null, 2)}`);
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
868
|
+
case 'agent_generate': {
|
|
869
|
+
const data = event.data;
|
|
870
|
+
activities.push(`⚙️ **Generation**: ${data.generationType}\n` +
|
|
871
|
+
` Full Details: ${JSON.stringify(data.parts, null, 2)}`);
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
default: {
|
|
875
|
+
activities.push(`📋 **${event.eventType}**: ${JSON.stringify(event.data, null, 2)}`);
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return activities;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* Generate fallback summary when LLM fails
|
|
884
|
+
*/
|
|
885
|
+
generateFallbackSummary(events, elapsedTime) {
|
|
886
|
+
const timeStr = Math.round(elapsedTime / 1000);
|
|
887
|
+
const toolCalls = events.filter((e) => e.eventType === 'tool_execution').length;
|
|
888
|
+
const artifacts = events.filter((e) => e.eventType === 'artifact_saved').length;
|
|
889
|
+
if (artifacts > 0) {
|
|
890
|
+
return `Generated ${artifacts} result${artifacts > 1 ? 's' : ''} so far (${timeStr}s elapsed)`;
|
|
891
|
+
}
|
|
892
|
+
else if (toolCalls > 0) {
|
|
893
|
+
return `Used ${toolCalls} tool${toolCalls > 1 ? 's' : ''} to gather information (${timeStr}s elapsed)`;
|
|
894
|
+
}
|
|
895
|
+
else {
|
|
896
|
+
return `Processing your request... (${timeStr}s elapsed)`;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
900
|
+
* Process a single artifact to generate name and description using conversation context
|
|
901
|
+
*/
|
|
902
|
+
async processArtifact(artifactData) {
|
|
903
|
+
return tracer.startActiveSpan(createSpanName('graph_session.process_artifact'), {
|
|
904
|
+
attributes: {
|
|
905
|
+
'graph_session.id': this.sessionId,
|
|
906
|
+
'artifact.id': artifactData.artifactId,
|
|
907
|
+
'artifact.type': artifactData.artifactType || 'unknown',
|
|
908
|
+
'tenant.id': artifactData.tenantId || 'unknown',
|
|
909
|
+
'project.id': artifactData.projectId || 'unknown',
|
|
910
|
+
'context.id': artifactData.contextId || 'unknown',
|
|
911
|
+
has_tenant_id: !!artifactData.tenantId,
|
|
912
|
+
has_project_id: !!artifactData.projectId,
|
|
913
|
+
has_context_id: !!artifactData.contextId,
|
|
914
|
+
has_metadata: !!artifactData.metadata,
|
|
915
|
+
tool_call_id: artifactData.metadata?.toolCallId || 'missing',
|
|
916
|
+
pending_generation: !!artifactData.pendingGeneration,
|
|
917
|
+
},
|
|
918
|
+
}, async (span) => {
|
|
919
|
+
try {
|
|
920
|
+
// We need tenantId, projectId, and contextId to get conversation history
|
|
921
|
+
if (!artifactData.tenantId || !artifactData.projectId || !artifactData.contextId) {
|
|
922
|
+
span.setAttributes({
|
|
923
|
+
'validation.failed': true,
|
|
924
|
+
missing_tenant_id: !artifactData.tenantId,
|
|
925
|
+
missing_project_id: !artifactData.projectId,
|
|
926
|
+
missing_context_id: !artifactData.contextId,
|
|
927
|
+
});
|
|
928
|
+
throw new Error('Missing required session info (tenantId, projectId, or contextId) for artifact processing');
|
|
929
|
+
}
|
|
930
|
+
span.setAttributes({ 'validation.passed': true });
|
|
931
|
+
const { getFormattedConversationHistory } = await import('../data/conversations.js');
|
|
932
|
+
const conversationHistory = await getFormattedConversationHistory({
|
|
933
|
+
tenantId: artifactData.tenantId,
|
|
934
|
+
projectId: artifactData.projectId,
|
|
935
|
+
conversationId: artifactData.contextId,
|
|
936
|
+
options: {
|
|
937
|
+
limit: 10, // Only need recent context
|
|
938
|
+
includeInternal: false, // Focus on user messages
|
|
939
|
+
messageTypes: ['chat'],
|
|
940
|
+
},
|
|
941
|
+
});
|
|
942
|
+
// Find the specific tool call that generated this artifact
|
|
943
|
+
// Now toolId and toolCallId should be the same since we use AI SDK's toolCallId consistently
|
|
944
|
+
const toolCallEvent = this.events.find((event) => event.eventType === 'tool_execution' &&
|
|
945
|
+
event.data &&
|
|
946
|
+
'toolId' in event.data &&
|
|
947
|
+
event.data.toolId === artifactData.metadata?.toolCallId);
|
|
948
|
+
// Prepare context for name/description generation
|
|
949
|
+
const toolContext = toolCallEvent
|
|
950
|
+
? {
|
|
951
|
+
toolName: toolCallEvent.data.toolName,
|
|
952
|
+
args: toolCallEvent.data.args,
|
|
953
|
+
}
|
|
954
|
+
: null;
|
|
955
|
+
const prompt = `Name this artifact (max 50 chars) and describe it (max 150 chars).
|
|
956
|
+
|
|
957
|
+
Tool Context: ${toolContext ? JSON.stringify(toolContext, null, 2) : 'No tool context'}
|
|
958
|
+
Context: ${conversationHistory?.slice(-200) || 'Processing'}
|
|
959
|
+
Type: ${artifactData.artifactType || 'data'}
|
|
960
|
+
Summary: ${JSON.stringify(artifactData.summaryProps, null, 2)}
|
|
961
|
+
Full: ${JSON.stringify(artifactData.fullProps, null, 2)}
|
|
962
|
+
|
|
963
|
+
Make it specific and relevant.`;
|
|
964
|
+
const model = ModelFactory.createModel(this.statusUpdateState?.summarizerModel || { model: 'openai/gpt-4.1-nano-2025-04-14' });
|
|
965
|
+
const schema = z.object({
|
|
966
|
+
name: z.string().max(50).describe('Concise, descriptive name for the artifact'),
|
|
967
|
+
description: z
|
|
968
|
+
.string()
|
|
969
|
+
.max(150)
|
|
970
|
+
.describe("Brief description of the artifact's relevance to the user's question"),
|
|
971
|
+
});
|
|
972
|
+
// Add nested span for LLM generation
|
|
973
|
+
const { object: result } = await tracer.startActiveSpan(createSpanName('graph_session.generate_artifact_metadata'), {
|
|
974
|
+
attributes: {
|
|
975
|
+
'llm.model': this.statusUpdateState?.summarizerModel?.model ||
|
|
976
|
+
'openai/gpt-4.1-nano-2025-04-14',
|
|
977
|
+
'llm.operation': 'generate_object',
|
|
978
|
+
'artifact.id': artifactData.artifactId,
|
|
979
|
+
'prompt.length': prompt.length,
|
|
980
|
+
},
|
|
981
|
+
}, async (generationSpan) => {
|
|
982
|
+
try {
|
|
983
|
+
const result = await generateObject({
|
|
984
|
+
model,
|
|
985
|
+
prompt,
|
|
986
|
+
schema,
|
|
987
|
+
experimental_telemetry: {
|
|
988
|
+
isEnabled: true,
|
|
989
|
+
functionId: `artifact_processing_${artifactData.artifactId}`,
|
|
990
|
+
recordInputs: true,
|
|
991
|
+
recordOutputs: true,
|
|
992
|
+
metadata: {
|
|
993
|
+
operation: 'artifact_name_description_generation',
|
|
994
|
+
sessionId: this.sessionId,
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
});
|
|
998
|
+
generationSpan.setAttributes({
|
|
999
|
+
'generation.name_length': result.object.name.length,
|
|
1000
|
+
'generation.description_length': result.object.description.length,
|
|
1001
|
+
});
|
|
1002
|
+
generationSpan.setStatus({ code: SpanStatusCode.OK });
|
|
1003
|
+
return result;
|
|
1004
|
+
}
|
|
1005
|
+
catch (error) {
|
|
1006
|
+
handleSpanError(generationSpan, error);
|
|
1007
|
+
throw error;
|
|
1008
|
+
}
|
|
1009
|
+
finally {
|
|
1010
|
+
generationSpan.end();
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
// Now save the artifact to the ledger with the generated name and description
|
|
1014
|
+
const { addLedgerArtifacts } = await import('@inkeep/agents-core');
|
|
1015
|
+
const artifactToSave = {
|
|
1016
|
+
artifactId: artifactData.artifactId,
|
|
1017
|
+
name: result.name,
|
|
1018
|
+
description: result.description,
|
|
1019
|
+
type: 'source',
|
|
1020
|
+
taskId: artifactData.taskId,
|
|
1021
|
+
parts: [
|
|
1022
|
+
{
|
|
1023
|
+
kind: 'data',
|
|
1024
|
+
data: {
|
|
1025
|
+
summary: artifactData.summaryProps || {},
|
|
1026
|
+
full: artifactData.fullProps || {},
|
|
1027
|
+
},
|
|
1028
|
+
},
|
|
1029
|
+
],
|
|
1030
|
+
metadata: artifactData.metadata || {},
|
|
1031
|
+
};
|
|
1032
|
+
await addLedgerArtifacts(dbClient)({
|
|
1033
|
+
scopes: {
|
|
1034
|
+
tenantId: artifactData.tenantId,
|
|
1035
|
+
projectId: artifactData.projectId,
|
|
1036
|
+
},
|
|
1037
|
+
contextId: artifactData.contextId,
|
|
1038
|
+
taskId: artifactData.taskId,
|
|
1039
|
+
artifacts: [artifactToSave],
|
|
1040
|
+
});
|
|
1041
|
+
logger.info({
|
|
1042
|
+
sessionId: this.sessionId,
|
|
1043
|
+
artifactId: artifactData.artifactId,
|
|
1044
|
+
name: result.name,
|
|
1045
|
+
description: result.description,
|
|
1046
|
+
}, 'Artifact successfully saved to ledger with generated name and description');
|
|
1047
|
+
// Mark main span as successful
|
|
1048
|
+
span.setAttributes({
|
|
1049
|
+
'artifact.name': result.name,
|
|
1050
|
+
'artifact.description': result.description,
|
|
1051
|
+
'processing.success': true,
|
|
1052
|
+
});
|
|
1053
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
1054
|
+
}
|
|
1055
|
+
catch (error) {
|
|
1056
|
+
// Handle span error
|
|
1057
|
+
handleSpanError(span, error);
|
|
1058
|
+
logger.error({
|
|
1059
|
+
sessionId: this.sessionId,
|
|
1060
|
+
artifactId: artifactData.artifactId,
|
|
1061
|
+
error: error instanceof Error ? error.message : 'Unknown error',
|
|
1062
|
+
}, 'Failed to process artifact');
|
|
1063
|
+
// Fallback: save artifact with basic info
|
|
1064
|
+
try {
|
|
1065
|
+
const { addLedgerArtifacts } = await import('@inkeep/agents-core');
|
|
1066
|
+
const fallbackArtifact = {
|
|
1067
|
+
artifactId: artifactData.artifactId,
|
|
1068
|
+
name: `Artifact ${artifactData.artifactId.substring(0, 8)}`,
|
|
1069
|
+
description: `${artifactData.artifactType || 'Data'} from ${artifactData.metadata?.toolName || 'tool results'}`,
|
|
1070
|
+
taskId: artifactData.taskId,
|
|
1071
|
+
parts: [
|
|
1072
|
+
{
|
|
1073
|
+
kind: 'data',
|
|
1074
|
+
data: {
|
|
1075
|
+
summary: artifactData.summaryProps || {},
|
|
1076
|
+
full: artifactData.fullProps || {},
|
|
1077
|
+
},
|
|
1078
|
+
},
|
|
1079
|
+
],
|
|
1080
|
+
metadata: artifactData.metadata || {},
|
|
1081
|
+
};
|
|
1082
|
+
if (artifactData.tenantId && artifactData.projectId) {
|
|
1083
|
+
await addLedgerArtifacts(dbClient)({
|
|
1084
|
+
scopes: {
|
|
1085
|
+
tenantId: artifactData.tenantId,
|
|
1086
|
+
projectId: artifactData.projectId,
|
|
1087
|
+
},
|
|
1088
|
+
contextId: artifactData.contextId || 'unknown',
|
|
1089
|
+
taskId: artifactData.taskId,
|
|
1090
|
+
artifacts: [fallbackArtifact],
|
|
1091
|
+
});
|
|
1092
|
+
logger.info({
|
|
1093
|
+
sessionId: this.sessionId,
|
|
1094
|
+
artifactId: artifactData.artifactId,
|
|
1095
|
+
}, 'Saved artifact with fallback name/description after processing error');
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
catch (fallbackError) {
|
|
1099
|
+
logger.error({
|
|
1100
|
+
sessionId: this.sessionId,
|
|
1101
|
+
artifactId: artifactData.artifactId,
|
|
1102
|
+
error: fallbackError instanceof Error ? fallbackError.message : 'Unknown error',
|
|
1103
|
+
}, 'Failed to save artifact even with fallback');
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
finally {
|
|
1107
|
+
// Always end the main span
|
|
1108
|
+
span.end();
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Manages GraphSession instances for message-level tracking
|
|
1115
|
+
*/
|
|
1116
|
+
export class GraphSessionManager {
|
|
1117
|
+
sessions = new Map();
|
|
1118
|
+
/**
|
|
1119
|
+
* Create a new session for a message
|
|
1120
|
+
*/
|
|
1121
|
+
createSession(messageId, graphId, tenantId, projectId) {
|
|
1122
|
+
const sessionId = messageId; // Use messageId directly as sessionId
|
|
1123
|
+
const session = new GraphSession(sessionId, messageId, graphId, tenantId, projectId);
|
|
1124
|
+
this.sessions.set(sessionId, session);
|
|
1125
|
+
logger.info({ sessionId, messageId, graphId, tenantId, projectId }, 'GraphSession created');
|
|
1126
|
+
return sessionId;
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Initialize status updates for a session
|
|
1130
|
+
*/
|
|
1131
|
+
initializeStatusUpdates(sessionId, config, summarizerModel) {
|
|
1132
|
+
const session = this.sessions.get(sessionId);
|
|
1133
|
+
if (session) {
|
|
1134
|
+
session.initializeStatusUpdates(config, summarizerModel);
|
|
1135
|
+
}
|
|
1136
|
+
else {
|
|
1137
|
+
logger.error({
|
|
1138
|
+
sessionId,
|
|
1139
|
+
availableSessions: Array.from(this.sessions.keys()),
|
|
1140
|
+
}, 'Session not found for status updates initialization');
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
/**
|
|
1144
|
+
* Get an existing session
|
|
1145
|
+
*/
|
|
1146
|
+
getSession(sessionId) {
|
|
1147
|
+
return this.sessions.get(sessionId) || null;
|
|
1148
|
+
}
|
|
1149
|
+
/**
|
|
1150
|
+
* Record an event in a session
|
|
1151
|
+
*/
|
|
1152
|
+
recordEvent(sessionId, eventType, agentId, data) {
|
|
1153
|
+
const session = this.sessions.get(sessionId);
|
|
1154
|
+
if (!session) {
|
|
1155
|
+
logger.warn({ sessionId }, 'Attempted to record event in non-existent session');
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
session.recordEvent(eventType, agentId, data);
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* End a session and return the final event data
|
|
1162
|
+
*/
|
|
1163
|
+
endSession(sessionId) {
|
|
1164
|
+
const session = this.sessions.get(sessionId);
|
|
1165
|
+
if (!session) {
|
|
1166
|
+
logger.warn({ sessionId }, 'Attempted to end non-existent session');
|
|
1167
|
+
return [];
|
|
1168
|
+
}
|
|
1169
|
+
const events = session.getEvents();
|
|
1170
|
+
const summary = session.getSummary();
|
|
1171
|
+
logger.info({ sessionId, summary }, 'GraphSession ended');
|
|
1172
|
+
// Clean up session resources including status update timers
|
|
1173
|
+
session.cleanup();
|
|
1174
|
+
// Clean up the session from memory
|
|
1175
|
+
this.sessions.delete(sessionId);
|
|
1176
|
+
return events;
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Set text streaming state for a session
|
|
1180
|
+
*/
|
|
1181
|
+
setTextStreaming(sessionId, isStreaming) {
|
|
1182
|
+
const session = this.sessions.get(sessionId);
|
|
1183
|
+
if (session) {
|
|
1184
|
+
session.setTextStreaming(isStreaming);
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* Get summary of all active sessions
|
|
1189
|
+
*/
|
|
1190
|
+
getActiveSessions() {
|
|
1191
|
+
return Array.from(this.sessions.values()).map((session) => ({
|
|
1192
|
+
sessionId: session.sessionId,
|
|
1193
|
+
messageId: session.messageId,
|
|
1194
|
+
eventCount: session.getEvents().length,
|
|
1195
|
+
}));
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
// Global instance
|
|
1199
|
+
export const graphSessionManager = new GraphSessionManager();
|