@inkeep/agents-run-api 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/README.md +117 -0
  2. package/dist/AgentExecutionServer.d.ts +23 -0
  3. package/dist/AgentExecutionServer.d.ts.map +1 -0
  4. package/dist/AgentExecutionServer.js +32 -0
  5. package/dist/__tests__/setup.d.ts +4 -0
  6. package/dist/__tests__/setup.d.ts.map +1 -0
  7. package/dist/__tests__/setup.js +50 -0
  8. package/dist/__tests__/utils/testProject.d.ts +18 -0
  9. package/dist/__tests__/utils/testProject.d.ts.map +1 -0
  10. package/dist/__tests__/utils/testProject.js +26 -0
  11. package/dist/__tests__/utils/testRequest.d.ts +8 -0
  12. package/dist/__tests__/utils/testRequest.d.ts.map +1 -0
  13. package/dist/__tests__/utils/testRequest.js +32 -0
  14. package/dist/__tests__/utils/testTenant.d.ts +64 -0
  15. package/dist/__tests__/utils/testTenant.d.ts.map +1 -0
  16. package/dist/__tests__/utils/testTenant.js +71 -0
  17. package/dist/a2a/client.d.ts +182 -0
  18. package/dist/a2a/client.d.ts.map +1 -0
  19. package/dist/a2a/client.js +645 -0
  20. package/dist/a2a/handlers.d.ts +4 -0
  21. package/dist/a2a/handlers.d.ts.map +1 -0
  22. package/dist/a2a/handlers.js +657 -0
  23. package/dist/a2a/transfer.d.ts +18 -0
  24. package/dist/a2a/transfer.d.ts.map +1 -0
  25. package/dist/a2a/transfer.js +22 -0
  26. package/dist/a2a/types.d.ts +63 -0
  27. package/dist/a2a/types.d.ts.map +1 -0
  28. package/dist/a2a/types.js +1 -0
  29. package/dist/agents/Agent.d.ts +154 -0
  30. package/dist/agents/Agent.d.ts.map +1 -0
  31. package/dist/agents/Agent.js +1105 -0
  32. package/dist/agents/ModelFactory.d.ts +62 -0
  33. package/dist/agents/ModelFactory.d.ts.map +1 -0
  34. package/dist/agents/ModelFactory.js +208 -0
  35. package/dist/agents/SystemPromptBuilder.d.ts +14 -0
  36. package/dist/agents/SystemPromptBuilder.d.ts.map +1 -0
  37. package/dist/agents/SystemPromptBuilder.js +62 -0
  38. package/dist/agents/ToolSessionManager.d.ts +61 -0
  39. package/dist/agents/ToolSessionManager.d.ts.map +1 -0
  40. package/dist/agents/ToolSessionManager.js +143 -0
  41. package/dist/agents/artifactTools.d.ts +30 -0
  42. package/dist/agents/artifactTools.d.ts.map +1 -0
  43. package/dist/agents/artifactTools.js +463 -0
  44. package/dist/agents/generateTaskHandler.d.ts +41 -0
  45. package/dist/agents/generateTaskHandler.d.ts.map +1 -0
  46. package/dist/agents/generateTaskHandler.js +350 -0
  47. package/dist/agents/relationTools.d.ts +33 -0
  48. package/dist/agents/relationTools.d.ts.map +1 -0
  49. package/dist/agents/relationTools.js +245 -0
  50. package/dist/agents/types.d.ts +23 -0
  51. package/dist/agents/types.d.ts.map +1 -0
  52. package/dist/agents/types.js +1 -0
  53. package/dist/agents/versions/V1Config.d.ts +21 -0
  54. package/dist/agents/versions/V1Config.d.ts.map +1 -0
  55. package/dist/agents/versions/V1Config.js +285 -0
  56. package/dist/app.d.ts +4 -0
  57. package/dist/app.d.ts.map +1 -0
  58. package/dist/app.js +194 -0
  59. package/dist/data/agentGraph.d.ts +4 -0
  60. package/dist/data/agentGraph.d.ts.map +1 -0
  61. package/dist/data/agentGraph.js +73 -0
  62. package/dist/data/agents.d.ts +4 -0
  63. package/dist/data/agents.d.ts.map +1 -0
  64. package/dist/data/agents.js +73 -0
  65. package/dist/data/conversations.d.ts +59 -0
  66. package/dist/data/conversations.d.ts.map +1 -0
  67. package/dist/data/conversations.js +216 -0
  68. package/dist/data/db/clean.d.ts +6 -0
  69. package/dist/data/db/clean.d.ts.map +1 -0
  70. package/dist/data/db/clean.js +77 -0
  71. package/dist/data/db/dbClient.d.ts +3 -0
  72. package/dist/data/db/dbClient.d.ts.map +1 -0
  73. package/dist/data/db/dbClient.js +13 -0
  74. package/dist/env.d.ts +43 -0
  75. package/dist/env.d.ts.map +1 -0
  76. package/dist/env.js +63 -0
  77. package/dist/handlers/executionHandler.d.ts +36 -0
  78. package/dist/handlers/executionHandler.d.ts.map +1 -0
  79. package/dist/handlers/executionHandler.js +402 -0
  80. package/dist/index.d.ts +5 -0
  81. package/dist/index.d.ts.map +1 -0
  82. package/dist/index.js +43 -0
  83. package/dist/instrumentation.d.ts +13 -0
  84. package/dist/instrumentation.d.ts.map +1 -0
  85. package/dist/instrumentation.js +66 -0
  86. package/dist/logger.d.ts +4 -0
  87. package/dist/logger.d.ts.map +1 -0
  88. package/dist/logger.js +32 -0
  89. package/dist/middleware/api-key-auth.d.ts +22 -0
  90. package/dist/middleware/api-key-auth.d.ts.map +1 -0
  91. package/dist/middleware/api-key-auth.js +139 -0
  92. package/dist/middleware/index.d.ts +2 -0
  93. package/dist/middleware/index.d.ts.map +1 -0
  94. package/dist/middleware/index.js +1 -0
  95. package/dist/openapi.d.ts +2 -0
  96. package/dist/openapi.d.ts.map +1 -0
  97. package/dist/openapi.js +36 -0
  98. package/dist/routes/agents.d.ts +4 -0
  99. package/dist/routes/agents.d.ts.map +1 -0
  100. package/dist/routes/agents.js +155 -0
  101. package/dist/routes/chat.d.ts +4 -0
  102. package/dist/routes/chat.d.ts.map +1 -0
  103. package/dist/routes/chat.js +308 -0
  104. package/dist/routes/chatDataStream.d.ts +4 -0
  105. package/dist/routes/chatDataStream.d.ts.map +1 -0
  106. package/dist/routes/chatDataStream.js +179 -0
  107. package/dist/routes/mcp.d.ts +4 -0
  108. package/dist/routes/mcp.d.ts.map +1 -0
  109. package/dist/routes/mcp.js +500 -0
  110. package/dist/tracer.d.ts +24 -0
  111. package/dist/tracer.d.ts.map +1 -0
  112. package/dist/tracer.js +97 -0
  113. package/dist/types/chat.d.ts +25 -0
  114. package/dist/types/chat.d.ts.map +1 -0
  115. package/dist/types/chat.js +1 -0
  116. package/dist/types/execution-context.d.ts +14 -0
  117. package/dist/types/execution-context.d.ts.map +1 -0
  118. package/dist/types/execution-context.js +14 -0
  119. package/dist/utils/agent-operations.d.ts +79 -0
  120. package/dist/utils/agent-operations.d.ts.map +1 -0
  121. package/dist/utils/agent-operations.js +67 -0
  122. package/dist/utils/artifact-component-schema.d.ts +29 -0
  123. package/dist/utils/artifact-component-schema.d.ts.map +1 -0
  124. package/dist/utils/artifact-component-schema.js +119 -0
  125. package/dist/utils/artifact-parser.d.ts +71 -0
  126. package/dist/utils/artifact-parser.d.ts.map +1 -0
  127. package/dist/utils/artifact-parser.js +251 -0
  128. package/dist/utils/cleanup.d.ts +19 -0
  129. package/dist/utils/cleanup.d.ts.map +1 -0
  130. package/dist/utils/cleanup.js +66 -0
  131. package/dist/utils/data-component-schema.d.ts +6 -0
  132. package/dist/utils/data-component-schema.d.ts.map +1 -0
  133. package/dist/utils/data-component-schema.js +43 -0
  134. package/dist/utils/graph-session.d.ts +200 -0
  135. package/dist/utils/graph-session.d.ts.map +1 -0
  136. package/dist/utils/graph-session.js +1009 -0
  137. package/dist/utils/incremental-stream-parser.d.ts +57 -0
  138. package/dist/utils/incremental-stream-parser.d.ts.map +1 -0
  139. package/dist/utils/incremental-stream-parser.js +287 -0
  140. package/dist/utils/response-formatter.d.ts +27 -0
  141. package/dist/utils/response-formatter.d.ts.map +1 -0
  142. package/dist/utils/response-formatter.js +160 -0
  143. package/dist/utils/stream-helpers.d.ts +162 -0
  144. package/dist/utils/stream-helpers.d.ts.map +1 -0
  145. package/dist/utils/stream-helpers.js +385 -0
  146. package/dist/utils/stream-registry.d.ts +18 -0
  147. package/dist/utils/stream-registry.d.ts.map +1 -0
  148. package/dist/utils/stream-registry.js +33 -0
  149. package/package.json +88 -0
