Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/claude/node/claudeOTelTracker.ts
13405 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import type { SDKMessage } from '@anthropic-ai/claude-agent-sdk';
7
import { CopilotChatAttr, emitSessionStartEvent, GenAiAttr, GenAiMetrics, GenAiOperationName, GenAiProviderName, IOTelService, type ISpanHandle, SpanKind, SpanStatusCode, type TraceContext, truncateForOTel } from '../../../../platform/otel/common/index';
8
import { IClaudeSessionStateService } from '../common/claudeSessionStateService';
9
10
/**
11
* Manages OTel span lifecycle for a Claude agent session.
12
*
13
* Extracted from ClaudeCodeSession to keep tracing concerns separate from
14
* session orchestration. Tracks the invoke_agent root span, accumulates
15
* parent-only token usage, and manages trace context for subagent nesting.
16
*/
17
export class ClaudeOTelTracker {
18
private _currentSpan: ISpanHandle | undefined;
19
private _currentTraceContext: TraceContext | undefined;
20
private _startTime: number | undefined;
21
private _isFirstRequest = true;
22
private _turnCount = 0;
23
private _parentInputTokens = 0;
24
private _parentOutputTokens = 0;
25
private _parentCacheReadTokens = 0;
26
private _parentCacheCreationTokens = 0;
27
28
constructor(
29
private readonly _sessionId: string,
30
private readonly _otelService: IOTelService,
31
private readonly _sessionStateService: IClaudeSessionStateService,
32
) { }
33
34
/** The trace context of the current invoke_agent span, used to parent child spans. */
35
get traceContext(): TraceContext | undefined {
36
return this._currentTraceContext;
37
}
38
39
/**
40
* Starts a new invoke_agent span for a user request.
41
* Ends any previous span and resets accumulators.
42
*/
43
startRequest(modelId: string): void {
44
this.endRequest();
45
46
this._currentSpan = this._otelService.startSpan('invoke_agent claude', {
47
kind: SpanKind.INTERNAL,
48
attributes: {
49
[GenAiAttr.OPERATION_NAME]: GenAiOperationName.INVOKE_AGENT,
50
[GenAiAttr.AGENT_NAME]: 'claude',
51
[GenAiAttr.PROVIDER_NAME]: GenAiProviderName.GITHUB,
52
[GenAiAttr.CONVERSATION_ID]: this._sessionId,
53
[CopilotChatAttr.SESSION_ID]: this._sessionId,
54
[CopilotChatAttr.CHAT_SESSION_ID]: this._sessionId,
55
[GenAiAttr.REQUEST_MODEL]: modelId,
56
},
57
});
58
this._currentTraceContext = this._currentSpan.getSpanContext();
59
this._startTime = Date.now();
60
this._turnCount = 0;
61
this._parentInputTokens = 0;
62
this._parentOutputTokens = 0;
63
this._parentCacheReadTokens = 0;
64
this._parentCacheCreationTokens = 0;
65
66
// Store trace context so the language model server can parent chat spans
67
this._sessionStateService.setTraceContextForSession(this._sessionId, this._currentTraceContext);
68
69
// Emit session start event and metric for the first request
70
if (this._isFirstRequest) {
71
this._isFirstRequest = false;
72
GenAiMetrics.incrementSessionCount(this._otelService);
73
emitSessionStartEvent(this._otelService, this._sessionId, modelId, 'claude');
74
}
75
}
76
77
/**
78
* Emits a user_message span event for the debug panel.
79
*/
80
emitUserMessage(promptLabel: string): void {
81
const userMsgSpan = this._otelService.startSpan('user_message', {
82
kind: SpanKind.INTERNAL,
83
attributes: {
84
[GenAiAttr.OPERATION_NAME]: 'user_message',
85
[CopilotChatAttr.CHAT_SESSION_ID]: this._sessionId,
86
},
87
parentTraceContext: this._currentTraceContext,
88
});
89
const userContent = truncateForOTel(promptLabel);
90
userMsgSpan.setAttribute(CopilotChatAttr.USER_REQUEST, userContent);
91
userMsgSpan.addEvent('user_message', { content: userContent, [CopilotChatAttr.CHAT_SESSION_ID]: this._sessionId });
92
userMsgSpan.end();
93
}
94
95
/**
96
* Processes an SDK message for OTel tracking.
97
* Call this for every message in the processing loop.
98
*/
99
onMessage(message: SDKMessage, subagentTraceContexts: Map<string, TraceContext>): void {
100
if (message.type === 'assistant') {
101
this._turnCount++;
102
this._accumulateParentTokenUsage(message);
103
}
104
105
if (message.type === 'result' && this._currentSpan) {
106
this._setResultAttributes(message);
107
}
108
109
this._updateTraceContextForMessage(message, subagentTraceContexts);
110
}
111
112
/**
113
* Ends the current invoke_agent span with OK status and records metrics.
114
*/
115
endRequest(): void {
116
this._endSpan();
117
}
118
119
/**
120
* Ends the current invoke_agent span with ERROR status.
121
*/
122
endRequestWithError(message: string): void {
123
this._endSpan(SpanStatusCode.ERROR, message);
124
}
125
126
// ── Private ──────────────────────────────────────────────────────────────
127
128
private _endSpan(statusCode?: SpanStatusCode, statusMessage?: string): void {
129
if (!this._currentSpan) {
130
return;
131
}
132
const span = this._currentSpan;
133
span.setAttribute(CopilotChatAttr.TURN_COUNT, this._turnCount);
134
135
// Set parent-only token usage (comparable with foreground agent).
136
span.setAttributes({
137
[GenAiAttr.USAGE_INPUT_TOKENS]: this._parentInputTokens,
138
[GenAiAttr.USAGE_OUTPUT_TOKENS]: this._parentOutputTokens,
139
...(this._parentCacheReadTokens ? { [GenAiAttr.USAGE_CACHE_READ_INPUT_TOKENS]: this._parentCacheReadTokens } : {}),
140
...(this._parentCacheCreationTokens ? { [GenAiAttr.USAGE_CACHE_CREATION_INPUT_TOKENS]: this._parentCacheCreationTokens } : {}),
141
});
142
143
if (statusCode !== undefined) {
144
span.setStatus(statusCode, statusMessage);
145
} else {
146
span.setStatus(SpanStatusCode.OK);
147
}
148
span.end();
149
150
// Record agent-level metrics
151
if (this._startTime) {
152
const durationSec = (Date.now() - this._startTime) / 1000;
153
GenAiMetrics.recordAgentDuration(this._otelService, 'claude', durationSec);
154
}
155
GenAiMetrics.recordAgentTurnCount(this._otelService, 'claude', this._turnCount);
156
157
this._currentSpan = undefined;
158
this._currentTraceContext = undefined;
159
this._startTime = undefined;
160
this._sessionStateService.setTraceContextForSession(this._sessionId, undefined);
161
}
162
163
/**
164
* Accumulates parent-only token usage from an assistant message.
165
* Excludes subagent turns so gen_ai.usage.* on the root span is comparable
166
* with the foreground agent.
167
*/
168
private _accumulateParentTokenUsage(message: SDKMessage & { type: 'assistant' }): void {
169
if (message.parent_tool_use_id) {
170
return;
171
}
172
const msgUsage = message.message?.usage;
173
if (msgUsage) {
174
this._parentInputTokens += (msgUsage.input_tokens ?? 0)
175
+ (msgUsage.cache_creation_input_tokens ?? 0)
176
+ (msgUsage.cache_read_input_tokens ?? 0);
177
this._parentOutputTokens += (msgUsage.output_tokens ?? 0);
178
this._parentCacheReadTokens += (msgUsage.cache_read_input_tokens ?? 0);
179
this._parentCacheCreationTokens += (msgUsage.cache_creation_input_tokens ?? 0);
180
}
181
}
182
183
/**
184
* Sets cost, turn count, and response model on the invoke_agent span from a result message.
185
*/
186
private _setResultAttributes(message: SDKMessage & { type: 'result' }): void {
187
if (!this._currentSpan) {
188
return;
189
}
190
if (message.num_turns !== undefined) {
191
this._currentSpan.setAttribute(CopilotChatAttr.TURN_COUNT, message.num_turns);
192
}
193
if (message.total_cost_usd !== undefined) {
194
this._currentSpan.setAttribute(CopilotChatAttr.TOTAL_COST_USD, message.total_cost_usd);
195
}
196
const responseModel = message.modelUsage ? Object.keys(message.modelUsage)[0] : undefined;
197
if (responseModel) {
198
this._currentSpan.setAttribute(GenAiAttr.RESPONSE_MODEL, responseModel);
199
}
200
}
201
202
/**
203
* Updates the session trace context based on whether a message is from a subagent.
204
* Ensures chat spans created by chatMLFetcher are parented under the correct
205
* Agent tool span during subagent execution.
206
*/
207
private _updateTraceContextForMessage(message: SDKMessage, subagentTraceContexts: Map<string, TraceContext>): void {
208
if (!('parent_tool_use_id' in message)) {
209
return;
210
}
211
if (message.parent_tool_use_id) {
212
const subagentCtx = subagentTraceContexts.get(message.parent_tool_use_id);
213
if (subagentCtx) {
214
this._sessionStateService.setTraceContextForSession(this._sessionId, subagentCtx);
215
}
216
} else {
217
this._sessionStateService.setTraceContextForSession(this._sessionId, this._currentTraceContext);
218
}
219
}
220
}
221
222