@@ -97,21 +97,6 @@ def _resolve_secret(self, value: str | SecretReference) -> str:
9797 context = f"step '{ self .step .id } '"
9898 return self ._secret_manager (value , context )
9999
100- def _enrich_with_span_metadata (
101- self , message : FlowMessage , span : trace .Span
102- ) -> FlowMessage :
103- """Add span_id and trace_id to message metadata for feedback tracking."""
104- if not span .is_recording ():
105- return message
106-
107- span_context = span .get_span_context ()
108- updated_metadata = {
109- ** message .metadata ,
110- "span_id" : format (span_context .span_id , "016x" ),
111- "trace_id" : format (span_context .trace_id , "032x" ),
112- }
113- return message .model_copy (update = {"metadata" : updated_metadata })
114-
115100 async def _filter_and_collect_errors (
116101 self ,
117102 message_stream : AsyncIterator [FlowMessage ],
@@ -191,7 +176,10 @@ async def execute(
191176 Processed messages, with failed messages emitted first
192177 """
193178 # Start a span for tracking
194- # Note: We manually manage the span lifecycle to allow yielding
179+ # Note: We do NOT attach this span to context here to avoid
180+ # making upstream steps children of this step when we consume
181+ # the input stream. Instead, _process_message_with_telemetry
182+ # will attach it when calling process_message().
195183 span = self ._tracer .start_span (
196184 f"step.{ self .step .id } " ,
197185 attributes = {
@@ -201,10 +189,8 @@ async def execute(
201189 },
202190 )
203191
204- # Make this span the active context so child spans will nest under it
205- # Only attach if span is recording (i.e., real tracer is configured)
206- ctx = trace .set_span_in_context (span )
207- token = context .attach (ctx ) if span .is_recording () else None
192+ # Store span in self so _process_message_with_telemetry can access it
193+ self ._current_step_span = span
208194
209195 # Initialize the cache
210196 # this is done once per execution so re-runs are fast
@@ -259,7 +245,7 @@ async def process_item(
259245 self .progress .update_for_message (
260246 result , self .context .on_progress
261247 )
262- yield self . _enrich_with_span_metadata ( result , span )
248+ yield result
263249
264250 # Emit failed messages after processing completes
265251 for msg in failed_messages :
@@ -268,14 +254,14 @@ async def process_item(
268254 self .progress .update_for_message (
269255 msg , self .context .on_progress
270256 )
271- yield self . _enrich_with_span_metadata ( msg , span )
257+ yield msg
272258
273259 # Finalize and track those messages too
274260 async for msg in self .finalize ():
275261 message_count += 1
276262 if msg .is_failed ():
277263 error_count += 1
278- yield self . _enrich_with_span_metadata ( msg , span )
264+ yield msg
279265
280266 # Close the cache
281267 if self .cache :
@@ -302,10 +288,8 @@ async def process_item(
302288 span .set_status (Status (StatusCode .ERROR , f"Step failed: { e } " ))
303289 raise
304290 finally :
305- # Detach the context and end the span
306- # Only detach if we successfully attached (span was recording)
307- if token is not None :
308- context .detach (token )
291+ # Clean up step span reference and end the span
292+ self ._current_step_span = None
309293 span .end ()
310294
311295 @abstractmethod
@@ -383,51 +367,77 @@ async def _process_message_with_telemetry(
383367
384368 This method creates a child span for each message processing
385369 operation, automatically recording errors and success metrics.
386- The child span will automatically be nested under the current
387- active span in the context.
370+ The child span will be nested under the step span.
371+
372+ The step span context is attached here (not in execute()) to
373+ ensure step spans are siblings under the flow span, not nested.
388374 """
389- # Get current context and create child span within it
390- span = self . _tracer . start_span (
391- f"step. { self . step . id } .process_message" ,
392- attributes = {
393- "session.id" : message . session . session_id ,
394- },
395- )
375+ # Attach step span context so process_message span becomes its child
376+ step_span = getattr ( self , "_current_step_span" , None )
377+ if step_span and step_span . is_recording ():
378+ ctx = trace . set_span_in_context ( step_span )
379+ token = context . attach ( ctx )
380+ else :
381+ token = None
396382
397383 try :
398- output_count = 0
399- error_occurred = False
400-
401- async for output_msg in self .process_message (message ):
402- output_count += 1
403- if output_msg .is_failed ():
404- error_occurred = True
405- span .add_event (
406- "message_failed" ,
407- {
408- "error" : str (output_msg .error ),
409- },
384+ # Create child span for this specific message processing
385+ span = self ._tracer .start_span (
386+ f"step.{ self .step .id } .process_message" ,
387+ attributes = {
388+ "session.id" : message .session .session_id ,
389+ },
390+ )
391+
392+ try :
393+ output_count = 0
394+ error_occurred = False
395+
396+ async for output_msg in self .process_message (message ):
397+ output_count += 1
398+ if output_msg .is_failed ():
399+ error_occurred = True
400+ span .add_event (
401+ "message_failed" ,
402+ {
403+ "error" : str (output_msg .error ),
404+ },
405+ )
406+ # Enrich with process_message span for feedback tracking
407+ span_context = span .get_span_context ()
408+ updated_metadata = {
409+ ** output_msg .metadata ,
410+ "span_id" : format (span_context .span_id , "016x" ),
411+ "trace_id" : format (span_context .trace_id , "032x" ),
412+ }
413+ yield output_msg .model_copy (
414+ update = {"metadata" : updated_metadata }
410415 )
411- yield output_msg
412416
413- # Record processing metrics
414- span .set_attribute ("message.outputs" , output_count )
417+ # Record processing metrics
418+ span .set_attribute ("message.outputs" , output_count )
415419
416- if error_occurred :
420+ if error_occurred :
421+ span .set_status (
422+ Status (
423+ StatusCode .ERROR , "Message processing had errors"
424+ )
425+ )
426+ else :
427+ span .set_status (Status (StatusCode .OK ))
428+
429+ except Exception as e :
430+ span .record_exception (e )
417431 span .set_status (
418- Status (StatusCode .ERROR , "Message processing had errors " )
432+ Status (StatusCode .ERROR , f"Processing failed: { e } " )
419433 )
420- else :
421- span .set_status (Status (StatusCode .OK ))
422-
423- except Exception as e :
424- span .record_exception (e )
425- span .set_status (
426- Status (StatusCode .ERROR , f"Processing failed: { e } " )
427- )
428- raise
434+ raise
435+ finally :
436+ span .end ()
429437 finally :
430- span .end ()
438+ # Detach step span context
439+ if token is not None :
440+ context .detach (token )
431441
432442 async def finalize (self ) -> AsyncIterator [FlowMessage ]:
433443 """
0 commit comments