@@ -808,3 +808,117 @@ def validate_policy_configuration(self) -> None:
808808 )
809809 self .logger .error (error_message )
810810 raise RuntimeError (error_message )
811+
812+
813+ class Recovery (Composite ):
814+ """
815+ A Recovery composite that wraps a main behaviour with a sequence of recovery behaviours.
816+
817+ .. graphviz:: dot/recovery.dot
818+
819+ Execution model:
820+
821+ - Tick the main behaviour first.
822+ - If main returns SUCCESS or RUNNING, propagate that.
823+ - If main returns FAILURE:
824+ * Attempt the next recovery behaviour in sequence.
825+ * If recovery RUNNING, propagate RUNNING.
826+ * If recovery completes (SUCCESS or FAILURE), consume it and retry main (if any recoveries remain).
827+ - If all recoveries are exhausted and main still fails, return FAILURE.
828+
829+ Args:
830+ name (:obj:`str`): the composite behaviour name
831+ children ([:class:`~py_trees.behaviour.Behaviour`]): list of children,
832+ where the first is the main behaviour and the rest are recovery behaviours
833+ """
834+
835+ def __init__ (
836+ self ,
837+ name : str ,
838+ children : typing .Optional [typing .Sequence [behaviour .Behaviour ]] = None ,
839+ ):
840+ super ().__init__ (name , children )
841+ if not children or len (children ) < 1 :
842+ raise ValueError ("Recovery requires at least a main behaviour" )
843+
844+ # Explicit references
845+ self .main : behaviour .Behaviour = children [0 ]
846+ self .recoveries : typing .List [behaviour .Behaviour ] = (
847+ list (children [1 :]) if len (children ) > 1 else []
848+ )
849+ self .current_recovery_index : int = 0
850+ self .running_main = True
851+
852+ def initialise (self ) -> None :
853+ """Reset to the initial state: run main behaviour and restart recovery behaviours sequence."""
854+ self .current_recovery_index = 0
855+ self .running_main = True
856+
857+ def tick (self ) -> typing .Iterator [behaviour .Behaviour ]:
858+ """
859+ Tick over the children.
860+
861+ Yields:
862+ :class:`~py_trees.behaviour.Behaviour`: a reference to itself or one of its children
863+ """
864+ self .logger .debug ("%s.tick()" % self .__class__ .__name__ )
865+
866+ if not self .children :
867+ self .stop (common .Status .FAILURE )
868+ yield self
869+ return
870+
871+ # First try the main behaviour if we are not in the middle of a recovery
872+ if self .running_main :
873+ for node in self .main .tick ():
874+ yield node
875+ if node is self .main :
876+ if node .status in (common .Status .SUCCESS , common .Status .RUNNING ):
877+ self .status = node .status
878+ yield self
879+ return
880+ elif node .status == common .Status .FAILURE :
881+ # proceed to next recovery
882+ self .running_main = False
883+
884+ # Try recoveries
885+ while self .current_recovery_index < len (self .recoveries ):
886+ recovery = self .recoveries [self .current_recovery_index ]
887+ for node in recovery .tick ():
888+ yield node
889+ if node is recovery :
890+ if node .status == common .Status .RUNNING :
891+ self .status = common .Status .RUNNING
892+ yield self
893+ return
894+ elif node .status == common .Status .SUCCESS :
895+ self .status = common .Status .RUNNING
896+ # consume this recovery and retry main
897+ recovery .stop (common .Status .INVALID )
898+ self .current_recovery_index += 1
899+ self .main .stop (common .Status .INVALID )
900+ self .running_main = True
901+ yield self
902+ return
903+ elif node .status == common .Status .FAILURE :
904+ # consume this recovery and move to next
905+ recovery .stop (common .Status .INVALID )
906+ self .current_recovery_index += 1
907+ yield self
908+
909+ # No recoveries left → fail
910+ self .status = common .Status .FAILURE
911+ yield self
912+
913+ def stop (self , new_status : common .Status = common .Status .INVALID ) -> None :
914+ """
915+ Ensure that children are appropriately stopped and update status.
916+
917+ Args:
918+ new_status : the composite is transitioning to this new status
919+ """
920+ for child in self .children :
921+ if child .status != common .Status .INVALID :
922+ child .stop (common .Status .INVALID )
923+ self .current_recovery_index = 0
924+ super ().stop (new_status )
0 commit comments