@@ -0,0 +1,1009 @@
1
+ import { generateText, generateObject } from 'ai';
2
+ import { z } from 'zod';
3
+ import { SpanStatusCode } from '@opentelemetry/api';
4
+ import { ModelFactory } from '../agents/ModelFactory.js';
5
+ import { getLogger } from '../logger.js';
6
+ import { getGlobalTracer, createSpanName, handleSpanError } from '../tracer.js';
7
+ import { statusUpdateOp } from './agent-operations.js';
8
+ import { getStreamHelper } from './stream-registry.js';
9
+ import { getFormattedConversationHistory } from '../data/conversations.js';
10
+ import dbClient from '../data/db/dbClient.js';
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
+ constructor(sessionId, messageId, graphId, tenantId, projectId) {
31
+ this.sessionId = sessionId;
32
+ this.messageId = messageId;
33
+ this.graphId = graphId;
34
+ this.tenantId = tenantId;
35
+ this.projectId = projectId;
36
+ logger.debug({ sessionId, messageId, graphId }, 'GraphSession created');
37
+ }
38
+ /**
39
+ * Initialize status updates for this session
40
+ */
41
+ initializeStatusUpdates(config, summarizerModel) {
42
+ const now = Date.now();
43
+ this.statusUpdateState = {
44
+ lastUpdateTime: now,
45
+ lastEventCount: 0,
46
+ startTime: now,
47
+ summarizerModel,
48
+ config: {
49
+ numEvents: config.numEvents || 10,
50
+ timeInSeconds: config.timeInSeconds || 30,
51
+ ...config,
52
+ },
53
+ };
54
+ // Set up time-based updates if configured
55
+ if (this.statusUpdateState.config.timeInSeconds) {
56
+ this.statusUpdateTimer = setInterval(async () => {
57
+ // Guard against cleanup race condition
58
+ if (!this.statusUpdateState || this.isEnded) {
59
+ logger.debug({ sessionId: this.sessionId }, 'Timer triggered but session already cleaned up or ended');
60
+ if (this.statusUpdateTimer) {
61
+ clearInterval(this.statusUpdateTimer);
62
+ this.statusUpdateTimer = undefined;
63
+ }
64
+ return;
65
+ }
66
+ await this.checkAndSendTimeBasedUpdate();
67
+ }, this.statusUpdateState.config.timeInSeconds * 1000);
68
+ logger.info({
69
+ sessionId: this.sessionId,
70
+ intervalMs: this.statusUpdateState.config.timeInSeconds * 1000,
71
+ }, 'Time-based status update timer started');
72
+ }
73
+ }
74
+ /**
75
+ * Record an event in the session and trigger status updates if configured
76
+ */
77
+ recordEvent(eventType, agentId, data) {
78
+ // Don't record events or trigger updates if session has ended
79
+ if (this.isEnded) {
80
+ logger.debug({
81
+ sessionId: this.sessionId,
82
+ eventType,
83
+ agentId,
84
+ }, 'Event received after session ended - ignoring');
85
+ return;
86
+ }
87
+ const event = {
88
+ timestamp: Date.now(),
89
+ eventType,
90
+ agentId,
91
+ data,
92
+ };
93
+ this.events.push(event);
94
+ // Process artifact if it's pending generation
95
+ if (eventType === 'artifact_saved' && data.pendingGeneration) {
96
+ // Fire and forget - process artifact completely asynchronously without any blocking
97
+ setImmediate(() => {
98
+ // No await, no spans at trigger level - truly fire and forget
99
+ this.processArtifact(data).catch((error) => {
100
+ logger.error({
101
+ sessionId: this.sessionId,
102
+ artifactId: data.artifactId,
103
+ error: error instanceof Error ? error.message : 'Unknown error',
104
+ stack: error instanceof Error ? error.stack : undefined,
105
+ artifactData: data,
106
+ }, 'Failed to process artifact - fire and forget error');
107
+ });
108
+ });
109
+ }
110
+ // Trigger status updates check (only sends if thresholds met)
111
+ if (!this.isEnded) {
112
+ this.checkStatusUpdates();
113
+ }
114
+ }
115
+ /**
116
+ * Check and send status updates if configured (async, non-blocking)
117
+ */
118
+ checkStatusUpdates() {
119
+ if (this.isEnded) {
120
+ logger.debug({ sessionId: this.sessionId }, 'Session has ended - skipping status update check');
121
+ return;
122
+ }
123
+ if (!this.statusUpdateState) {
124
+ logger.debug({ sessionId: this.sessionId }, 'No status update state - skipping check');
125
+ return;
126
+ }
127
+ // Status updates are enabled by having statusUpdateState
128
+ // Store reference to prevent race condition during async execution
129
+ const statusUpdateState = this.statusUpdateState;
130
+ // Run async without blocking the main flow
131
+ setImmediate(async () => {
132
+ try {
133
+ // Check if session is still active and statusUpdateState hasn't been cleaned up or text is streaming
134
+ if (this.isEnded || !statusUpdateState || this.isTextStreaming) {
135
+ return;
136
+ }
137
+ const currentEventCount = this.events.length;
138
+ const numEventsThreshold = statusUpdateState.config.numEvents;
139
+ const shouldUpdateByEvents = numEventsThreshold &&
140
+ currentEventCount >= statusUpdateState.lastEventCount + numEventsThreshold;
141
+ if (shouldUpdateByEvents) {
142
+ await this.generateAndSendUpdate();
143
+ }
144
+ }
145
+ catch (error) {
146
+ logger.error({
147
+ sessionId: this.sessionId,
148
+ error: error instanceof Error ? error.message : 'Unknown error',
149
+ }, 'Failed to check status updates during event recording');
150
+ }
151
+ });
152
+ }
153
+ /**
154
+ * Check and send time-based status updates
155
+ */
156
+ async checkAndSendTimeBasedUpdate() {
157
+ if (this.isEnded) {
158
+ logger.debug({ sessionId: this.sessionId }, 'Session has ended - skipping time-based update');
159
+ return;
160
+ }
161
+ if (!this.statusUpdateState) {
162
+ logger.debug({ sessionId: this.sessionId }, 'No status updates configured for time-based check');
163
+ return;
164
+ }
165
+ // Only send if we have new events since last update
166
+ const newEventCount = this.events.length - this.statusUpdateState.lastEventCount;
167
+ if (newEventCount === 0) {
168
+ return;
169
+ }
170
+ try {
171
+ // Always send time-based updates regardless of event count
172
+ await this.generateAndSendUpdate();
173
+ }
174
+ catch (error) {
175
+ logger.error({
176
+ sessionId: this.sessionId,
177
+ error: error instanceof Error ? error.message : 'Unknown error',
178
+ }, 'Failed to send time-based status update');
179
+ }
180
+ }
181
+ /**
182
+ * Get all events in chronological order
183
+ */
184
+ getEvents() {
185
+ return [...this.events];
186
+ }
187
+ /**
188
+ * Get events filtered by type
189
+ */
190
+ getEventsByType(eventType) {
191
+ return this.events.filter((event) => event.eventType === eventType);
192
+ }
193
+ /**
194
+ * Get events filtered by agent
195
+ */
196
+ getEventsByAgent(agentId) {
197
+ return this.events.filter((event) => event.agentId === agentId);
198
+ }
199
+ /**
200
+ * Get summary of session activity
201
+ */
202
+ getSummary() {
203
+ const eventCounts = this.events.reduce((counts, event) => {
204
+ counts[event.eventType] = (counts[event.eventType] || 0) + 1;
205
+ return counts;
206
+ }, {});
207
+ const agentCounts = this.events.reduce((counts, event) => {
208
+ counts[event.agentId] = (counts[event.agentId] || 0) + 1;
209
+ return counts;
210
+ }, {});
211
+ return {
212
+ sessionId: this.sessionId,
213
+ messageId: this.messageId,
214
+ graphId: this.graphId,
215
+ totalEvents: this.events.length,
216
+ eventCounts,
217
+ agentCounts,
218
+ startTime: this.events[0]?.timestamp,
219
+ endTime: this.events[this.events.length - 1]?.timestamp,
220
+ duration: this.events.length > 0
221
+ ? this.events[this.events.length - 1].timestamp - this.events[0].timestamp
222
+ : 0,
223
+ };
224
+ }
225
+ /**
226
+ * Mark that text streaming has started (to suppress status updates)
227
+ */
228
+ setTextStreaming(isStreaming) {
229
+ this.isTextStreaming = isStreaming;
230
+ }
231
+ /**
232
+ * Check if text is currently being streamed
233
+ */
234
+ isCurrentlyStreaming() {
235
+ return this.isTextStreaming;
236
+ }
237
+ /**
238
+ * Clean up status update resources when session ends
239
+ */
240
+ cleanup() {
241
+ // Mark session as ended
242
+ this.isEnded = true;
243
+ if (this.statusUpdateTimer) {
244
+ clearInterval(this.statusUpdateTimer);
245
+ this.statusUpdateTimer = undefined;
246
+ }
247
+ this.statusUpdateState = undefined;
248
+ }
249
+ /**
250
+ * Generate and send a status update using graph-level summarizer
251
+ */
252
+ async generateAndSendUpdate() {
253
+ if (this.isEnded) {
254
+ logger.debug({ sessionId: this.sessionId }, 'Session has ended - not generating update');
255
+ return;
256
+ }
257
+ if (this.isTextStreaming) {
258
+ logger.debug({ sessionId: this.sessionId }, 'Text is currently streaming - skipping status update');
259
+ return;
260
+ }
261
+ if (this.isGeneratingUpdate) {
262
+ logger.debug({ sessionId: this.sessionId }, 'Update already in progress - skipping duplicate generation');
263
+ return;
264
+ }
265
+ if (!this.statusUpdateState) {
266
+ logger.warn({ sessionId: this.sessionId }, 'No status update state - cannot generate update');
267
+ return;
268
+ }
269
+ if (!this.graphId) {
270
+ logger.warn({ sessionId: this.sessionId }, 'No graph ID - cannot generate update');
271
+ return;
272
+ }
273
+ // Only send if we have new events since last update
274
+ const newEventCount = this.events.length - this.statusUpdateState.lastEventCount;
275
+ if (newEventCount === 0) {
276
+ return;
277
+ }
278
+ // Set flag to prevent concurrent updates
279
+ this.isGeneratingUpdate = true;
280
+ // Store references at start to prevent race conditions
281
+ const statusUpdateState = this.statusUpdateState;
282
+ const graphId = this.graphId;
283
+ try {
284
+ const streamHelper = getStreamHelper(this.sessionId);
285
+ if (!streamHelper) {
286
+ logger.warn({ sessionId: this.sessionId }, 'No stream helper found - cannot send status update');
287
+ this.isGeneratingUpdate = false;
288
+ return;
289
+ }
290
+ const now = Date.now();
291
+ const elapsedTime = now - statusUpdateState.startTime;
292
+ // Generate status update - either structured or text summary
293
+ let operation;
294
+ if (statusUpdateState.config.statusComponents &&
295
+ statusUpdateState.config.statusComponents.length > 0) {
296
+ // Use generateObject to intelligently select relevant data components
297
+ const result = await this.generateStructuredStatusUpdate(this.events.slice(statusUpdateState.lastEventCount), elapsedTime, statusUpdateState.config.statusComponents, statusUpdateState.summarizerModel, this.previousSummaries);
298
+ if (result.operations && result.operations.length > 0) {
299
+ // Send each operation separately using writeData for dynamic types
300
+ for (const op of result.operations) {
301
+ // Guard against empty/invalid operations
302
+ if (!op || !op.type || !op.data || Object.keys(op.data).length === 0) {
303
+ logger.warn({
304
+ sessionId: this.sessionId,
305
+ operation: op,
306
+ }, 'Skipping empty or invalid structured operation');
307
+ continue;
308
+ }
309
+ const operationToSend = {
310
+ type: op.type,
311
+ ctx: op.data,
312
+ };
313
+ await streamHelper.writeData('data-operation', operationToSend);
314
+ }
315
+ // Store summaries for next time - use full JSON for better comparison
316
+ const summaryTexts = result.operations.map((op) => JSON.stringify({ type: op.type, data: op.data }));
317
+ this.previousSummaries.push(...summaryTexts);
318
+ // Update state after sending all operations
319
+ if (this.statusUpdateState) {
320
+ this.statusUpdateState.lastUpdateTime = now;
321
+ this.statusUpdateState.lastEventCount = this.events.length;
322
+ }
323
+ return;
324
+ }
325
+ else {
326
+ // Fall through to regular text summary if no structured updates
327
+ }
328
+ }
329
+ else {
330
+ // Use regular text generation for simple summaries
331
+ const summary = await this.generateProgressSummary(this.events.slice(statusUpdateState.lastEventCount), elapsedTime, statusUpdateState.summarizerModel, this.previousSummaries);
332
+ // Store this summary for next time
333
+ this.previousSummaries.push(summary);
334
+ // Create standard status update operation
335
+ operation = statusUpdateOp({
336
+ summary,
337
+ eventCount: this.events.length,
338
+ elapsedTime,
339
+ currentPhase: 'processing',
340
+ activeAgent: 'system',
341
+ graphId,
342
+ sessionId: this.sessionId,
343
+ });
344
+ }
345
+ // Keep only last 3 summaries to avoid context getting too large
346
+ if (this.previousSummaries.length > 3) {
347
+ this.previousSummaries.shift();
348
+ }
349
+ // Guard against sending empty/undefined operations that break streams
350
+ if (!operation || !operation.type || !operation.ctx) {
351
+ logger.warn({
352
+ sessionId: this.sessionId,
353
+ operation,
354
+ }, 'Skipping empty or invalid status update operation');
355
+ return;
356
+ }
357
+ await streamHelper.writeData('data-operation', operation);
358
+ // Update state - check if still exists (could be cleaned up during async operation)
359
+ if (this.statusUpdateState) {
360
+ this.statusUpdateState.lastUpdateTime = now;
361
+ this.statusUpdateState.lastEventCount = this.events.length;
362
+ }
363
+ }
364
+ catch (error) {
365
+ logger.error({
366
+ sessionId: this.sessionId,
367
+ error: error instanceof Error ? error.message : 'Unknown error',
368
+ stack: error instanceof Error ? error.stack : undefined,
369
+ }, '❌ Failed to generate status update');
370
+ }
371
+ finally {
372
+ // Clear the flag to allow future updates
373
+ this.isGeneratingUpdate = false;
374
+ }
375
+ }
376
+ /**
377
+ * Generate user-focused progress summary hiding internal operations
378
+ */
379
+ async generateProgressSummary(newEvents, elapsedTime, summarizerModel, previousSummaries = []) {
380
+ return tracer.startActiveSpan(createSpanName('graph_session.generate_progress_summary'), {
381
+ attributes: {
382
+ 'graph_session.id': this.sessionId,
383
+ 'events.count': newEvents.length,
384
+ 'elapsed_time.seconds': Math.round(elapsedTime / 1000),
385
+ 'llm.model': summarizerModel?.model || 'openai/gpt-4.1-nano-2025-04-14',
386
+ 'previous_summaries.count': previousSummaries.length,
387
+ },
388
+ }, async (span) => {
389
+ try {
390
+ // Extract user-visible activities (hide internal agent operations)
391
+ const userVisibleActivities = this.extractUserVisibleActivities(newEvents);
392
+ // Get conversation history to understand user's context and question
393
+ let conversationContext = '';
394
+ if (this.tenantId && this.projectId) {
395
+ try {
396
+ const conversationHistory = await getFormattedConversationHistory({
397
+ tenantId: this.tenantId,
398
+ projectId: this.projectId,
399
+ conversationId: this.sessionId,
400
+ options: {
401
+ limit: 10, // Get recent conversation context
402
+ maxOutputTokens: 2000,
403
+ },
404
+ filters: {},
405
+ });
406
+ conversationContext = conversationHistory.trim()
407
+ ? `\nUser's Question/Context:\n${conversationHistory}\n`
408
+ : '';
409
+ }
410
+ catch (error) {
411
+ logger.warn({ sessionId: this.sessionId, error }, 'Failed to fetch conversation history for status update');
412
+ }
413
+ }
414
+ const previousSummaryContext = previousSummaries.length > 0
415
+ ? `\nPrevious updates provided to user:\n${previousSummaries.map((s, i) => `${i + 1}. ${s}`).join('\n')}\n`
416
+ : '';
417
+ // Use custom prompt if provided, otherwise use default
418
+ const basePrompt = `Generate a brief status update for what is happening now to the user. Please keep it short and concise and informative based on what the user has asked for.${conversationContext}${previousSummaries.length > 0 ? `\n${previousSummaryContext}` : ''}
419
+
420
+ Activities:\n${userVisibleActivities.join('\n') || 'No New Activities'}
421
+
422
+ What's happening now?
423
+
424
+ ${this.statusUpdateState?.config.prompt?.trim() || ''}`;
425
+ const prompt = basePrompt;
426
+ const model = ModelFactory.createModel((summarizerModel && summarizerModel.model?.trim()) ? summarizerModel : { model: 'openai/gpt-4.1-nano-2025-04-14' });
427
+ const { text } = await generateText({
428
+ model,
429
+ prompt,
430
+ experimental_telemetry: {
431
+ isEnabled: true,
432
+ functionId: `status_update_${this.sessionId}`,
433
+ recordInputs: true,
434
+ recordOutputs: true,
435
+ metadata: {
436
+ operation: 'progress_summary_generation',
437
+ sessionId: this.sessionId,
438
+ },
439
+ },
440
+ });
441
+ span.setAttributes({
442
+ 'summary.length': text.trim().length,
443
+ 'user_activities.count': userVisibleActivities.length,
444
+ });
445
+ span.setStatus({ code: SpanStatusCode.OK });
446
+ return text.trim();
447
+ }
448
+ catch (error) {
449
+ handleSpanError(span, error);
450
+ logger.error({ error }, 'Failed to generate summary, using fallback');
451
+ return this.generateFallbackSummary(newEvents, elapsedTime);
452
+ }
453
+ finally {
454
+ span.end();
455
+ }
456
+ });
457
+ }
458
+ /**
459
+ * Generate structured status update using configured data components
460
+ */
461
+ async generateStructuredStatusUpdate(newEvents, elapsedTime, statusComponents, summarizerModel, previousSummaries = []) {
462
+ return tracer.startActiveSpan(createSpanName('graph_session.generate_structured_update'), {
463
+ attributes: {
464
+ 'graph_session.id': this.sessionId,
465
+ 'events.count': newEvents.length,
466
+ 'elapsed_time.seconds': Math.round(elapsedTime / 1000),
467
+ 'llm.model': summarizerModel?.model || 'openai/gpt-4.1-nano-2025-04-14',
468
+ 'status_components.count': statusComponents.length,
469
+ 'previous_summaries.count': previousSummaries.length,
470
+ },
471
+ }, async (span) => {
472
+ try {
473
+ // Extract user-visible activities
474
+ const userVisibleActivities = this.extractUserVisibleActivities(newEvents);
475
+ // Get conversation history to understand user's context and question
476
+ let conversationContext = '';
477
+ if (this.tenantId && this.projectId) {
478
+ try {
479
+ const conversationHistory = await getFormattedConversationHistory({
480
+ tenantId: this.tenantId,
481
+ projectId: this.projectId,
482
+ conversationId: this.sessionId,
483
+ options: {
484
+ limit: 10, // Get recent conversation context
485
+ maxOutputTokens: 2000,
486
+ },
487
+ filters: {},
488
+ });
489
+ conversationContext = conversationHistory.trim()
490
+ ? `\nUser's Question/Context:\n${conversationHistory}\n`
491
+ : '';
492
+ }
493
+ catch (error) {
494
+ logger.warn({ sessionId: this.sessionId, error }, 'Failed to fetch conversation history for structured status update');
495
+ }
496
+ }
497
+ const previousSummaryContext = previousSummaries.length > 0
498
+ ? `\nPrevious updates sent to user:\n${previousSummaries.map((s, i) => `${i + 1}. ${s}`).join('\n')}\n`
499
+ : '';
500
+ // Build schema for data components and no_relevant_updates option
501
+ const selectionSchema = z.object(Object.fromEntries([
502
+ // Add no_relevant_updates schema
503
+ [
504
+ 'no_relevant_updates',
505
+ z
506
+ .object({
507
+ no_updates: z.boolean().default(true),
508
+ })
509
+ .optional()
510
+ .describe('Use when nothing substantially new to report. Should only use on its own.'),
511
+ ],
512
+ // Add all other component schemas
513
+ ...statusComponents.map((component) => [
514
+ component.id,
515
+ this.buildZodSchema(component.schema)
516
+ .optional()
517
+ .describe(component.description || component.name),
518
+ ]),
519
+ ]));
520
+ // Use custom prompt if provided, otherwise use default
521
+ const basePrompt = `Generate status updates for relevant components based on what the user has asked for.${conversationContext}${previousSummaries.length > 0 ? `\n${previousSummaryContext}` : ''}
522
+
523
+ Activities:\n${userVisibleActivities.join('\n') || 'No New Activities'}
524
+
525
+ Available components: no_relevant_updates, ${statusComponents.map((c) => c.id).join(', ')}
526
+
527
+ Rules:
528
+ - Fill in data for relevant components only
529
+ - Use 'no_relevant_updates' if nothing substantially new to report
530
+ - Never repeat previous values
531
+ - You are ONE AI (no agents/delegations)
532
+
533
+ ${this.statusUpdateState?.config.prompt?.trim() || ''}`;
534
+ const prompt = basePrompt;
535
+ const model = ModelFactory.createModel((summarizerModel && summarizerModel.model?.trim()) ? summarizerModel : { model: 'openai/gpt-4.1-nano-2025-04-14' });
536
+ const { object } = await generateObject({
537
+ model,
538
+ prompt,
539
+ schema: selectionSchema,
540
+ experimental_telemetry: {
541
+ isEnabled: true,
542
+ functionId: `structured_update_${this.sessionId}`,
543
+ recordInputs: true,
544
+ recordOutputs: true,
545
+ metadata: {
546
+ operation: 'structured_status_update_generation',
547
+ sessionId: this.sessionId,
548
+ },
549
+ },
550
+ });
551
+ const result = object;
552
+ // Extract components that have data (skip no_relevant_updates and empty components)
553
+ const operations = [];
554
+ for (const [componentId, data] of Object.entries(result)) {
555
+ // Skip no_relevant_updates - we don't send any operation for this
556
+ if (componentId === 'no_relevant_updates') {
557
+ continue;
558
+ }
559
+ // Only include components that have actual data
560
+ if (data && typeof data === 'object' && Object.keys(data).length > 0) {
561
+ operations.push({
562
+ type: componentId,
563
+ data: data,
564
+ });
565
+ }
566
+ }
567
+ span.setAttributes({
568
+ 'operations.count': operations.length,
569
+ 'user_activities.count': userVisibleActivities.length,
570
+ 'result_keys.count': Object.keys(result).length,
571
+ });
572
+ span.setStatus({ code: SpanStatusCode.OK });
573
+ return { operations };
574
+ }
575
+ catch (error) {
576
+ handleSpanError(span, error);
577
+ logger.error({ error }, 'Failed to generate structured update, using fallback');
578
+ return { operations: [] };
579
+ }
580
+ finally {
581
+ span.end();
582
+ }
583
+ });
584
+ }
585
+ /**
586
+ * Build Zod schema from JSON schema configuration
587
+ */
588
+ buildZodSchema(jsonSchema) {
589
+ const properties = {};
590
+ for (const [key, value] of Object.entries(jsonSchema.properties)) {
591
+ // Simple type mapping - can be expanded as needed
592
+ if (value.type === 'string') {
593
+ properties[key] = z.string();
594
+ }
595
+ else if (value.type === 'number') {
596
+ properties[key] = z.number();
597
+ }
598
+ else if (value.type === 'boolean') {
599
+ properties[key] = z.boolean();
600
+ }
601
+ else if (value.type === 'array') {
602
+ properties[key] = z.array(z.any());
603
+ }
604
+ else if (value.type === 'object') {
605
+ properties[key] = z.record(z.string(), z.any());
606
+ }
607
+ else {
608
+ properties[key] = z.any();
609
+ }
610
+ // Make optional if not in required array
611
+ if (!jsonSchema.required?.includes(key)) {
612
+ properties[key] = properties[key].optional();
613
+ }
614
+ }
615
+ return z.object(properties);
616
+ }
617
+ /**
618
+ * Extract user-visible activities with rich formatting and complete information
619
+ */
620
+ extractUserVisibleActivities(events) {
621
+ const activities = [];
622
+ for (const event of events) {
623
+ switch (event.eventType) {
624
+ case 'tool_execution': {
625
+ const data = event.data;
626
+ const resultStr = JSON.stringify(data.result);
627
+ activities.push(`🔧 **${data.toolName}** ${data.duration ? `(${data.duration}ms)` : ''}\n` +
628
+ ` 📥 Input: ${JSON.stringify(data.args)}\n` +
629
+ ` 📤 Output: ${resultStr}`);
630
+ break;
631
+ }
632
+ case 'transfer': {
633
+ const data = event.data;
634
+ activities.push(`🔄 **Transfer**: ${data.fromAgent} → ${data.targetAgent}\n` +
635
+ ` ${data.reason ? `Reason: ${data.reason}` : 'Control transfer'}\n` +
636
+ ` ${data.context ? `Context: ${JSON.stringify(data.context, null, 2)}` : ''}`);
637
+ break;
638
+ }
639
+ case 'delegation_sent': {
640
+ const data = event.data;
641
+ activities.push(`📤 **Delegation Sent** [${data.delegationId}]: ${data.fromAgent} → ${data.targetAgent}\n` +
642
+ ` Task: ${data.taskDescription}\n` +
643
+ ` ${data.context ? `Context: ${JSON.stringify(data.context, null, 2)}` : ''}`);
644
+ break;
645
+ }
646
+ case 'delegation_returned': {
647
+ const data = event.data;
648
+ activities.push(`📥 **Delegation Returned** [${data.delegationId}]: ${data.fromAgent} ← ${data.targetAgent}\n` +
649
+ ` Result: ${JSON.stringify(data.result, null, 2)}`);
650
+ break;
651
+ }
652
+ case 'artifact_saved': {
653
+ const data = event.data;
654
+ activities.push(`💾 **Artifact Saved**: ${data.artifactType}\n` +
655
+ ` ID: ${data.artifactId}\n` +
656
+ ` Task: ${data.taskId}\n` +
657
+ ` ${data.summaryData ? `Summary: ${data.summaryData}` : ''}\n` +
658
+ ` ${data.fullData ? `Full Data: ${data.fullData}` : ''}`);
659
+ break;
660
+ }
661
+ case 'agent_generate': {
662
+ const data = event.data;
663
+ if (data.generationType !== 'artifact_name_description') {
664
+ activities.push(`⚙️ **Generation**: ${data.generationType}\n` +
665
+ ` Full Details: ${JSON.stringify(data.parts, null, 2)}`);
666
+ }
667
+ break;
668
+ }
669
+ default: {
670
+ activities.push(`📋 **${event.eventType}**: ${JSON.stringify(event.data, null, 2)}`);
671
+ break;
672
+ }
673
+ }
674
+ }
675
+ return activities;
676
+ }
677
+ /**
678
+ * Generate fallback summary when LLM fails
679
+ */
680
+ generateFallbackSummary(events, elapsedTime) {
681
+ const timeStr = Math.round(elapsedTime / 1000);
682
+ const toolCalls = events.filter((e) => e.eventType === 'tool_execution').length;
683
+ const artifacts = events.filter((e) => e.eventType === 'artifact_saved').length;
684
+ if (artifacts > 0) {
685
+ return `Generated ${artifacts} result${artifacts > 1 ? 's' : ''} so far (${timeStr}s elapsed)`;
686
+ }
687
+ else if (toolCalls > 0) {
688
+ return `Used ${toolCalls} tool${toolCalls > 1 ? 's' : ''} to gather information (${timeStr}s elapsed)`;
689
+ }
690
+ else {
691
+ return `Processing your request... (${timeStr}s elapsed)`;
692
+ }
693
+ }
694
+ /**
695
+ * Process a single artifact to generate name and description using conversation context
696
+ */
697
+ async processArtifact(artifactData) {
698
+ return tracer.startActiveSpan(createSpanName('graph_session.process_artifact'), {
699
+ attributes: {
700
+ 'graph_session.id': this.sessionId,
701
+ 'artifact.id': artifactData.artifactId,
702
+ 'artifact.type': artifactData.artifactType || 'unknown',
703
+ 'tenant.id': artifactData.tenantId || 'unknown',
704
+ 'project.id': artifactData.projectId || 'unknown',
705
+ 'context.id': artifactData.contextId || 'unknown',
706
+ has_tenant_id: !!artifactData.tenantId,
707
+ has_project_id: !!artifactData.projectId,
708
+ has_context_id: !!artifactData.contextId,
709
+ has_metadata: !!artifactData.metadata,
710
+ tool_call_id: artifactData.metadata?.toolCallId || 'missing',
711
+ pending_generation: !!artifactData.pendingGeneration,
712
+ },
713
+ }, async (span) => {
714
+ try {
715
+ // We need tenantId, projectId, and contextId to get conversation history
716
+ if (!artifactData.tenantId || !artifactData.projectId || !artifactData.contextId) {
717
+ span.setAttributes({
718
+ 'validation.failed': true,
719
+ missing_tenant_id: !artifactData.tenantId,
720
+ missing_project_id: !artifactData.projectId,
721
+ missing_context_id: !artifactData.contextId,
722
+ });
723
+ throw new Error('Missing required session info (tenantId, projectId, or contextId) for artifact processing');
724
+ }
725
+ span.setAttributes({ 'validation.passed': true });
726
+ // Get conversation history for context
727
+ span.setAttributes({ step: 'importing_conversations' });
728
+ const { getFormattedConversationHistory } = await import('../data/conversations.js');
729
+ span.setAttributes({ step: 'fetching_conversation_history' });
730
+ const conversationHistory = await getFormattedConversationHistory({
731
+ tenantId: artifactData.tenantId,
732
+ projectId: artifactData.projectId,
733
+ conversationId: artifactData.contextId,
734
+ options: {
735
+ limit: 10, // Only need recent context
736
+ includeInternal: false, // Focus on user messages
737
+ messageTypes: ['chat'],
738
+ },
739
+ });
740
+ span.setAttributes({
741
+ step: 'conversation_history_fetched',
742
+ 'conversation_history.length': conversationHistory.length,
743
+ });
744
+ // Find the specific tool call that generated this artifact
745
+ // Now toolId and toolCallId should be the same since we use AI SDK's toolCallId consistently
746
+ const toolCallEvent = this.events.find((event) => event.eventType === 'tool_execution' &&
747
+ event.data &&
748
+ 'toolId' in event.data &&
749
+ event.data.toolId === artifactData.metadata?.toolCallId);
750
+ const toolResultEvent = this.events.find((event) => event.eventType === 'tool_execution' &&
751
+ event.data &&
752
+ 'toolId' in event.data &&
753
+ event.data.toolId === artifactData.metadata?.toolCallId);
754
+ // Prepare context for name/description generation
755
+ const toolContext = toolCallEvent
756
+ ? {
757
+ toolName: toolCallEvent.data.toolName,
758
+ args: toolCallEvent.data.args,
759
+ }
760
+ : null;
761
+ const toolResult = toolResultEvent
762
+ ? {
763
+ result: toolResultEvent.data.result,
764
+ }
765
+ : null;
766
+ const prompt = `Name this artifact (max 50 chars) and describe it (max 150 chars).
767
+
768
+ Context: ${conversationHistory?.slice(-200) || 'Processing'}
769
+ Type: ${artifactData.artifactType || 'data'}
770
+ Summary: ${JSON.stringify(artifactData.summaryProps, null, 2)}
771
+ Full: ${JSON.stringify(artifactData.fullProps, null, 2)}
772
+
773
+ Make it specific and relevant.`;
774
+ const model = ModelFactory.createModel(this.statusUpdateState?.summarizerModel || { model: 'openai/gpt-4.1-nano-2025-04-14' });
775
+ const schema = z.object({
776
+ name: z.string().max(50).describe('Concise, descriptive name for the artifact'),
777
+ description: z
778
+ .string()
779
+ .max(150)
780
+ .describe("Brief description of the artifact's relevance to the user's question"),
781
+ });
782
+ // Add nested span for LLM generation
783
+ const { object: result } = await tracer.startActiveSpan(createSpanName('graph_session.generate_artifact_metadata'), {
784
+ attributes: {
785
+ 'llm.model': this.statusUpdateState?.summarizerModel?.model ||
786
+ 'openai/gpt-4.1-nano-2025-04-14',
787
+ 'llm.operation': 'generate_object',
788
+ 'artifact.id': artifactData.artifactId,
789
+ 'prompt.length': prompt.length,
790
+ },
791
+ }, async (generationSpan) => {
792
+ try {
793
+ const result = await generateObject({
794
+ model,
795
+ prompt,
796
+ schema,
797
+ experimental_telemetry: {
798
+ isEnabled: true,
799
+ functionId: `artifact_processing_${artifactData.artifactId}`,
800
+ recordInputs: true,
801
+ recordOutputs: true,
802
+ metadata: {
803
+ operation: 'artifact_name_description_generation',
804
+ sessionId: this.sessionId,
805
+ },
806
+ },
807
+ });
808
+ generationSpan.setAttributes({
809
+ 'generation.name_length': result.object.name.length,
810
+ 'generation.description_length': result.object.description.length,
811
+ });
812
+ generationSpan.setStatus({ code: SpanStatusCode.OK });
813
+ return result;
814
+ }
815
+ catch (error) {
816
+ handleSpanError(generationSpan, error);
817
+ throw error;
818
+ }
819
+ finally {
820
+ generationSpan.end();
821
+ }
822
+ });
823
+ // Now save the artifact to the ledger with the generated name and description
824
+ const { addLedgerArtifacts } = await import('@inkeep/agents-core');
825
+ const artifactToSave = {
826
+ artifactId: artifactData.artifactId,
827
+ name: result.name,
828
+ description: result.description,
829
+ type: 'source',
830
+ taskId: artifactData.taskId,
831
+ parts: [
832
+ {
833
+ kind: 'data',
834
+ data: {
835
+ summary: artifactData.summaryProps || {},
836
+ full: artifactData.fullProps || {},
837
+ },
838
+ },
839
+ ],
840
+ metadata: artifactData.metadata || {},
841
+ };
842
+ await addLedgerArtifacts(dbClient)({
843
+ scopes: {
844
+ tenantId: artifactData.tenantId,
845
+ projectId: artifactData.projectId,
846
+ },
847
+ contextId: artifactData.contextId,
848
+ taskId: artifactData.taskId,
849
+ artifacts: [artifactToSave],
850
+ });
851
+ logger.info({
852
+ sessionId: this.sessionId,
853
+ artifactId: artifactData.artifactId,
854
+ name: result.name,
855
+ description: result.description,
856
+ }, 'Artifact successfully saved to ledger with generated name and description');
857
+ // Mark main span as successful
858
+ span.setAttributes({
859
+ 'artifact.name': result.name,
860
+ 'artifact.description': result.description,
861
+ 'processing.success': true,
862
+ });
863
+ span.setStatus({ code: SpanStatusCode.OK });
864
+ }
865
+ catch (error) {
866
+ // Handle span error
867
+ handleSpanError(span, error);
868
+ logger.error({
869
+ sessionId: this.sessionId,
870
+ artifactId: artifactData.artifactId,
871
+ error: error instanceof Error ? error.message : 'Unknown error',
872
+ }, 'Failed to process artifact');
873
+ // Fallback: save artifact with basic info
874
+ try {
875
+ const { addLedgerArtifacts } = await import('@inkeep/agents-core');
876
+ const fallbackArtifact = {
877
+ artifactId: artifactData.artifactId,
878
+ name: `Artifact ${artifactData.artifactId.substring(0, 8)}`,
879
+ description: `${artifactData.artifactType || 'Data'} from ${artifactData.metadata?.toolName || 'tool results'}`,
880
+ taskId: artifactData.taskId,
881
+ parts: [
882
+ {
883
+ kind: 'data',
884
+ data: {
885
+ summary: artifactData.summaryProps || {},
886
+ full: artifactData.fullProps || {},
887
+ },
888
+ },
889
+ ],
890
+ metadata: artifactData.metadata || {},
891
+ };
892
+ if (artifactData.tenantId && artifactData.projectId) {
893
+ await addLedgerArtifacts(dbClient)({
894
+ scopes: {
895
+ tenantId: artifactData.tenantId,
896
+ projectId: artifactData.projectId,
897
+ },
898
+ contextId: artifactData.contextId || 'unknown',
899
+ taskId: artifactData.taskId,
900
+ artifacts: [fallbackArtifact],
901
+ });
902
+ logger.info({
903
+ sessionId: this.sessionId,
904
+ artifactId: artifactData.artifactId,
905
+ }, 'Saved artifact with fallback name/description after processing error');
906
+ }
907
+ }
908
+ catch (fallbackError) {
909
+ logger.error({
910
+ sessionId: this.sessionId,
911
+ artifactId: artifactData.artifactId,
912
+ error: fallbackError instanceof Error ? fallbackError.message : 'Unknown error',
913
+ }, 'Failed to save artifact even with fallback');
914
+ }
915
+ }
916
+ finally {
917
+ // Always end the main span
918
+ span.end();
919
+ }
920
+ });
921
+ }
922
+ }
923
+ /**
924
+ * Manages GraphSession instances for message-level tracking
925
+ */
926
+ export class GraphSessionManager {
927
+ sessions = new Map();
928
+ /**
929
+ * Create a new session for a message
930
+ */
931
+ createSession(messageId, graphId, tenantId, projectId) {
932
+ const sessionId = messageId; // Use messageId directly as sessionId
933
+ const session = new GraphSession(sessionId, messageId, graphId, tenantId, projectId);
934
+ this.sessions.set(sessionId, session);
935
+ logger.info({ sessionId, messageId, graphId, tenantId, projectId }, 'GraphSession created');
936
+ return sessionId;
937
+ }
938
+ /**
939
+ * Initialize status updates for a session
940
+ */
941
+ initializeStatusUpdates(sessionId, config, summarizerModel) {
942
+ const session = this.sessions.get(sessionId);
943
+ if (session) {
944
+ session.initializeStatusUpdates(config, summarizerModel);
945
+ }
946
+ else {
947
+ logger.error({
948
+ sessionId,
949
+ availableSessions: Array.from(this.sessions.keys()),
950
+ }, 'Session not found for status updates initialization');
951
+ }
952
+ }
953
+ /**
954
+ * Get an existing session
955
+ */
956
+ getSession(sessionId) {
957
+ return this.sessions.get(sessionId) || null;
958
+ }
959
+ /**
960
+ * Record an event in a session
961
+ */
962
+ recordEvent(sessionId, eventType, agentId, data) {
963
+ const session = this.sessions.get(sessionId);
964
+ if (!session) {
965
+ logger.warn({ sessionId }, 'Attempted to record event in non-existent session');
966
+ return;
967
+ }
968
+ session.recordEvent(eventType, agentId, data);
969
+ }
970
+ /**
971
+ * End a session and return the final event data
972
+ */
973
+ endSession(sessionId) {
974
+ const session = this.sessions.get(sessionId);
975
+ if (!session) {
976
+ logger.warn({ sessionId }, 'Attempted to end non-existent session');
977
+ return [];
978
+ }
979
+ const events = session.getEvents();
980
+ const summary = session.getSummary();
981
+ logger.info({ sessionId, summary }, 'GraphSession ended');
982
+ // Clean up session resources including status update timers
983
+ session.cleanup();
984
+ // Clean up the session from memory
985
+ this.sessions.delete(sessionId);
986
+ return events;
987
+ }
988
+ /**
989
+ * Set text streaming state for a session
990
+ */
991
+ setTextStreaming(sessionId, isStreaming) {
992
+ const session = this.sessions.get(sessionId);
993
+ if (session) {
994
+ session.setTextStreaming(isStreaming);
995
+ }
996
+ }
997
+ /**
998
+ * Get summary of all active sessions
999
+ */
1000
+ getActiveSessions() {
1001
+ return Array.from(this.sessions.values()).map((session) => ({
1002
+ sessionId: session.sessionId,
1003
+ messageId: session.messageId,
1004
+ eventCount: session.getEvents().length,
1005
+ }));
1006
+ }
1007
+ }
1008
+ // Global instance
1009
+ export const graphSessionManager = new GraphSessionManager();