@@ -49,12 +49,15 @@ tracer = trace.get_tracer(__name__)
4949### 2. Create Spans Around Agent Operations
5050
5151``` python
52- from copilot import CopilotClient, SessionConfig
52+ from copilot import CopilotClient, PermissionHandler
53+ from copilot.generated.session_events import SessionEventType
5354from opentelemetry import trace, context
5455from opentelemetry.trace import SpanKind
5556
56- # Initialize client
57- client = CopilotClient(SessionConfig(model = " gpt-5" ))
57+ # Initialize client and start the CLI server
58+ client = CopilotClient()
59+ await client.start()
60+
5861tracer = trace.get_tracer(__name__ )
5962
6063# Create a span for the agent invocation
@@ -73,18 +76,34 @@ span = tracer.start_span(
7376token = context.attach(trace.set_span_in_context(span))
7477
7578try :
76- # Your agent code here
77- async for event in session.send(" Hello, world!" ):
78- # Process events and add attributes
79- pass
79+ # Create a session (model is set here, not on the client)
80+ session = await client.create_session({
81+ " model" : " gpt-5" ,
82+ " on_permission_request" : PermissionHandler.approve_all,
83+ })
84+
85+ # Subscribe to events via callback
86+ def handle_event (event ):
87+ if event.type == SessionEventType.ASSISTANT_USAGE :
88+ if event.data.model:
89+ span.set_attribute(" gen_ai.response.model" , event.data.model)
90+
91+ unsubscribe = session.on(handle_event)
92+
93+ # Send a message (returns a message ID)
94+ await session.send({" prompt" : " Hello, world!" })
95+
96+ # Or send and wait for the session to become idle
97+ response = await session.send_and_wait({" prompt" : " Hello, world!" })
8098finally :
8199 context.detach(token)
82100 span.end()
101+ await client.stop()
83102```
84103
85104## Copilot SDK Event to GenAI Attribute Mapping
86105
87- The Copilot SDK emits ` SessionEventType ` events during agent execution. Here's how to map these events to GenAI semantic convention attributes:
106+ The Copilot SDK emits ` SessionEventType ` events during agent execution. Subscribe to these events using ` session.on(handler) ` , which returns an unsubscribe function. Here's how to map these events to GenAI semantic convention attributes:
88107
89108### Core Session Events
90109
@@ -131,7 +150,7 @@ When you receive an `ASSISTANT_USAGE` event, extract token usage:
131150``` python
132151from copilot.generated.session_events import SessionEventType
133152
134- async for event in session.send( " Hello " ):
153+ def handle_usage ( event ):
135154 if event.type == SessionEventType.ASSISTANT_USAGE :
136155 data = event.data
137156 if data.model:
@@ -140,9 +159,13 @@ async for event in session.send("Hello"):
140159 span.set_attribute(" gen_ai.usage.input_tokens" , int (data.input_tokens))
141160 if data.output_tokens is not None :
142161 span.set_attribute(" gen_ai.usage.output_tokens" , int (data.output_tokens))
162+
163+ unsubscribe = session.on(handle_usage)
164+ await session.send({" prompt" : " Hello" })
143165```
144166
145167** Event Data Structure:**
168+ <!-- docs-validate: skip -->
146169``` python
147170@dataclass
148171class Usage :
@@ -168,43 +191,43 @@ import json
168191# Dictionary to track active tool spans
169192tool_spans = {}
170193
171- async for event in session.send( " What's the weather? " ):
194+ def handle_tool_events ( event ):
172195 data = event.data
173-
196+
174197 if event.type == SessionEventType.TOOL_EXECUTION_START and data:
175198 call_id = data.tool_call_id or str (uuid.uuid4())
176199 tool_name = data.tool_name or " unknown"
177-
200+
178201 tool_attrs = {
179202 " gen_ai.tool.name" : tool_name,
180203 " gen_ai.operation.name" : " execute_tool" ,
181204 }
182-
205+
183206 if call_id:
184207 tool_attrs[" gen_ai.tool.call.id" ] = call_id
185-
208+
186209 # Optional: include tool arguments (may contain sensitive data)
187210 if data.arguments is not None :
188211 try :
189212 tool_attrs[" gen_ai.tool.call.arguments" ] = json.dumps(data.arguments)
190213 except Exception :
191214 tool_attrs[" gen_ai.tool.call.arguments" ] = str (data.arguments)
192-
215+
193216 tool_span = tracer.start_span(
194217 name = f " execute_tool { tool_name} " ,
195218 kind = SpanKind.CLIENT ,
196219 attributes = tool_attrs
197220 )
198221 tool_token = context.attach(trace.set_span_in_context(tool_span))
199222 tool_spans[call_id] = (tool_span, tool_token)
200-
223+
201224 elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE and data:
202225 call_id = data.tool_call_id
203226 entry = tool_spans.pop(call_id, None ) if call_id else None
204-
227+
205228 if entry:
206229 tool_span, tool_token = entry
207-
230+
208231 # Optional: include tool result (may contain sensitive data)
209232 if data.result is not None :
210233 try :
@@ -213,13 +236,16 @@ async for event in session.send("What's the weather?"):
213236 result_str = str (data.result)
214237 # Truncate to 512 chars to avoid huge spans
215238 tool_span.set_attribute(" gen_ai.tool.call.result" , result_str[:512 ])
216-
239+
217240 # Mark as error if tool failed
218241 if hasattr (data, " success" ) and data.success is False :
219242 tool_span.set_attribute(" error.type" , " tool_error" )
220-
243+
221244 context.detach(tool_token)
222245 tool_span.end()
246+
247+ unsubscribe = session.on(handle_tool_events)
248+ await session.send({" prompt" : " What's the weather?" })
223249```
224250
225251** Tool Event Data:**
@@ -233,7 +259,7 @@ async for event in session.send("What's the weather?"):
233259Capture the final message as a span event:
234260
235261``` python
236- async for event in session.send( " Tell me a joke " ):
262+ def handle_message ( event ):
237263 if event.type == SessionEventType.ASSISTANT_MESSAGE and event.data:
238264 if event.data.content:
239265 # Add as a span event (opt-in for content recording)
@@ -246,6 +272,9 @@ async for event in session.send("Tell me a joke"):
246272 })
247273 }
248274 )
275+
276+ unsubscribe = session.on(handle_message)
277+ await session.send({" prompt" : " Tell me a joke" })
249278```
250279
251280## Complete Example
@@ -254,7 +283,7 @@ async for event in session.send("Tell me a joke"):
254283import asyncio
255284import json
256285import uuid
257- from copilot import CopilotClient, SessionConfig
286+ from copilot import CopilotClient, PermissionHandler
258287from copilot.generated.session_events import SessionEventType
259288from opentelemetry import trace, context
260289from opentelemetry.trace import SpanKind
@@ -269,72 +298,85 @@ tracer = trace.get_tracer(__name__)
269298
270299async def invoke_agent (prompt : str ):
271300 """ Invoke agent with full OpenTelemetry instrumentation."""
272-
301+
273302 # Create main span
274303 span_attrs = {
275304 " gen_ai.operation.name" : " invoke_agent" ,
276305 " gen_ai.provider.name" : " github.copilot" ,
277306 " gen_ai.agent.name" : " example-agent" ,
278307 " gen_ai.request.model" : " gpt-5" ,
279308 }
280-
309+
281310 span = tracer.start_span(
282311 name = " invoke_agent example-agent" ,
283312 kind = SpanKind.CLIENT ,
284313 attributes = span_attrs
285314 )
286315 token = context.attach(trace.set_span_in_context(span))
287316 tool_spans = {}
288-
317+
289318 try :
290- client = CopilotClient(SessionConfig(model = " gpt-5" ))
291- async with client.create_session() as session:
292- async for event in session.send(prompt):
293- data = event.data
294-
295- # Handle usage events
296- if event.type == SessionEventType.ASSISTANT_USAGE and data:
297- if data.model:
298- span.set_attribute(" gen_ai.response.model" , data.model)
299- if data.input_tokens is not None :
300- span.set_attribute(" gen_ai.usage.input_tokens" , int (data.input_tokens))
301- if data.output_tokens is not None :
302- span.set_attribute(" gen_ai.usage.output_tokens" , int (data.output_tokens))
303-
304- # Handle tool execution
305- elif event.type == SessionEventType.TOOL_EXECUTION_START and data:
306- call_id = data.tool_call_id or str (uuid.uuid4())
307- tool_name = data.tool_name or " unknown"
308-
309- tool_attrs = {
310- " gen_ai.tool.name" : tool_name,
311- " gen_ai.operation.name" : " execute_tool" ,
312- " gen_ai.tool.call.id" : call_id,
313- }
314-
315- tool_span = tracer.start_span(
316- name = f " execute_tool { tool_name} " ,
317- kind = SpanKind.CLIENT ,
318- attributes = tool_attrs
319- )
320- tool_token = context.attach(trace.set_span_in_context(tool_span))
321- tool_spans[call_id] = (tool_span, tool_token)
322-
323- elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE and data:
324- call_id = data.tool_call_id
325- entry = tool_spans.pop(call_id, None ) if call_id else None
326- if entry:
327- tool_span, tool_token = entry
328- context.detach(tool_token)
329- tool_span.end()
330-
331- # Capture final message
332- elif event.type == SessionEventType.ASSISTANT_MESSAGE and data:
333- if data.content:
334- print (f " Assistant: { data.content} " )
335-
319+ client = CopilotClient()
320+ await client.start()
321+
322+ session = await client.create_session({
323+ " model" : " gpt-5" ,
324+ " on_permission_request" : PermissionHandler.approve_all,
325+ })
326+
327+ # Subscribe to events via callback
328+ def handle_event (event ):
329+ data = event.data
330+
331+ # Handle usage events
332+ if event.type == SessionEventType.ASSISTANT_USAGE and data:
333+ if data.model:
334+ span.set_attribute(" gen_ai.response.model" , data.model)
335+ if data.input_tokens is not None :
336+ span.set_attribute(" gen_ai.usage.input_tokens" , int (data.input_tokens))
337+ if data.output_tokens is not None :
338+ span.set_attribute(" gen_ai.usage.output_tokens" , int (data.output_tokens))
339+
340+ # Handle tool execution
341+ elif event.type == SessionEventType.TOOL_EXECUTION_START and data:
342+ call_id = data.tool_call_id or str (uuid.uuid4())
343+ tool_name = data.tool_name or " unknown"
344+
345+ tool_attrs = {
346+ " gen_ai.tool.name" : tool_name,
347+ " gen_ai.operation.name" : " execute_tool" ,
348+ " gen_ai.tool.call.id" : call_id,
349+ }
350+
351+ tool_span = tracer.start_span(
352+ name = f " execute_tool { tool_name} " ,
353+ kind = SpanKind.CLIENT ,
354+ attributes = tool_attrs
355+ )
356+ tool_token = context.attach(trace.set_span_in_context(tool_span))
357+ tool_spans[call_id] = (tool_span, tool_token)
358+
359+ elif event.type == SessionEventType.TOOL_EXECUTION_COMPLETE and data:
360+ call_id = data.tool_call_id
361+ entry = tool_spans.pop(call_id, None ) if call_id else None
362+ if entry:
363+ tool_span, tool_token = entry
364+ context.detach(tool_token)
365+ tool_span.end()
366+
367+ # Capture final message
368+ elif event.type == SessionEventType.ASSISTANT_MESSAGE and data:
369+ if data.content:
370+ print (f " Assistant: { data.content} " )
371+
372+ unsubscribe = session.on(handle_event)
373+
374+ # Send message and wait for completion
375+ response = await session.send_and_wait({" prompt" : prompt})
376+
336377 span.set_attribute(" gen_ai.response.finish_reasons" , [" stop" ])
337-
378+ unsubscribe()
379+
338380 except Exception as e:
339381 span.set_attribute(" error.type" , type (e).__name__ )
340382 raise
@@ -344,9 +386,10 @@ async def invoke_agent(prompt: str):
344386 tool_span.set_attribute(" error.type" , " stream_aborted" )
345387 context.detach(tool_token)
346388 tool_span.end()
347-
389+
348390 context.detach(token)
349391 span.end()
392+ await client.stop()
350393
351394# Run
352395asyncio.run(invoke_agent(" What's 2+2?" ))
@@ -388,6 +431,7 @@ export OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=true
388431
389432### Checking at Runtime
390433
434+ <!-- docs-validate: skip -->
391435``` python
392436import os
393437
@@ -403,6 +447,7 @@ if should_record_content() and event.data.content:
403447
404448For MCP-based tools, add these additional attributes following the [ OpenTelemetry MCP semantic conventions] ( https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/ ) :
405449
450+ <!-- docs-validate: skip -->
406451``` python
407452tool_attrs = {
408453 # Required
@@ -507,6 +552,7 @@ View traces in the Azure Portal under your Application Insights resource → Tra
507552### Tool spans not showing as children
508553
509554Make sure to attach the tool span to the parent context:
555+ <!-- docs-validate: skip -->
510556``` python
511557tool_token = context.attach(trace.set_span_in_context(tool_span))
512558```
0 commit comments