1818import { InMemoryRunner , isFinalResponse } from '@google/adk' ;
1919import type { LlmAgent } from '@google/adk' ;
2020import type { Content } from '@google/genai' ;
21- import type { Span as OtelSpan } from '@opentelemetry/api' ;
21+ import type { Context as OtelContext , Span as OtelSpan } from '@opentelemetry/api' ;
2222import { SpanStatusCode , context , trace } from '@opentelemetry/api' ;
2323import { agentLogger } from '../utils/logger.js' ;
2424import { createPlanningAgent } from './planning.js' ;
@@ -141,14 +141,11 @@ function buildRetryMessage(
141141}
142142
143143/**
144- * Record an OTel span for a tool call, nested under the current agent span.
144+ * Start an OTel span for a tool call, nested under the current agent span.
145+ * The caller is responsible for ending the span (after the tool response arrives).
145146 */
146- function recordToolSpan (
147- name : string ,
148- args : unknown ,
149- parentCtx : ReturnType < typeof context . active >
150- ) : void {
151- const span = tracer . startSpan (
147+ function startToolSpan ( name : string , args : unknown , parentCtx : OtelContext ) : OtelSpan {
148+ return tracer . startSpan (
152149 `tool: ${ name } ` ,
153150 {
154151 attributes : {
@@ -159,7 +156,14 @@ function recordToolSpan(
159156 } ,
160157 parentCtx
161158 ) ;
162- span . end ( ) ;
159+ }
160+
161+ /**
162+ * Truncate a serialized result string to a maximum length for span attributes.
163+ */
164+ function truncateForSpan ( value : string , maxLength = 4096 ) : string {
165+ if ( value . length <= maxLength ) return value ;
166+ return `${ value . slice ( 0 , maxLength ) } ...[truncated]` ;
163167}
164168
165169const MAX_RETRIES = 5 ;
@@ -188,6 +192,7 @@ export async function* runAgent(
188192
189193 let currentAgentSpan : OtelSpan | null = null ;
190194 let currentAgentCtx = rootCtx ;
195+ const openToolSpans = new Map < string , OtelSpan > ( ) ;
191196
192197 try {
193198 await getOrCreateSession ( runner , sid ) ;
@@ -268,7 +273,9 @@ export async function* runAgent(
268273 yield { type : 'text' , content : part . text } ;
269274 }
270275 if ( 'functionCall' in part && part . functionCall ?. name ) {
271- recordToolSpan ( part . functionCall . name , part . functionCall . args , currentAgentCtx ) ;
276+ const callId = part . functionCall . id ?? part . functionCall . name ;
277+ const toolSpan = startToolSpan ( part . functionCall . name , part . functionCall . args , currentAgentCtx ) ;
278+ openToolSpans . set ( callId , toolSpan ) ;
272279 agentLogger . debug (
273280 `[Runner] Tool call: ${ part . functionCall . name } , args: ${ JSON . stringify ( part . functionCall . args ) } `
274281 ) ;
@@ -282,6 +289,23 @@ export async function* runAgent(
282289 } ,
283290 } ;
284291 }
292+ if ( 'functionResponse' in part && part . functionResponse ) {
293+ const { id, name, response } = part . functionResponse as {
294+ id ?: string ;
295+ name ?: string ;
296+ response ?: unknown ;
297+ } ;
298+ const callId = id ?? name ;
299+ const toolSpan = callId ? openToolSpans . get ( callId ) : undefined ;
300+ if ( toolSpan ) {
301+ toolSpan . setAttribute (
302+ 'gen_ai.tool.call.result' ,
303+ truncateForSpan ( JSON . stringify ( response ) )
304+ ) ;
305+ toolSpan . end ( ) ;
306+ openToolSpans . delete ( callId ) ;
307+ }
308+ }
285309 }
286310 }
287311
@@ -334,6 +358,10 @@ export async function* runAgent(
334358 yield { type : 'done' } ;
335359 }
336360 } finally {
361+ for ( const span of openToolSpans . values ( ) ) {
362+ span . end ( ) ;
363+ }
364+ openToolSpans . clear ( ) ;
337365 if ( currentAgentSpan ) currentAgentSpan . end ( ) ;
338366 rootSpan . end ( ) ;
339367 }
0 commit comments