@@ -117,13 +117,15 @@ export function recordToolObservation(
117117 output : string
118118 startTime ?: Date
119119 isError ?: boolean
120+ parentBatchSpan ?: LangfuseSpan | null
120121 } ,
121122) : void {
122123 if ( ! rootSpan || ! isLangfuseEnabled ( ) ) return
123124 try {
124125 // Use the global startObservation directly instead of rootSpan.startObservation().
125126 // The instance method only forwards asType and drops startTime,
126127 // causing tool execution duration to be 0.
128+ const parentSpan = params . parentBatchSpan ?? rootSpan
127129 const toolObs = startObservation (
128130 params . toolName ,
129131 {
@@ -136,7 +138,7 @@ export function recordToolObservation(
136138 {
137139 asType : 'tool' ,
138140 ...( params . startTime && { startTime : params . startTime } ) ,
139- parentSpanContext : rootSpan . otelSpan . spanContext ( ) ,
141+ parentSpanContext : parentSpan . otelSpan . spanContext ( ) ,
140142 } ,
141143 )
142144
@@ -158,6 +160,55 @@ export function recordToolObservation(
158160 }
159161}
160162
163+ /**
164+ * Create a span that wraps a batch of concurrent tool calls.
165+ * Returns the batch span (to be passed as parentBatchSpan to recordToolObservation)
166+ * and must be ended with endToolBatchSpan() after all tools complete.
167+ */
168+ export function createToolBatchSpan (
169+ rootSpan : LangfuseSpan | null ,
170+ params : { toolNames : string [ ] ; batchIndex : number } ,
171+ ) : LangfuseSpan | null {
172+ if ( ! rootSpan || ! isLangfuseEnabled ( ) ) return null
173+ try {
174+ const batchSpan = startObservation (
175+ `tools` ,
176+ {
177+ metadata : {
178+ toolNames : params . toolNames . join ( ', ' ) ,
179+ toolCount : String ( params . toolNames . length ) ,
180+ batchIndex : String ( params . batchIndex ) ,
181+ } ,
182+ } ,
183+ {
184+ asType : 'span' ,
185+ parentSpanContext : rootSpan . otelSpan . spanContext ( ) ,
186+ } ,
187+ ) as LangfuseSpan
188+
189+ const sessionId = ( rootSpan as unknown as RootTrace ) . _sessionId
190+ if ( sessionId ) {
191+ batchSpan . otelSpan . setAttribute ( LangfuseOtelSpanAttributes . TRACE_SESSION_ID , sessionId )
192+ }
193+
194+ logForDebugging ( `[langfuse] Tool batch span created: ${ batchSpan . id } (tools=${ params . toolNames . join ( ',' ) } )` )
195+ return batchSpan
196+ } catch ( e ) {
197+ logForDebugging ( `[langfuse] createToolBatchSpan failed: ${ e } ` , { level : 'error' } )
198+ return null
199+ }
200+ }
201+
202+ export function endToolBatchSpan ( batchSpan : LangfuseSpan | null ) : void {
203+ if ( ! batchSpan ) return
204+ try {
205+ batchSpan . end ( )
206+ logForDebugging ( `[langfuse] Tool batch span ended: ${ batchSpan . id } ` )
207+ } catch ( e ) {
208+ logForDebugging ( `[langfuse] endToolBatchSpan failed: ${ e } ` , { level : 'error' } )
209+ }
210+ }
211+
161212export function createSubagentTrace ( params : {
162213 sessionId : string
163214 agentType : string
@@ -187,14 +238,20 @@ export function createSubagentTrace(params: {
187238 }
188239}
189240
190- export function endTrace ( rootSpan : LangfuseSpan | null , output ?: unknown ) : void {
241+ export function endTrace (
242+ rootSpan : LangfuseSpan | null ,
243+ output ?: unknown ,
244+ status ?: 'interrupted' | 'error' ,
245+ ) : void {
191246 if ( ! rootSpan ) return
192247 try {
193- if ( output !== undefined ) {
194- rootSpan . update ( { output } )
195- }
248+ const updatePayload : Record < string , unknown > = { }
249+ if ( output !== undefined ) updatePayload . output = output
250+ if ( status === 'interrupted' ) updatePayload . level = 'WARNING'
251+ else if ( status === 'error' ) updatePayload . level = 'ERROR'
252+ if ( Object . keys ( updatePayload ) . length > 0 ) rootSpan . update ( updatePayload )
196253 rootSpan . end ( )
197- logForDebugging ( `[langfuse] Trace ended: ${ rootSpan . id } ` )
254+ logForDebugging ( `[langfuse] Trace ended: ${ rootSpan . id } ${ status ? ` ( ${ status } )` : '' } ` )
198255 } catch ( e ) {
199256 logForDebugging ( `[langfuse] endTrace failed: ${ e } ` , { level : 'error' } )
200257 }
0 commit comments