2121from haystack .core .pipeline .breakpoint import (
2222 SnapshotCallback ,
2323 _create_pipeline_snapshot ,
24+ _deserialize_internal_inputs ,
2425 _save_pipeline_snapshot ,
2526 _validate_break_point_against_pipeline ,
2627 _validate_pipeline_snapshot_against_pipeline ,
2728)
2829from haystack .core .pipeline .utils import _deepcopy_with_exceptions
2930from haystack .dataclasses import AsyncStreamingCallbackT , StreamingCallbackT , StreamingChunk , select_streaming_callback
30- from haystack .dataclasses .breakpoints import Breakpoint , PipelineSnapshot
31+ from haystack .dataclasses .breakpoints import INTERNAL_INPUTS_FORMAT , Breakpoint , PipelineSnapshot
3132from haystack .dataclasses .streaming_chunk import _invoke_streaming_callback
3233from haystack .telemetry import pipeline_running
3334from haystack .utils import _deserialize_value_with_schema
@@ -347,6 +348,9 @@ def run( # noqa: PLR0915, PLR0912, C901
347348 include_outputs_from = set ()
348349
349350 pipeline_outputs : dict [str , Any ] = {}
351+ # Set when resuming from a snapshot that predates `INTERNAL_INPUTS_FORMAT` and therefore lost the sender of
352+ # each input. Cleared as soon as the paused component has run.
353+ legacy_resume_component : str | None = None
350354
351355 if not pipeline_snapshot :
352356 # normalize `data`
@@ -362,14 +366,26 @@ def run( # noqa: PLR0915, PLR0912, C901
362366 # We track component visits to decide if a component can run.
363367 component_visits = dict .fromkeys (ordered_component_names , 0 )
364368
369+ inputs = self ._convert_to_internal_format (pipeline_inputs = data )
370+
365371 else :
366372 # Validate the pipeline snapshot against the current pipeline graph
367373 _validate_pipeline_snapshot_against_pipeline (pipeline_snapshot , self .graph )
368374
369375 # Handle resuming the pipeline from a snapshot
370376 component_visits = pipeline_snapshot .pipeline_state .component_visits
371377 ordered_component_names = pipeline_snapshot .ordered_component_names
372- data = _deserialize_value_with_schema (pipeline_snapshot .pipeline_state .inputs )
378+ data = _deserialize_value_with_schema (pipeline_snapshot .original_input_data )
379+
380+ if pipeline_snapshot .pipeline_state .inputs_format == INTERNAL_INPUTS_FORMAT :
381+ inputs = _deserialize_internal_inputs (pipeline_snapshot .pipeline_state .inputs )
382+ else :
383+ # A legacy snapshot lost the sender of each input, so the only thing we can do is treat them as if
384+ # they came from outside the pipeline. The paused component then has to be let through once.
385+ inputs = self ._convert_to_internal_format (
386+ pipeline_inputs = _deserialize_value_with_schema (pipeline_snapshot .pipeline_state .inputs )
387+ )
388+ legacy_resume_component = pipeline_snapshot .break_point .component_name
373389
374390 # include_outputs_from from the snapshot when resuming
375391 include_outputs_from = pipeline_snapshot .include_outputs_from
@@ -391,7 +407,6 @@ def run( # noqa: PLR0915, PLR0912, C901
391407 "haystack.pipeline.execution_mode" : "sync" ,
392408 },
393409 ) as span :
394- inputs = self ._convert_to_internal_format (pipeline_inputs = data )
395410 priority_queue = self ._fill_queue (ordered_component_names , inputs , component_visits )
396411
397412 # check if pipeline is blocked before execution
@@ -431,10 +446,15 @@ def run( # noqa: PLR0915, PLR0912, C901
431446 component_name , component_visits [component_name ]
432447 )
433448
434- if pipeline_snapshot :
435- is_resume = pipeline_snapshot .break_point .component_name == component_name
436- else :
437- is_resume = False
449+ is_resume = legacy_resume_component == component_name
450+ if is_resume :
451+ # Only the first execution of the paused component needs the legacy handling. Later visits of the
452+ # same component inside a loop receive regular inputs and must consume them normally.
453+ legacy_resume_component = None
454+
455+ # A snapshot has to store the component's inputs as they were before the component consumed them, so
456+ # that resuming re-triggers the component the same way this run did.
457+ component_inputs_before_consume = inputs .get (component_name , {})
438458 component_inputs = self ._consume_component_inputs (
439459 component_name = component_name , component = component , inputs = inputs , is_resume = is_resume
440460 )
@@ -444,13 +464,6 @@ def run( # noqa: PLR0915, PLR0912, C901
444464 # initialization
445465 component_inputs = self ._add_missing_input_defaults (component_inputs , component ["input_sockets" ])
446466
447- # Scenario 1: Pipeline snapshot is provided to resume the pipeline at a specific component
448- # Deserialize the component_inputs if they are passed in the pipeline_snapshot.
449- # this check will prevent other component_inputs generated at runtime from being deserialized
450- if pipeline_snapshot and component_name in pipeline_snapshot .pipeline_state .inputs .keys ():
451- for key , value in component_inputs .items ():
452- component_inputs [key ] = _deserialize_value_with_schema (value )
453-
454467 try :
455468 component_outputs = self ._run_component (
456469 component_name = component_name ,
@@ -474,7 +487,7 @@ def run( # noqa: PLR0915, PLR0912, C901
474487 # Create a snapshot of the state of the pipeline before the error occurred.
475488 pipeline_snapshot = _create_pipeline_snapshot (
476489 inputs = _deepcopy_with_exceptions (inputs ),
477- component_inputs = _deepcopy_with_exceptions (component_inputs ),
490+ component_inputs = _deepcopy_with_exceptions (component_inputs_before_consume ),
478491 break_point = saved_break_point ,
479492 component_visits = component_visits ,
480493 original_input_data = data ,
0 commit comments