@probelabs/probe-chat 0.6.0-rc100
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 +338 -0
- package/TRACING.md +226 -0
- package/appTracer.js +947 -0
- package/auth.js +76 -0
- package/bin/probe-chat.js +13 -0
- package/cancelRequest.js +84 -0
- package/fileSpanExporter.js +183 -0
- package/implement/README.md +228 -0
- package/implement/backends/AiderBackend.js +750 -0
- package/implement/backends/BaseBackend.js +276 -0
- package/implement/backends/ClaudeCodeBackend.js +767 -0
- package/implement/backends/MockBackend.js +237 -0
- package/implement/backends/registry.js +85 -0
- package/implement/core/BackendManager.js +567 -0
- package/implement/core/ImplementTool.js +354 -0
- package/implement/core/config.js +428 -0
- package/implement/core/timeouts.js +58 -0
- package/implement/core/utils.js +496 -0
- package/implement/types/BackendTypes.js +126 -0
- package/index.html +3751 -0
- package/index.js +582 -0
- package/logo.png +0 -0
- package/package.json +101 -0
- package/probeChat.js +269 -0
- package/probeTool.js +714 -0
- package/storage/JsonChatStorage.js +476 -0
- package/telemetry.js +287 -0
- package/test/integration/chatFlows.test.js +320 -0
- package/test/integration/toolCalling.test.js +471 -0
- package/test/mocks/mockLLMProvider.js +269 -0
- package/test/test-backends.js +90 -0
- package/test/testUtils.js +530 -0
- package/test/unit/backendTimeout.test.js +161 -0
- package/test/verify-tests.js +118 -0
- package/tokenCounter.js +419 -0
- package/tokenUsageDisplay.js +134 -0
- package/tools.js +186 -0
- package/webServer.js +1103 -0
package/appTracer.js
ADDED
|
@@ -0,0 +1,947 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Application Tracing Layer for Probe Chat
|
|
3
|
+
*
|
|
4
|
+
* This module provides granular tracing that follows application logic closely,
|
|
5
|
+
* replacing the generic Vercel AI SDK tracing with application-specific spans.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { trace, SpanStatusCode, SpanKind, context, TraceFlags } from '@opentelemetry/api';
|
|
9
|
+
import { randomUUID, createHash } from 'crypto';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Convert a session ID to a valid OpenTelemetry trace ID (32-char hex)
|
|
13
|
+
*/
|
|
14
|
+
function sessionIdToTraceId(sessionId) {
|
|
15
|
+
// Create a hash of the session ID and take first 32 chars
|
|
16
|
+
const hash = createHash('sha256').update(sessionId).digest('hex');
|
|
17
|
+
return hash.substring(0, 32);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// OpenTelemetry semantic conventions and custom attributes
|
|
21
|
+
const OTEL_ATTRS = {
|
|
22
|
+
// Standard semantic conventions
|
|
23
|
+
SERVICE_NAME: 'service.name',
|
|
24
|
+
SERVICE_VERSION: 'service.version',
|
|
25
|
+
HTTP_METHOD: 'http.method',
|
|
26
|
+
HTTP_STATUS_CODE: 'http.status_code',
|
|
27
|
+
ERROR_TYPE: 'error.type',
|
|
28
|
+
ERROR_MESSAGE: 'error.message',
|
|
29
|
+
|
|
30
|
+
// Custom application attributes following OpenTelemetry naming conventions
|
|
31
|
+
APP_SESSION_ID: 'app.session.id',
|
|
32
|
+
APP_MESSAGE_TYPE: 'app.message.type',
|
|
33
|
+
APP_MESSAGE_CONTENT: 'app.message.content',
|
|
34
|
+
APP_MESSAGE_LENGTH: 'app.message.length',
|
|
35
|
+
APP_MESSAGE_HASH: 'app.message.hash',
|
|
36
|
+
APP_AI_PROVIDER: 'app.ai.provider',
|
|
37
|
+
APP_AI_MODEL: 'app.ai.model',
|
|
38
|
+
APP_AI_TEMPERATURE: 'app.ai.temperature',
|
|
39
|
+
APP_AI_MAX_TOKENS: 'app.ai.max_tokens',
|
|
40
|
+
APP_AI_RESPONSE_CONTENT: 'app.ai.response.content',
|
|
41
|
+
APP_AI_RESPONSE_LENGTH: 'app.ai.response.length',
|
|
42
|
+
APP_AI_RESPONSE_HASH: 'app.ai.response.hash',
|
|
43
|
+
APP_AI_COMPLETION_TOKENS: 'app.ai.completion_tokens',
|
|
44
|
+
APP_AI_PROMPT_TOKENS: 'app.ai.prompt_tokens',
|
|
45
|
+
APP_AI_FINISH_REASON: 'app.ai.finish_reason',
|
|
46
|
+
APP_TOOL_NAME: 'app.tool.name',
|
|
47
|
+
APP_TOOL_PARAMS: 'app.tool.params',
|
|
48
|
+
APP_TOOL_RESULT: 'app.tool.result',
|
|
49
|
+
APP_TOOL_SUCCESS: 'app.tool.success',
|
|
50
|
+
APP_ITERATION_NUMBER: 'app.iteration.number'
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
class AppTracer {
|
|
54
|
+
constructor() {
|
|
55
|
+
// Use consistent tracer name across the application
|
|
56
|
+
this.tracer = trace.getTracer('probe-chat', '1.0.0');
|
|
57
|
+
this.activeSpans = new Map();
|
|
58
|
+
this.sessionSpans = new Map();
|
|
59
|
+
this.sessionContexts = new Map(); // Store active context for each session
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get the shared tracer instance
|
|
64
|
+
*/
|
|
65
|
+
getTracer() {
|
|
66
|
+
return this.tracer;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Hash a string for deduplication purposes
|
|
71
|
+
*/
|
|
72
|
+
_hashString(str) {
|
|
73
|
+
let hash = 0;
|
|
74
|
+
if (str.length === 0) return hash;
|
|
75
|
+
for (let i = 0; i < str.length; i++) {
|
|
76
|
+
const char = str.charCodeAt(i);
|
|
77
|
+
hash = ((hash << 5) - hash) + char;
|
|
78
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
79
|
+
}
|
|
80
|
+
return hash.toString();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get the active context for a session, creating spans within the session trace
|
|
85
|
+
*/
|
|
86
|
+
_getSessionContext(sessionId) {
|
|
87
|
+
return this.sessionContexts.get(sessionId) || context.active();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Start a chat session span with custom trace ID based on session ID
|
|
92
|
+
*/
|
|
93
|
+
startChatSession(sessionId, userMessage, provider, model) {
|
|
94
|
+
if (process.env.DEBUG_CHAT === '1') {
|
|
95
|
+
console.log(`[DEBUG] AppTracer: Starting chat session span for ${sessionId}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Create a custom trace ID from the session ID
|
|
99
|
+
const traceId = sessionIdToTraceId(sessionId);
|
|
100
|
+
|
|
101
|
+
// Generate a span ID for the root span
|
|
102
|
+
const spanId = randomUUID().replace(/-/g, '').substring(0, 16);
|
|
103
|
+
|
|
104
|
+
// Create trace context with custom trace ID
|
|
105
|
+
const spanContext = {
|
|
106
|
+
traceId: traceId,
|
|
107
|
+
spanId: spanId,
|
|
108
|
+
traceFlags: TraceFlags.SAMPLED,
|
|
109
|
+
isRemote: false
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Create a new context with our custom trace context
|
|
113
|
+
const activeContext = trace.setSpanContext(context.active(), spanContext);
|
|
114
|
+
|
|
115
|
+
// Start the span within this custom context
|
|
116
|
+
const span = context.with(activeContext, () => {
|
|
117
|
+
return this.tracer.startSpan('messaging.process', {
|
|
118
|
+
kind: SpanKind.SERVER,
|
|
119
|
+
attributes: {
|
|
120
|
+
[OTEL_ATTRS.APP_SESSION_ID]: sessionId,
|
|
121
|
+
[OTEL_ATTRS.APP_MESSAGE_CONTENT]: userMessage.substring(0, 500), // Capture more message content
|
|
122
|
+
[OTEL_ATTRS.APP_MESSAGE_LENGTH]: userMessage.length,
|
|
123
|
+
[OTEL_ATTRS.APP_MESSAGE_HASH]: this._hashString(userMessage), // Add hash for deduplication
|
|
124
|
+
[OTEL_ATTRS.APP_AI_PROVIDER]: provider,
|
|
125
|
+
[OTEL_ATTRS.APP_AI_MODEL]: model,
|
|
126
|
+
'app.session.start_time': Date.now(),
|
|
127
|
+
'app.trace.custom_id': true // Mark that we're using custom trace ID
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (process.env.DEBUG_CHAT === '1') {
|
|
133
|
+
console.log(`[DEBUG] AppTracer: Created chat session span ${span.spanContext().spanId} in trace ${span.spanContext().traceId}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Create session context with the span as the active span
|
|
137
|
+
const sessionContext = trace.setSpan(context.active(), span);
|
|
138
|
+
this.sessionContexts.set(sessionId, sessionContext);
|
|
139
|
+
this.sessionSpans.set(sessionId, span);
|
|
140
|
+
|
|
141
|
+
if (process.env.DEBUG_CHAT === '1') {
|
|
142
|
+
console.log(`[DEBUG] AppTracer: Session context established for ${sessionId}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return span;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Execute a function within the session context to ensure proper trace correlation
|
|
150
|
+
*/
|
|
151
|
+
withSessionContext(sessionId, fn) {
|
|
152
|
+
const sessionContext = this._getSessionContext(sessionId);
|
|
153
|
+
return context.with(sessionContext, fn);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Get the trace ID for a session (derived from session ID)
|
|
158
|
+
*/
|
|
159
|
+
getTraceIdForSession(sessionId) {
|
|
160
|
+
return sessionIdToTraceId(sessionId);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Start processing a user message
|
|
165
|
+
*/
|
|
166
|
+
startUserMessageProcessing(sessionId, messageId, message, imageUrlsFound = 0) {
|
|
167
|
+
if (process.env.DEBUG_CHAT === '1') {
|
|
168
|
+
console.log(`[DEBUG] AppTracer: Starting user message processing span for ${sessionId}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const sessionContext = this._getSessionContext(sessionId);
|
|
172
|
+
|
|
173
|
+
return context.with(sessionContext, () => {
|
|
174
|
+
// Get the parent span (should be the session span) from the context
|
|
175
|
+
const parentSpan = trace.getActiveSpan();
|
|
176
|
+
const spanOptions = {
|
|
177
|
+
kind: SpanKind.INTERNAL,
|
|
178
|
+
attributes: {
|
|
179
|
+
[OTEL_ATTRS.APP_SESSION_ID]: sessionId,
|
|
180
|
+
'app.message.id': messageId,
|
|
181
|
+
[OTEL_ATTRS.APP_MESSAGE_TYPE]: 'user',
|
|
182
|
+
[OTEL_ATTRS.APP_MESSAGE_CONTENT]: message.substring(0, 1000), // Include actual message content
|
|
183
|
+
[OTEL_ATTRS.APP_MESSAGE_LENGTH]: message.length,
|
|
184
|
+
[OTEL_ATTRS.APP_MESSAGE_HASH]: this._hashString(message),
|
|
185
|
+
'app.message.image_urls_found': imageUrlsFound,
|
|
186
|
+
'app.processing.start_time': Date.now()
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Explicitly set the parent if available
|
|
191
|
+
if (parentSpan) {
|
|
192
|
+
spanOptions.parent = parentSpan.spanContext();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const span = this.tracer.startSpan('messaging.message.process', spanOptions);
|
|
196
|
+
|
|
197
|
+
if (process.env.DEBUG_CHAT === '1') {
|
|
198
|
+
console.log(`[DEBUG] AppTracer: Created user message processing span ${span.spanContext().spanId} with parent ${parentSpan?.spanContext().spanId}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
this.activeSpans.set(`${sessionId}_user_processing`, span);
|
|
202
|
+
// DO NOT overwrite the session context - this breaks parent-child relationships
|
|
203
|
+
// Instead, create a temporary context for this message processing without storing it
|
|
204
|
+
const messageContext = trace.setSpan(sessionContext, span);
|
|
205
|
+
// Store the message context temporarily for child operations, but keep session context intact
|
|
206
|
+
this.sessionContexts.set(`${sessionId}_message_processing`, messageContext);
|
|
207
|
+
return span;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Execute a function within the context of user message processing span
|
|
213
|
+
*/
|
|
214
|
+
withUserProcessingContext(sessionId, fn) {
|
|
215
|
+
const span = this.activeSpans.get(`${sessionId}_user_processing`);
|
|
216
|
+
if (span) {
|
|
217
|
+
return context.with(trace.setSpan(context.active(), span), fn);
|
|
218
|
+
}
|
|
219
|
+
return fn();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Start the agent loop
|
|
224
|
+
*/
|
|
225
|
+
startAgentLoop(sessionId, maxIterations) {
|
|
226
|
+
const sessionContext = this._getSessionContext(sessionId);
|
|
227
|
+
|
|
228
|
+
return context.with(sessionContext, () => {
|
|
229
|
+
// Get the parent span from the context
|
|
230
|
+
const parentSpan = trace.getActiveSpan();
|
|
231
|
+
const spanOptions = {
|
|
232
|
+
kind: SpanKind.INTERNAL,
|
|
233
|
+
attributes: {
|
|
234
|
+
'app.session.id': sessionId,
|
|
235
|
+
'app.loop.max_iterations': maxIterations,
|
|
236
|
+
'app.loop.start_time': Date.now()
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// Explicitly set the parent if available
|
|
241
|
+
if (parentSpan) {
|
|
242
|
+
spanOptions.parent = parentSpan.spanContext();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const span = this.tracer.startSpan('agent.loop.start', spanOptions);
|
|
246
|
+
|
|
247
|
+
this.activeSpans.set(`${sessionId}_agent_loop`, span);
|
|
248
|
+
// DO NOT overwrite the session context - store agent loop context separately
|
|
249
|
+
const agentLoopContext = trace.setSpan(sessionContext, span);
|
|
250
|
+
this.sessionContexts.set(`${sessionId}_agent_loop`, agentLoopContext);
|
|
251
|
+
return span;
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Execute a function within the context of agent loop span
|
|
257
|
+
*/
|
|
258
|
+
withAgentLoopContext(sessionId, fn) {
|
|
259
|
+
const span = this.activeSpans.get(`${sessionId}_agent_loop`);
|
|
260
|
+
if (span) {
|
|
261
|
+
return context.with(trace.setSpan(context.active(), span), fn);
|
|
262
|
+
}
|
|
263
|
+
return fn();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Start a single iteration of the agent loop
|
|
268
|
+
*/
|
|
269
|
+
startAgentIteration(sessionId, iterationNumber, messagesCount, contextTokens) {
|
|
270
|
+
const sessionContext = this._getSessionContext(sessionId);
|
|
271
|
+
|
|
272
|
+
return context.with(sessionContext, () => {
|
|
273
|
+
const span = this.tracer.startSpan('agent.loop.iteration', {
|
|
274
|
+
kind: SpanKind.INTERNAL,
|
|
275
|
+
attributes: {
|
|
276
|
+
'app.session.id': sessionId,
|
|
277
|
+
'app.iteration.number': iterationNumber,
|
|
278
|
+
'app.iteration.messages_count': messagesCount,
|
|
279
|
+
'app.iteration.context_tokens': contextTokens,
|
|
280
|
+
'app.iteration.start_time': Date.now()
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
this.activeSpans.set(`${sessionId}_iteration_${iterationNumber}`, span);
|
|
285
|
+
// DO NOT overwrite the session context - store iteration context separately
|
|
286
|
+
const iterationContext = trace.setSpan(sessionContext, span);
|
|
287
|
+
this.sessionContexts.set(`${sessionId}_iteration_${iterationNumber}`, iterationContext);
|
|
288
|
+
return span;
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Execute a function within the context of agent iteration span
|
|
294
|
+
*/
|
|
295
|
+
withIterationContext(sessionId, iterationNumber, fn) {
|
|
296
|
+
const span = this.activeSpans.get(`${sessionId}_iteration_${iterationNumber}`);
|
|
297
|
+
if (span) {
|
|
298
|
+
return context.with(trace.setSpan(context.active(), span), fn);
|
|
299
|
+
}
|
|
300
|
+
return fn();
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Start an AI generation request
|
|
305
|
+
*/
|
|
306
|
+
startAiGenerationRequest(sessionId, iterationNumber, model, provider, settings = {}, messagesContext = []) {
|
|
307
|
+
if (process.env.DEBUG_CHAT === '1') {
|
|
308
|
+
console.log(`[DEBUG] AppTracer: Starting AI generation request span for session ${sessionId}, iteration ${iterationNumber}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Get the most appropriate context - prefer iteration context over session context
|
|
312
|
+
const iterationContext = this.sessionContexts.get(`${sessionId}_iteration_${iterationNumber}`);
|
|
313
|
+
const sessionContext = iterationContext || this._getSessionContext(sessionId);
|
|
314
|
+
|
|
315
|
+
return context.with(sessionContext, () => {
|
|
316
|
+
const span = this.tracer.startSpan('ai.generation.request', {
|
|
317
|
+
kind: SpanKind.CLIENT,
|
|
318
|
+
attributes: {
|
|
319
|
+
[OTEL_ATTRS.APP_SESSION_ID]: sessionId,
|
|
320
|
+
[OTEL_ATTRS.APP_ITERATION_NUMBER]: iterationNumber,
|
|
321
|
+
[OTEL_ATTRS.APP_AI_MODEL]: model,
|
|
322
|
+
[OTEL_ATTRS.APP_AI_PROVIDER]: provider,
|
|
323
|
+
[OTEL_ATTRS.APP_AI_TEMPERATURE]: settings.temperature || 0,
|
|
324
|
+
[OTEL_ATTRS.APP_AI_MAX_TOKENS]: settings.maxTokens || 0,
|
|
325
|
+
'app.ai.max_retries': settings.maxRetries || 0,
|
|
326
|
+
'app.ai.messages_count': messagesContext.length,
|
|
327
|
+
'app.ai.request_start_time': Date.now()
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
if (process.env.DEBUG_CHAT === '1') {
|
|
332
|
+
console.log(`[DEBUG] AppTracer: Created AI generation span ${span.spanContext().spanId}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
this.activeSpans.set(`${sessionId}_ai_request_${iterationNumber}`, span);
|
|
336
|
+
// Store AI request context separately, don't overwrite session context
|
|
337
|
+
const aiRequestContext = trace.setSpan(sessionContext, span);
|
|
338
|
+
this.sessionContexts.set(`${sessionId}_ai_request_${iterationNumber}`, aiRequestContext);
|
|
339
|
+
return span;
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Record AI response received
|
|
345
|
+
*/
|
|
346
|
+
recordAiResponse(sessionId, iterationNumber, responseData) {
|
|
347
|
+
const sessionContext = this._getSessionContext(sessionId);
|
|
348
|
+
|
|
349
|
+
return context.with(sessionContext, () => {
|
|
350
|
+
const span = this.tracer.startSpan('ai.generation.response', {
|
|
351
|
+
kind: SpanKind.INTERNAL,
|
|
352
|
+
attributes: {
|
|
353
|
+
[OTEL_ATTRS.APP_SESSION_ID]: sessionId,
|
|
354
|
+
[OTEL_ATTRS.APP_ITERATION_NUMBER]: iterationNumber,
|
|
355
|
+
[OTEL_ATTRS.APP_AI_RESPONSE_CONTENT]: responseData.response ? responseData.response.substring(0, 2000) : '', // Include actual response content
|
|
356
|
+
[OTEL_ATTRS.APP_AI_RESPONSE_LENGTH]: responseData.responseLength || (responseData.response ? responseData.response.length : 0),
|
|
357
|
+
[OTEL_ATTRS.APP_AI_RESPONSE_HASH]: responseData.response ? this._hashString(responseData.response) : '',
|
|
358
|
+
[OTEL_ATTRS.APP_AI_COMPLETION_TOKENS]: responseData.completionTokens || 0,
|
|
359
|
+
[OTEL_ATTRS.APP_AI_PROMPT_TOKENS]: responseData.promptTokens || 0,
|
|
360
|
+
[OTEL_ATTRS.APP_AI_FINISH_REASON]: responseData.finishReason || 'unknown',
|
|
361
|
+
'app.ai.response.time_to_first_chunk_ms': responseData.timeToFirstChunk || 0,
|
|
362
|
+
'app.ai.response.time_to_finish_ms': responseData.timeToFinish || 0,
|
|
363
|
+
'app.ai.response.received_time': Date.now()
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// End the span immediately since this is just recording the response
|
|
368
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
369
|
+
span.end();
|
|
370
|
+
return span;
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Record a parsed tool call
|
|
376
|
+
*/
|
|
377
|
+
recordToolCallParsed(sessionId, iterationNumber, toolName, toolParams) {
|
|
378
|
+
const aiRequestSpan = this.activeSpans.get(`${sessionId}_ai_request_${iterationNumber}`);
|
|
379
|
+
const spanOptions = {
|
|
380
|
+
kind: SpanKind.INTERNAL,
|
|
381
|
+
attributes: {
|
|
382
|
+
'app.session.id': sessionId,
|
|
383
|
+
'app.tool.name': toolName,
|
|
384
|
+
'app.tool.params': JSON.stringify(toolParams).substring(0, 500), // Truncate large params
|
|
385
|
+
'app.tool.parsed_time': Date.now()
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
if (aiRequestSpan) {
|
|
390
|
+
spanOptions.parent = aiRequestSpan.spanContext();
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const span = this.tracer.startSpan('tool.call.parse', spanOptions);
|
|
394
|
+
|
|
395
|
+
// End immediately since this is just recording the parsing
|
|
396
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
397
|
+
span.end();
|
|
398
|
+
|
|
399
|
+
return span;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Start tool execution
|
|
404
|
+
*/
|
|
405
|
+
startToolExecution(sessionId, iterationNumber, toolName, toolParams) {
|
|
406
|
+
// Get the most appropriate context - prefer AI request context over session context
|
|
407
|
+
const aiRequestContext = this.sessionContexts.get(`${sessionId}_ai_request_${iterationNumber}`);
|
|
408
|
+
const sessionContext = aiRequestContext || this._getSessionContext(sessionId);
|
|
409
|
+
|
|
410
|
+
return context.with(sessionContext, () => {
|
|
411
|
+
const span = this.tracer.startSpan('tool.call', {
|
|
412
|
+
kind: SpanKind.INTERNAL,
|
|
413
|
+
attributes: {
|
|
414
|
+
[OTEL_ATTRS.APP_SESSION_ID]: sessionId,
|
|
415
|
+
[OTEL_ATTRS.APP_ITERATION_NUMBER]: iterationNumber,
|
|
416
|
+
[OTEL_ATTRS.APP_TOOL_NAME]: toolName,
|
|
417
|
+
[OTEL_ATTRS.APP_TOOL_PARAMS]: JSON.stringify(toolParams).substring(0, 1000), // Include actual tool parameters
|
|
418
|
+
'app.tool.params.hash': this._hashString(JSON.stringify(toolParams)),
|
|
419
|
+
'app.tool.execution_start_time': Date.now(),
|
|
420
|
+
// Add specific attributes based on tool type
|
|
421
|
+
...(toolName === 'search' && toolParams.query ? { 'app.tool.search.query': toolParams.query } : {}),
|
|
422
|
+
...(toolName === 'extract' && toolParams.file_path ? { 'app.tool.extract.file_path': toolParams.file_path } : {}),
|
|
423
|
+
...(toolName === 'query' && toolParams.pattern ? { 'app.tool.query.pattern': toolParams.pattern } : {}),
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
this.activeSpans.set(`${sessionId}_tool_execution_${iterationNumber}`, span);
|
|
428
|
+
// Store tool execution context separately, don't overwrite session context
|
|
429
|
+
const toolExecutionContext = trace.setSpan(sessionContext, span);
|
|
430
|
+
this.sessionContexts.set(`${sessionId}_tool_execution_${iterationNumber}`, toolExecutionContext);
|
|
431
|
+
return span;
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* End tool execution with results
|
|
437
|
+
*/
|
|
438
|
+
endToolExecution(sessionId, iterationNumber, success, resultLength = 0, errorMessage = null, result = null) {
|
|
439
|
+
const span = this.activeSpans.get(`${sessionId}_tool_execution_${iterationNumber}`);
|
|
440
|
+
if (!span) return;
|
|
441
|
+
|
|
442
|
+
const attributes = {
|
|
443
|
+
[OTEL_ATTRS.APP_TOOL_SUCCESS]: success,
|
|
444
|
+
'app.tool.result_length': resultLength,
|
|
445
|
+
'app.tool.execution_end_time': Date.now(),
|
|
446
|
+
...(errorMessage ? { [OTEL_ATTRS.ERROR_MESSAGE]: errorMessage } : {}),
|
|
447
|
+
...(result ? {
|
|
448
|
+
[OTEL_ATTRS.APP_TOOL_RESULT]: typeof result === 'string' ? result.substring(0, 2000) : JSON.stringify(result).substring(0, 2000),
|
|
449
|
+
'app.tool.result.hash': this._hashString(typeof result === 'string' ? result : JSON.stringify(result))
|
|
450
|
+
} : {})
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
span.setAttributes(attributes);
|
|
454
|
+
|
|
455
|
+
span.setStatus({
|
|
456
|
+
code: success ? SpanStatusCode.OK : SpanStatusCode.ERROR,
|
|
457
|
+
message: errorMessage
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
span.end();
|
|
461
|
+
this.activeSpans.delete(`${sessionId}_tool_execution_${iterationNumber}`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* End an iteration
|
|
466
|
+
*/
|
|
467
|
+
endIteration(sessionId, iterationNumber, success = true, completedAction = null) {
|
|
468
|
+
const span = this.activeSpans.get(`${sessionId}_iteration_${iterationNumber}`);
|
|
469
|
+
if (!span) return;
|
|
470
|
+
|
|
471
|
+
span.setAttributes({
|
|
472
|
+
'app.iteration.success': success,
|
|
473
|
+
'app.iteration.end_time': Date.now(),
|
|
474
|
+
...(completedAction ? { 'app.iteration.completed_action': completedAction } : {})
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
span.setStatus({ code: success ? SpanStatusCode.OK : SpanStatusCode.ERROR });
|
|
478
|
+
span.end();
|
|
479
|
+
this.activeSpans.delete(`${sessionId}_iteration_${iterationNumber}`);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* End the agent loop
|
|
484
|
+
*/
|
|
485
|
+
endAgentLoop(sessionId, totalIterations, success = true, completionReason = null) {
|
|
486
|
+
const span = this.activeSpans.get(`${sessionId}_agent_loop`);
|
|
487
|
+
if (!span) return;
|
|
488
|
+
|
|
489
|
+
span.setAttributes({
|
|
490
|
+
'app.loop.total_iterations': totalIterations,
|
|
491
|
+
'app.loop.success': success,
|
|
492
|
+
'app.loop.end_time': Date.now(),
|
|
493
|
+
...(completionReason ? { 'app.loop.completion_reason': completionReason } : {})
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
span.setStatus({ code: success ? SpanStatusCode.OK : SpanStatusCode.ERROR });
|
|
497
|
+
span.end();
|
|
498
|
+
this.activeSpans.delete(`${sessionId}_agent_loop`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* End user message processing
|
|
503
|
+
*/
|
|
504
|
+
endUserMessageProcessing(sessionId, success = true) {
|
|
505
|
+
const span = this.activeSpans.get(`${sessionId}_user_processing`);
|
|
506
|
+
if (!span) {
|
|
507
|
+
if (process.env.DEBUG_CHAT === '1') {
|
|
508
|
+
console.log(`[DEBUG] AppTracer: No user message processing span found for ${sessionId}`);
|
|
509
|
+
}
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (process.env.DEBUG_CHAT === '1') {
|
|
514
|
+
console.log(`[DEBUG] AppTracer: Ending user message processing span ${span.spanContext().spanId} for ${sessionId}`);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
span.setAttributes({
|
|
518
|
+
'app.processing.success': success,
|
|
519
|
+
'app.processing.end_time': Date.now()
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
span.setStatus({ code: success ? SpanStatusCode.OK : SpanStatusCode.ERROR });
|
|
523
|
+
span.end();
|
|
524
|
+
|
|
525
|
+
this.activeSpans.delete(`${sessionId}_user_processing`);
|
|
526
|
+
// Clean up the message processing context
|
|
527
|
+
this.sessionContexts.delete(`${sessionId}_message_processing`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* End the chat session
|
|
532
|
+
*/
|
|
533
|
+
endChatSession(sessionId, success = true, totalTokensUsed = 0) {
|
|
534
|
+
const span = this.sessionSpans.get(sessionId);
|
|
535
|
+
if (!span) {
|
|
536
|
+
if (process.env.DEBUG_CHAT === '1') {
|
|
537
|
+
console.log(`[DEBUG] AppTracer: No chat session span found for ${sessionId}`);
|
|
538
|
+
}
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (process.env.DEBUG_CHAT === '1') {
|
|
543
|
+
console.log(`[DEBUG] AppTracer: Ending chat session span ${span.spanContext().spanId} for ${sessionId}`);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
span.setAttributes({
|
|
547
|
+
'app.session.success': success,
|
|
548
|
+
'app.session.total_tokens_used': totalTokensUsed,
|
|
549
|
+
'app.session.end_time': Date.now()
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
span.setStatus({ code: success ? SpanStatusCode.OK : SpanStatusCode.ERROR });
|
|
553
|
+
span.end();
|
|
554
|
+
|
|
555
|
+
this.sessionSpans.delete(sessionId);
|
|
556
|
+
// Clean up the session context after ending the span
|
|
557
|
+
this.sessionContexts.delete(sessionId);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* End AI request span
|
|
562
|
+
*/
|
|
563
|
+
endAiRequest(sessionId, iterationNumber, success = true) {
|
|
564
|
+
const span = this.activeSpans.get(`${sessionId}_ai_request_${iterationNumber}`);
|
|
565
|
+
if (!span) {
|
|
566
|
+
if (process.env.DEBUG_CHAT === '1') {
|
|
567
|
+
console.log(`[DEBUG] AppTracer: No AI request span found for ${sessionId}_ai_request_${iterationNumber}`);
|
|
568
|
+
}
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (process.env.DEBUG_CHAT === '1') {
|
|
573
|
+
console.log(`[DEBUG] AppTracer: Ending AI request span ${span.spanContext().spanId} for ${sessionId}, iteration ${iterationNumber}`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
span.setAttributes({
|
|
577
|
+
'app.ai.request_success': success,
|
|
578
|
+
'app.ai.request_end_time': Date.now()
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
span.setStatus({ code: success ? SpanStatusCode.OK : SpanStatusCode.ERROR });
|
|
582
|
+
span.end();
|
|
583
|
+
this.activeSpans.delete(`${sessionId}_ai_request_${iterationNumber}`);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Record a completion attempt
|
|
588
|
+
*/
|
|
589
|
+
recordCompletionAttempt(sessionId, success = true, finalResult = null) {
|
|
590
|
+
const sessionSpan = this.sessionSpans.get(sessionId);
|
|
591
|
+
const spanOptions = {
|
|
592
|
+
kind: SpanKind.INTERNAL,
|
|
593
|
+
attributes: {
|
|
594
|
+
'app.session.id': sessionId,
|
|
595
|
+
'app.completion.success': success,
|
|
596
|
+
'app.completion.result_length': finalResult ? finalResult.length : 0,
|
|
597
|
+
'app.completion.attempt_time': Date.now()
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
if (sessionSpan) {
|
|
602
|
+
spanOptions.parent = sessionSpan.spanContext();
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const span = this.tracer.startSpan('agent.completion.attempt', spanOptions);
|
|
606
|
+
|
|
607
|
+
span.setStatus({ code: success ? SpanStatusCode.OK : SpanStatusCode.ERROR });
|
|
608
|
+
span.end();
|
|
609
|
+
|
|
610
|
+
return span;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Start image URL processing
|
|
615
|
+
*/
|
|
616
|
+
startImageProcessing(sessionId, messageId, imageUrls = [], cleanedMessageLength = 0) {
|
|
617
|
+
const userProcessingSpan = this.activeSpans.get(`${sessionId}_user_processing`);
|
|
618
|
+
const spanOptions = {
|
|
619
|
+
kind: SpanKind.INTERNAL,
|
|
620
|
+
attributes: {
|
|
621
|
+
'app.session.id': sessionId,
|
|
622
|
+
'app.message.id': messageId,
|
|
623
|
+
'app.image.urls_found': imageUrls.length,
|
|
624
|
+
'app.image.message_cleaned_length': cleanedMessageLength,
|
|
625
|
+
'app.image.processing_start_time': Date.now(),
|
|
626
|
+
'app.image.urls_list': JSON.stringify(imageUrls).substring(0, 500)
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
if (userProcessingSpan) {
|
|
631
|
+
spanOptions.parent = userProcessingSpan.spanContext();
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const span = this.tracer.startSpan('content.image.processing', spanOptions);
|
|
635
|
+
this.activeSpans.set(`${sessionId}_image_processing`, span);
|
|
636
|
+
return span;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Record image URL validation results
|
|
641
|
+
*/
|
|
642
|
+
recordImageValidation(sessionId, validationResults) {
|
|
643
|
+
const imageProcessingSpan = this.activeSpans.get(`${sessionId}_image_processing`);
|
|
644
|
+
const spanOptions = {
|
|
645
|
+
kind: SpanKind.INTERNAL,
|
|
646
|
+
attributes: {
|
|
647
|
+
'app.session.id': sessionId,
|
|
648
|
+
'app.image.validation.total_urls': validationResults.totalUrls || 0,
|
|
649
|
+
'app.image.validation.valid_urls': validationResults.validUrls || 0,
|
|
650
|
+
'app.image.validation.invalid_urls': validationResults.invalidUrls || 0,
|
|
651
|
+
'app.image.validation.redirected_urls': validationResults.redirectedUrls || 0,
|
|
652
|
+
'app.image.validation.timeout_urls': validationResults.timeoutUrls || 0,
|
|
653
|
+
'app.image.validation.network_errors': validationResults.networkErrors || 0,
|
|
654
|
+
'app.image.validation.duration_ms': validationResults.durationMs || 0,
|
|
655
|
+
'app.image.validation_time': Date.now()
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
if (imageProcessingSpan) {
|
|
660
|
+
spanOptions.parent = imageProcessingSpan.spanContext();
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const span = this.tracer.startSpan('content.image.validation', spanOptions);
|
|
664
|
+
span.setStatus({
|
|
665
|
+
code: validationResults.validUrls > 0 ? SpanStatusCode.OK : SpanStatusCode.ERROR,
|
|
666
|
+
message: `${validationResults.validUrls}/${validationResults.totalUrls} URLs validated successfully`
|
|
667
|
+
});
|
|
668
|
+
span.end();
|
|
669
|
+
return span;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* End image processing
|
|
674
|
+
*/
|
|
675
|
+
endImageProcessing(sessionId, success = true, finalValidUrls = 0) {
|
|
676
|
+
const span = this.activeSpans.get(`${sessionId}_image_processing`);
|
|
677
|
+
if (!span) return;
|
|
678
|
+
|
|
679
|
+
span.setAttributes({
|
|
680
|
+
'app.image.processing_success': success,
|
|
681
|
+
'app.image.final_valid_urls': finalValidUrls,
|
|
682
|
+
'app.image.processing_end_time': Date.now()
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
span.setStatus({ code: success ? SpanStatusCode.OK : SpanStatusCode.ERROR });
|
|
686
|
+
span.end();
|
|
687
|
+
this.activeSpans.delete(`${sessionId}_image_processing`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Record AI model errors
|
|
692
|
+
*/
|
|
693
|
+
recordAiModelError(sessionId, iterationNumber, errorDetails) {
|
|
694
|
+
const aiRequestSpan = this.activeSpans.get(`${sessionId}_ai_request_${iterationNumber}`);
|
|
695
|
+
const spanOptions = {
|
|
696
|
+
kind: SpanKind.INTERNAL,
|
|
697
|
+
attributes: {
|
|
698
|
+
'app.session.id': sessionId,
|
|
699
|
+
'app.error.type': 'ai_model_error',
|
|
700
|
+
'app.error.category': errorDetails.category || 'unknown', // timeout, api_limit, network, etc.
|
|
701
|
+
'app.error.message': errorDetails.message?.substring(0, 500) || '',
|
|
702
|
+
'app.error.model': errorDetails.model || '',
|
|
703
|
+
'app.error.provider': errorDetails.provider || '',
|
|
704
|
+
'app.error.status_code': errorDetails.statusCode || 0,
|
|
705
|
+
'app.error.retry_attempt': errorDetails.retryAttempt || 0,
|
|
706
|
+
'app.error.timestamp': Date.now()
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
if (aiRequestSpan) {
|
|
711
|
+
spanOptions.parent = aiRequestSpan.spanContext();
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const span = this.tracer.startSpan('ai.generation.error', spanOptions);
|
|
715
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: errorDetails.message });
|
|
716
|
+
span.end();
|
|
717
|
+
return span;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Record tool execution errors
|
|
722
|
+
*/
|
|
723
|
+
recordToolError(sessionId, iterationNumber, toolName, errorDetails) {
|
|
724
|
+
const toolExecutionSpan = this.activeSpans.get(`${sessionId}_tool_execution_${iterationNumber}`);
|
|
725
|
+
const spanOptions = {
|
|
726
|
+
kind: SpanKind.INTERNAL,
|
|
727
|
+
attributes: {
|
|
728
|
+
'app.session.id': sessionId,
|
|
729
|
+
'app.error.type': 'tool_execution_error',
|
|
730
|
+
'app.error.tool_name': toolName,
|
|
731
|
+
'app.error.category': errorDetails.category || 'unknown', // validation, execution, network, filesystem
|
|
732
|
+
'app.error.message': errorDetails.message?.substring(0, 500) || '',
|
|
733
|
+
'app.error.exit_code': errorDetails.exitCode || 0,
|
|
734
|
+
'app.error.signal': errorDetails.signal || '',
|
|
735
|
+
'app.error.params': JSON.stringify(errorDetails.params || {}).substring(0, 300),
|
|
736
|
+
'app.error.timestamp': Date.now()
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
if (toolExecutionSpan) {
|
|
741
|
+
spanOptions.parent = toolExecutionSpan.spanContext();
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const span = this.tracer.startSpan('tool.call.error', spanOptions);
|
|
745
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: errorDetails.message });
|
|
746
|
+
span.end();
|
|
747
|
+
return span;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Record session cancellation
|
|
752
|
+
*/
|
|
753
|
+
recordSessionCancellation(sessionId, reason = 'user_request', context = {}) {
|
|
754
|
+
const sessionSpan = this.sessionSpans.get(sessionId);
|
|
755
|
+
const spanOptions = {
|
|
756
|
+
kind: SpanKind.INTERNAL,
|
|
757
|
+
attributes: {
|
|
758
|
+
'app.session.id': sessionId,
|
|
759
|
+
'app.cancellation.reason': reason, // user_request, timeout, error, signal
|
|
760
|
+
'app.cancellation.context': JSON.stringify(context).substring(0, 300),
|
|
761
|
+
'app.cancellation.current_iteration': context.currentIteration || 0,
|
|
762
|
+
'app.cancellation.active_tool': context.activeTool || '',
|
|
763
|
+
'app.cancellation.timestamp': Date.now()
|
|
764
|
+
}
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
if (sessionSpan) {
|
|
768
|
+
spanOptions.parent = sessionSpan.spanContext();
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const span = this.tracer.startSpan('messaging.session.cancel', spanOptions);
|
|
772
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: `Session cancelled: ${reason}` });
|
|
773
|
+
span.end();
|
|
774
|
+
return span;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/**
|
|
778
|
+
* Record token management metrics
|
|
779
|
+
*/
|
|
780
|
+
recordTokenMetrics(sessionId, tokenData) {
|
|
781
|
+
const sessionSpan = this.sessionSpans.get(sessionId);
|
|
782
|
+
const spanOptions = {
|
|
783
|
+
kind: SpanKind.INTERNAL,
|
|
784
|
+
attributes: {
|
|
785
|
+
'app.session.id': sessionId,
|
|
786
|
+
'app.tokens.context_window': tokenData.contextWindow || 0,
|
|
787
|
+
'app.tokens.current_total': tokenData.currentTotal || 0,
|
|
788
|
+
'app.tokens.request_tokens': tokenData.requestTokens || 0,
|
|
789
|
+
'app.tokens.response_tokens': tokenData.responseTokens || 0,
|
|
790
|
+
'app.tokens.cache_read': tokenData.cacheRead || 0,
|
|
791
|
+
'app.tokens.cache_write': tokenData.cacheWrite || 0,
|
|
792
|
+
'app.tokens.utilization_percent': tokenData.contextWindow ?
|
|
793
|
+
Math.round((tokenData.currentTotal / tokenData.contextWindow) * 100) : 0,
|
|
794
|
+
'app.tokens.measurement_time': Date.now()
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
if (sessionSpan) {
|
|
799
|
+
spanOptions.parent = sessionSpan.spanContext();
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const span = this.tracer.startSpan('ai.token.metrics', spanOptions);
|
|
803
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
804
|
+
span.end();
|
|
805
|
+
return span;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Record history management operations
|
|
810
|
+
*/
|
|
811
|
+
recordHistoryOperation(sessionId, operation, details = {}) {
|
|
812
|
+
const sessionSpan = this.sessionSpans.get(sessionId);
|
|
813
|
+
const spanOptions = {
|
|
814
|
+
kind: SpanKind.INTERNAL,
|
|
815
|
+
attributes: {
|
|
816
|
+
'app.session.id': sessionId,
|
|
817
|
+
'app.history.operation': operation, // trim, update, clear, save
|
|
818
|
+
'app.history.messages_before': details.messagesBefore || 0,
|
|
819
|
+
'app.history.messages_after': details.messagesAfter || 0,
|
|
820
|
+
'app.history.messages_removed': details.messagesRemoved || 0,
|
|
821
|
+
'app.history.reason': details.reason || '', // max_length, memory_limit, session_reset
|
|
822
|
+
'app.history.operation_time': Date.now()
|
|
823
|
+
}
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
if (sessionSpan) {
|
|
827
|
+
spanOptions.parent = sessionSpan.spanContext();
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const span = this.tracer.startSpan('messaging.history.manage', spanOptions);
|
|
831
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
832
|
+
span.end();
|
|
833
|
+
return span;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Record system prompt generation metrics
|
|
838
|
+
*/
|
|
839
|
+
recordSystemPromptGeneration(sessionId, promptData) {
|
|
840
|
+
const sessionSpan = this.sessionSpans.get(sessionId);
|
|
841
|
+
const spanOptions = {
|
|
842
|
+
kind: SpanKind.INTERNAL,
|
|
843
|
+
attributes: {
|
|
844
|
+
'app.session.id': sessionId,
|
|
845
|
+
'app.prompt.base_length': promptData.baseLength || 0,
|
|
846
|
+
'app.prompt.final_length': promptData.finalLength || 0,
|
|
847
|
+
'app.prompt.files_added': promptData.filesAdded || 0,
|
|
848
|
+
'app.prompt.generation_duration_ms': promptData.generationDurationMs || 0,
|
|
849
|
+
'app.prompt.type': promptData.promptType || 'default',
|
|
850
|
+
'app.prompt.estimated_tokens': promptData.estimatedTokens || 0,
|
|
851
|
+
'app.prompt.generation_time': Date.now()
|
|
852
|
+
}
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
if (sessionSpan) {
|
|
856
|
+
spanOptions.parent = sessionSpan.spanContext();
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const span = this.tracer.startSpan('ai.prompt.generate', spanOptions);
|
|
860
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
861
|
+
span.end();
|
|
862
|
+
return span;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/**
|
|
866
|
+
* Record file system operations
|
|
867
|
+
*/
|
|
868
|
+
recordFileSystemOperation(sessionId, operation, details = {}) {
|
|
869
|
+
const activeSpan = this.activeSpans.get(`${sessionId}_tool_execution_${details.iterationNumber}`) ||
|
|
870
|
+
this.sessionSpans.get(sessionId);
|
|
871
|
+
const spanOptions = {
|
|
872
|
+
kind: SpanKind.INTERNAL,
|
|
873
|
+
attributes: {
|
|
874
|
+
'app.session.id': sessionId,
|
|
875
|
+
'app.fs.operation': operation, // read, write, create_temp, delete, mkdir
|
|
876
|
+
'app.fs.path': details.path?.substring(0, 200) || '',
|
|
877
|
+
'app.fs.size_bytes': details.sizeBytes || 0,
|
|
878
|
+
'app.fs.duration_ms': details.durationMs || 0,
|
|
879
|
+
'app.fs.success': details.success !== false,
|
|
880
|
+
'app.fs.error_code': details.errorCode || '',
|
|
881
|
+
'app.fs.operation_time': Date.now()
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
if (activeSpan) {
|
|
886
|
+
spanOptions.parent = activeSpan.spanContext();
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const span = this.tracer.startSpan('fs.operation', spanOptions);
|
|
890
|
+
span.setStatus({
|
|
891
|
+
code: details.success !== false ? SpanStatusCode.OK : SpanStatusCode.ERROR,
|
|
892
|
+
message: details.errorMessage
|
|
893
|
+
});
|
|
894
|
+
span.end();
|
|
895
|
+
return span;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
/**
|
|
899
|
+
* Clean up any remaining active spans for a session
|
|
900
|
+
*/
|
|
901
|
+
cleanup(sessionId) {
|
|
902
|
+
if (process.env.DEBUG_CHAT === '1') {
|
|
903
|
+
console.log(`[DEBUG] AppTracer: Cleaning up session ${sessionId}`);
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// End any remaining active spans
|
|
907
|
+
const keysToDelete = [];
|
|
908
|
+
for (const [key, span] of this.activeSpans.entries()) {
|
|
909
|
+
if (key.includes(sessionId)) {
|
|
910
|
+
if (process.env.DEBUG_CHAT === '1') {
|
|
911
|
+
console.log(`[DEBUG] AppTracer: Cleaning up active span ${key}`);
|
|
912
|
+
}
|
|
913
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: 'Session cleanup' });
|
|
914
|
+
span.end();
|
|
915
|
+
keysToDelete.push(key);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
keysToDelete.forEach(key => this.activeSpans.delete(key));
|
|
919
|
+
|
|
920
|
+
// Only clean up session span if it still exists (wasn't properly ended by endChatSession)
|
|
921
|
+
const sessionSpan = this.sessionSpans.get(sessionId);
|
|
922
|
+
if (sessionSpan) {
|
|
923
|
+
if (process.env.DEBUG_CHAT === '1') {
|
|
924
|
+
console.log(`[DEBUG] AppTracer: Cleaning up orphaned session span for ${sessionId}`);
|
|
925
|
+
}
|
|
926
|
+
sessionSpan.setStatus({ code: SpanStatusCode.ERROR, message: 'Session cleanup - orphaned span' });
|
|
927
|
+
sessionSpan.end();
|
|
928
|
+
this.sessionSpans.delete(sessionId);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Clean up all session-related contexts (session, message processing, iterations, AI requests, tool executions)
|
|
932
|
+
const contextKeysToDelete = [];
|
|
933
|
+
for (const [key] of this.sessionContexts.entries()) {
|
|
934
|
+
if (key.includes(sessionId)) {
|
|
935
|
+
contextKeysToDelete.push(key);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
contextKeysToDelete.forEach(key => this.sessionContexts.delete(key));
|
|
939
|
+
|
|
940
|
+
if (process.env.DEBUG_CHAT === '1') {
|
|
941
|
+
console.log(`[DEBUG] AppTracer: Session cleanup completed for ${sessionId}, cleaned ${contextKeysToDelete.length} contexts`);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Export a singleton instance
|
|
947
|
+
export const appTracer = new AppTracer();
|