@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.
- package/README.md +117 -0
- package/dist/AgentExecutionServer.d.ts +23 -0
- package/dist/AgentExecutionServer.d.ts.map +1 -0
- package/dist/AgentExecutionServer.js +32 -0
- package/dist/__tests__/setup.d.ts +4 -0
- package/dist/__tests__/setup.d.ts.map +1 -0
- package/dist/__tests__/setup.js +50 -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 +657 -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 +154 -0
- package/dist/agents/Agent.d.ts.map +1 -0
- package/dist/agents/Agent.js +1105 -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 +61 -0
- package/dist/agents/ToolSessionManager.d.ts.map +1 -0
- package/dist/agents/ToolSessionManager.js +143 -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 +33 -0
- package/dist/agents/relationTools.d.ts.map +1 -0
- package/dist/agents/relationTools.js +245 -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 +4 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +194 -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 +73 -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 +43 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +63 -0
- package/dist/handlers/executionHandler.d.ts +36 -0
- package/dist/handlers/executionHandler.d.ts.map +1 -0
- package/dist/handlers/executionHandler.js +402 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -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 +4 -0
- package/dist/routes/agents.d.ts.map +1 -0
- package/dist/routes/agents.js +155 -0
- package/dist/routes/chat.d.ts +4 -0
- package/dist/routes/chat.d.ts.map +1 -0
- package/dist/routes/chat.js +308 -0
- package/dist/routes/chatDataStream.d.ts +4 -0
- package/dist/routes/chatDataStream.d.ts.map +1 -0
- package/dist/routes/chatDataStream.js +179 -0
- package/dist/routes/mcp.d.ts +4 -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 +97 -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 +79 -0
- package/dist/utils/agent-operations.d.ts.map +1 -0
- package/dist/utils/agent-operations.js +67 -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 +251 -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 +200 -0
- package/dist/utils/graph-session.d.ts.map +1 -0
- package/dist/utils/graph-session.js +1009 -0
- package/dist/utils/incremental-stream-parser.d.ts +57 -0
- package/dist/utils/incremental-stream-parser.d.ts.map +1 -0
- package/dist/utils/incremental-stream-parser.js +287 -0
- package/dist/utils/response-formatter.d.ts +27 -0
- package/dist/utils/response-formatter.d.ts.map +1 -0
- package/dist/utils/response-formatter.js +160 -0
- package/dist/utils/stream-helpers.d.ts +162 -0
- package/dist/utils/stream-helpers.d.ts.map +1 -0
- package/dist/utils/stream-helpers.js +385 -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 +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();
|