flock-core 0.4.0b26__py3-none-any.whl → 0.4.0b27__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

flock/core/flock_agent.py CHANGED
@@ -10,6 +10,7 @@ from datetime import datetime
10
10
  from typing import TYPE_CHECKING, Any, TypeVar
11
11
 
12
12
  from flock.core.serialization.json_encoder import FlockJSONEncoder
13
+ from flock.workflow.temporal_config import TemporalActivityConfig
13
14
 
14
15
  if TYPE_CHECKING:
15
16
  from flock.core.context.context import FlockContext
@@ -110,6 +111,12 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
110
111
  description="Dictionary of FlockModules attached to this agent.",
111
112
  )
112
113
 
114
+ # --- Temporal Configuration (Optional) ---
115
+ temporal_activity_config: TemporalActivityConfig | None = Field(
116
+ default=None,
117
+ description="Optional Temporal settings specific to this agent's activity execution.",
118
+ )
119
+
113
120
  # --- Runtime State (Excluded from Serialization) ---
114
121
  context: FlockContext | None = Field(
115
122
  default=None,
@@ -130,6 +137,7 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
130
137
  modules: dict[str, "FlockModule"] | None = None, # Use dict for modules
131
138
  write_to_file: bool = False,
132
139
  wait_for_input: bool = False,
140
+ temporal_activity_config: TemporalActivityConfig | None = None,
133
141
  **kwargs,
134
142
  ):
135
143
  super().__init__(
@@ -146,6 +154,7 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
146
154
  modules=modules
147
155
  if modules is not None
148
156
  else {}, # Ensure modules is a dict
157
+ temporal_activity_config=temporal_activity_config,
149
158
  **kwargs,
150
159
  )
151
160
 
@@ -723,202 +732,196 @@ class FlockAgent(BaseModel, Serializable, DSPyIntegrationMixin, ABC):
723
732
 
724
733
  @classmethod
725
734
  def from_dict(cls: type[T], data: dict[str, Any]) -> T:
726
- """Create instance from dictionary representation."""
727
- from flock.core.flock_registry import get_registry
728
-
729
- logger.debug(
730
- f"Deserializing agent from dict. Provided keys: {list(data.keys())}"
735
+ """Deserialize the agent from a dictionary, including components, tools, and callables."""
736
+ from flock.core.flock_registry import (
737
+ get_registry, # Import registry locally
731
738
  )
732
- if "name" not in data:
733
- raise ValueError("Agent data must include a 'name' field.")
734
- FlockRegistry = get_registry()
735
- agent_name = data["name"] # For logging context
736
- logger.info(f"Deserializing agent '{agent_name}'")
737
-
738
- # Pop complex components to handle them after basic agent instantiation
739
- evaluator_data = data.pop("evaluator", None)
740
- router_data = data.pop("handoff_router", None)
741
- modules_data = data.pop("modules", {})
742
- tools_data = data.pop("tools", [])
743
- description_callable = data.pop("description_callable", None)
744
- input_callable = data.pop("input_callable", None)
745
- output_callable = data.pop("output_callable", None)
746
739
 
740
+ registry = get_registry()
747
741
  logger.debug(
748
- f"Agent '{agent_name}' has {len(modules_data)} modules and {len(tools_data)} tools"
742
+ f"Deserializing agent from dict. Keys: {list(data.keys())}"
749
743
  )
750
744
 
751
- # Deserialize remaining data recursively (handles nested basic types/callables)
752
- # Note: Pydantic v2 handles most basic deserialization well if types match.
753
- # Explicit deserialize_item might be needed if complex non-pydantic structures exist.
754
- # For now, assume Pydantic handles basic fields based on type hints.
755
- deserialized_basic_data = data # Assume Pydantic handles basic fields
756
-
757
- try:
758
- # Create the agent instance using Pydantic's constructor
759
- logger.debug(
760
- f"Creating agent instance with fields: {list(deserialized_basic_data.keys())}"
761
- )
762
- agent = cls(**deserialized_basic_data)
763
- except Exception as e:
764
- logger.error(
765
- f"Pydantic validation/init failed for agent '{agent_name}': {e}",
766
- exc_info=True,
767
- )
745
+ # --- Separate Data ---
746
+ component_configs = {}
747
+ callable_configs = {}
748
+ tool_config = []
749
+ agent_data = {}
750
+
751
+ component_keys = [
752
+ "evaluator",
753
+ "handoff_router",
754
+ "modules",
755
+ "temporal_activity_config",
756
+ ]
757
+ callable_keys = [
758
+ "description_callable",
759
+ "input_callable",
760
+ "output_callable",
761
+ ]
762
+ tool_key = "tools"
763
+
764
+ for key, value in data.items():
765
+ if key in component_keys and value is not None:
766
+ component_configs[key] = value
767
+ elif key in callable_keys and value is not None:
768
+ callable_configs[key] = value
769
+ elif key == tool_key and value is not None:
770
+ tool_config = value # Expecting a list of names
771
+ elif key not in component_keys + callable_keys + [
772
+ tool_key
773
+ ]: # Avoid double adding
774
+ agent_data[key] = value
775
+ # else: ignore keys that are None or already handled
776
+
777
+ # --- Deserialize Base Agent ---
778
+ # Ensure required fields like 'name' are present if needed by __init__
779
+ if "name" not in agent_data:
768
780
  raise ValueError(
769
- f"Failed to initialize agent '{agent_name}' from dict: {e}"
770
- ) from e
781
+ "Agent data must include a 'name' field for deserialization."
782
+ )
783
+ agent_name_log = agent_data["name"] # For logging
784
+ logger.info(f"Deserializing base agent data for '{agent_name_log}'")
785
+
786
+ # Pydantic should handle base fields based on type hints in __init__
787
+ agent = cls(**agent_data)
788
+ logger.debug(f"Base agent '{agent.name}' instantiated.")
771
789
 
772
- # --- Deserialize and Attach Components ---
790
+ # --- Deserialize Components ---
791
+ logger.debug(f"Deserializing components for '{agent.name}'")
773
792
  # Evaluator
774
- if evaluator_data:
793
+ if "evaluator" in component_configs:
775
794
  try:
776
- logger.debug(
777
- f"Deserializing evaluator for agent '{agent_name}'"
778
- )
779
795
  agent.evaluator = deserialize_component(
780
- evaluator_data, FlockEvaluator
781
- )
782
- if agent.evaluator is None:
783
- raise ValueError("deserialize_component returned None")
784
- logger.debug(
785
- f"Deserialized evaluator '{agent.evaluator.name}' of type '{evaluator_data.get('type')}' for agent '{agent_name}'"
796
+ component_configs["evaluator"], FlockEvaluator
786
797
  )
798
+ logger.debug(f"Deserialized evaluator for '{agent.name}'")
787
799
  except Exception as e:
788
800
  logger.error(
789
- f"Failed to deserialize evaluator for agent '{agent_name}': {e}",
801
+ f"Failed to deserialize evaluator for '{agent.name}': {e}",
790
802
  exc_info=True,
791
803
  )
792
- # Decide: raise error or continue without evaluator?
793
- # raise ValueError(f"Failed to deserialize evaluator for agent '{agent_name}': {e}") from e
794
804
 
795
- # Router
796
- if router_data:
805
+ # Handoff Router
806
+ if "handoff_router" in component_configs:
797
807
  try:
798
- logger.debug(f"Deserializing router for agent '{agent_name}'")
799
808
  agent.handoff_router = deserialize_component(
800
- router_data, FlockRouter
801
- )
802
- if agent.handoff_router is None:
803
- raise ValueError("deserialize_component returned None")
804
- logger.debug(
805
- f"Deserialized router '{agent.handoff_router.name}' of type '{router_data.get('type')}' for agent '{agent_name}'"
809
+ component_configs["handoff_router"], FlockRouter
806
810
  )
811
+ logger.debug(f"Deserialized handoff_router for '{agent.name}'")
807
812
  except Exception as e:
808
813
  logger.error(
809
- f"Failed to deserialize router for agent '{agent_name}': {e}",
814
+ f"Failed to deserialize handoff_router for '{agent.name}': {e}",
810
815
  exc_info=True,
811
816
  )
812
- # Decide: raise error or continue without router?
813
817
 
814
818
  # Modules
815
- if modules_data:
816
- agent.modules = {} # Ensure it's initialized
817
- logger.debug(
818
- f"Deserializing {len(modules_data)} modules for agent '{agent_name}'"
819
- )
820
- for name, module_data in modules_data.items():
819
+ if "modules" in component_configs:
820
+ agent.modules = {} # Initialize
821
+ for module_name, module_data in component_configs[
822
+ "modules"
823
+ ].items():
821
824
  try:
822
- logger.debug(
823
- f"Deserializing module '{name}' of type '{module_data.get('type')}' for agent '{agent_name}'"
824
- )
825
825
  module_instance = deserialize_component(
826
826
  module_data, FlockModule
827
827
  )
828
828
  if module_instance:
829
- # Ensure instance name matches key if possible
830
- module_instance.name = module_data.get("name", name)
831
- agent.add_module(
832
- module_instance
833
- ) # Use add_module for consistency
829
+ # Use add_module for potential logic within it
830
+ agent.add_module(module_instance)
834
831
  logger.debug(
835
- f"Successfully added module '{name}' to agent '{agent_name}'"
832
+ f"Deserialized and added module '{module_name}' for '{agent.name}'"
836
833
  )
837
- else:
838
- raise ValueError("deserialize_component returned None")
839
834
  except Exception as e:
840
835
  logger.error(
841
- f"Failed to deserialize module '{name}' for agent '{agent_name}': {e}",
836
+ f"Failed to deserialize module '{module_name}' for '{agent.name}': {e}",
842
837
  exc_info=True,
843
838
  )
844
- # Decide: skip module or raise error?
839
+
840
+ # Temporal Activity Config
841
+ if "temporal_activity_config" in component_configs:
842
+ try:
843
+ agent.temporal_activity_config = TemporalActivityConfig(
844
+ **component_configs["temporal_activity_config"]
845
+ )
846
+ logger.debug(
847
+ f"Deserialized temporal_activity_config for '{agent.name}'"
848
+ )
849
+ except Exception as e:
850
+ logger.error(
851
+ f"Failed to deserialize temporal_activity_config for '{agent.name}': {e}",
852
+ exc_info=True,
853
+ )
854
+ agent.temporal_activity_config = None
845
855
 
846
856
  # --- Deserialize Tools ---
847
857
  agent.tools = [] # Initialize tools list
848
- if tools_data:
849
- # Get component registry to look up function imports
850
- registry = get_registry()
851
- components = getattr(registry, "_callables", {})
852
- logger.debug(
853
- f"Deserializing {len(tools_data)} tools for agent '{agent_name}'"
854
- )
858
+ if tool_config:
855
859
  logger.debug(
856
- f"Available callables in registry: {list(components.keys())}"
860
+ f"Deserializing {len(tool_config)} tools for '{agent.name}'"
857
861
  )
858
-
859
- for tool_name in tools_data:
862
+ # Use get_callable to find each tool
863
+ for tool_name_or_path in tool_config:
860
864
  try:
861
- logger.debug(f"Looking for tool '{tool_name}' in registry")
862
- # First try to lookup by simple name in the registry's callables
863
- found = False
864
- for path_str, func in components.items():
865
- if (
866
- path_str.endswith("." + tool_name)
867
- or path_str == tool_name
868
- ):
869
- agent.tools.append(func)
870
- found = True
871
- logger.info(
872
- f"Found tool '{tool_name}' via path '{path_str}' for agent '{agent_name}'"
873
- )
874
- break
875
-
876
- # If not found by simple name, try manual import
877
- if not found:
865
+ found_tool = registry.get_callable(tool_name_or_path)
866
+ if found_tool and callable(found_tool):
867
+ agent.tools.append(found_tool)
878
868
  logger.debug(
879
- f"Attempting to import tool '{tool_name}' from modules"
869
+ f"Resolved and added tool '{tool_name_or_path}' for agent '{agent.name}'"
880
870
  )
881
- # Check in relevant modules (could be customized based on project structure)
882
- import __main__
883
-
884
- if hasattr(__main__, tool_name):
885
- agent.tools.append(getattr(__main__, tool_name))
886
- found = True
887
- logger.info(
888
- f"Found tool '{tool_name}' in __main__ module for agent '{agent_name}'"
889
- )
890
-
891
- if not found:
871
+ else:
872
+ # Should not happen if get_callable returns successfully but just in case
892
873
  logger.warning(
893
- f"Could not find tool '{tool_name}' for agent '{agent_name}'"
874
+ f"Registry returned non-callable for tool '{tool_name_or_path}' for agent '{agent.name}'. Skipping."
894
875
  )
876
+ except (
877
+ ValueError
878
+ ) as e: # get_callable raises ValueError if not found/ambiguous
879
+ logger.warning(
880
+ f"Could not resolve tool '{tool_name_or_path}' for agent '{agent.name}': {e}. Skipping."
881
+ )
895
882
  except Exception as e:
896
883
  logger.error(
897
- f"Error adding tool '{tool_name}' to agent '{agent_name}': {e}",
884
+ f"Unexpected error resolving tool '{tool_name_or_path}' for agent '{agent.name}': {e}. Skipping.",
898
885
  exc_info=True,
899
886
  )
900
887
 
901
- if description_callable:
902
- logger.debug(
903
- f"Deserializing description callable '{description_callable}' for agent '{agent_name}'"
904
- )
905
- agent.description = components[description_callable]
888
+ # --- Deserialize Callables ---
889
+ logger.debug(f"Deserializing callable fields for '{agent.name}'")
890
+ # available_callables = registry.get_all_callables() # Incorrect
906
891
 
907
- if input_callable:
908
- logger.debug(
909
- f"Deserializing input callable '{input_callable}' for agent '{agent_name}'"
910
- )
911
- agent.input = components[input_callable]
892
+ def resolve_and_assign(field_name: str, callable_key: str):
893
+ if callable_key in callable_configs:
894
+ callable_name = callable_configs[callable_key]
895
+ try:
896
+ # Use get_callable to find the signature function
897
+ found_callable = registry.get_callable(callable_name)
898
+ if found_callable and callable(found_callable):
899
+ setattr(agent, field_name, found_callable)
900
+ logger.debug(
901
+ f"Resolved callable '{callable_name}' for field '{field_name}' on agent '{agent.name}'"
902
+ )
903
+ else:
904
+ logger.warning(
905
+ f"Registry returned non-callable for name '{callable_name}' for field '{field_name}' on agent '{agent.name}'. Field remains default."
906
+ )
907
+ except (
908
+ ValueError
909
+ ) as e: # get_callable raises ValueError if not found/ambiguous
910
+ logger.warning(
911
+ f"Could not resolve callable '{callable_name}' in registry for field '{field_name}' on agent '{agent.name}': {e}. Field remains default."
912
+ )
913
+ except Exception as e:
914
+ logger.error(
915
+ f"Unexpected error resolving callable '{callable_name}' for field '{field_name}' on agent '{agent.name}': {e}. Field remains default.",
916
+ exc_info=True,
917
+ )
918
+ # Else: key not present, field retains its default value from __init__
912
919
 
913
- if output_callable:
914
- logger.debug(
915
- f"Deserializing output callable '{output_callable}' for agent '{agent_name}'"
916
- )
917
- agent.output = components[output_callable]
920
+ resolve_and_assign("description", "description_callable")
921
+ resolve_and_assign("input", "input_callable")
922
+ resolve_and_assign("output", "output_callable")
918
923
 
919
- logger.info(
920
- f"Successfully deserialized agent '{agent_name}' with {len(agent.modules)} modules and {len(agent.tools)} tools"
921
- )
924
+ logger.info(f"Successfully deserialized agent '{agent.name}'.")
922
925
  return agent
923
926
 
924
927
  # --- Pydantic v2 Configuration ---
@@ -14,6 +14,7 @@ from flock.modules.performance.metrics_module import (
14
14
  MetricsModule,
15
15
  MetricsModuleConfig,
16
16
  )
17
+ from flock.workflow.temporal_config import TemporalActivityConfig
17
18
 
18
19
 
19
20
  class FlockFactory:
@@ -39,6 +40,7 @@ class FlockFactory:
39
40
  write_to_file: bool = False,
40
41
  stream: bool = False,
41
42
  include_thought_process: bool = False,
43
+ temporal_activity_config: TemporalActivityConfig | None = None,
42
44
  ) -> FlockAgent:
43
45
  """Creates a default FlockAgent.
44
46
 
@@ -69,6 +71,7 @@ class FlockFactory:
69
71
  evaluator=evaluator,
70
72
  write_to_file=write_to_file,
71
73
  wait_for_input=wait_for_input,
74
+ temporal_activity_config=temporal_activity_config,
72
75
  )
73
76
  output_config = OutputModuleConfig(
74
77
  render_table=enable_rich_tables,
@@ -0,0 +1,228 @@
1
+ """Defines granular Temporal activities for executing a single agent
2
+ and determining the next agent in a Flock workflow.
3
+ """
4
+
5
+ from collections.abc import Callable
6
+
7
+ from opentelemetry import trace
8
+ from temporalio import activity
9
+
10
+ # Third-party imports only within activity functions if needed, or pass context
11
+ # For core flock types, import directly
12
+ from flock.core.context.context import FlockContext
13
+ from flock.core.context.context_vars import FLOCK_MODEL
14
+ from flock.core.flock_agent import FlockAgent # Import concrete class if needed
15
+ from flock.core.flock_registry import get_registry
16
+ from flock.core.flock_router import HandOffRequest
17
+ from flock.core.logging.logging import get_logger
18
+ from flock.core.util.input_resolver import resolve_inputs
19
+
20
+ logger = get_logger("agent_activity") # Using a distinct logger category
21
+ tracer = trace.get_tracer(__name__)
22
+ registry = get_registry() # Get registry instance once
23
+
24
+
25
+ @activity.defn
26
+ async def execute_single_agent(agent_name: str, context: FlockContext) -> dict:
27
+ """Executes a single specified agent and returns its result.
28
+
29
+ Args:
30
+ agent_name: The name of the agent to execute.
31
+ context: The current FlockContext (passed from the workflow).
32
+
33
+ Returns:
34
+ The raw result dictionary from the agent's execution.
35
+
36
+ Raises:
37
+ ValueError: If the agent is not found in the registry.
38
+ Exception: Propagates exceptions from agent execution for Temporal retries.
39
+ """
40
+ with tracer.start_as_current_span("execute_single_agent") as span:
41
+ span.set_attribute("agent.name", agent_name)
42
+ logger.info("Executing single agent", agent=agent_name)
43
+
44
+ agent = registry.get_agent(agent_name)
45
+ if not agent:
46
+ logger.error("Agent not found in registry", agent=agent_name)
47
+ # Raise error for Temporal to potentially retry/fail the activity
48
+ raise ValueError(f"Agent '{agent_name}' not found in registry.")
49
+
50
+ # Set agent's context reference (transient, for this execution)
51
+ agent.context = context
52
+
53
+ # Ensure model is set (using context value if needed)
54
+ # Consider if this should be done once when agent is added or workflow starts
55
+ if agent.model is None:
56
+ agent_model = context.get_variable(FLOCK_MODEL)
57
+ if agent_model:
58
+ agent.set_model(agent_model)
59
+ logger.debug(
60
+ f"Set model for agent '{agent_name}' from context: {agent_model}"
61
+ )
62
+
63
+ # Resolve agent-specific callables if necessary
64
+ # This might be better handled in the workflow before the loop starts
65
+ # or when agents are initially loaded. Assuming it's handled elsewhere for now.
66
+ # agent.resolve_callables(context=context)
67
+
68
+ # Resolve inputs for this specific agent run
69
+ previous_agent_name = (
70
+ context.get_last_agent_name()
71
+ ) # Relies on context method
72
+ logger.debug(
73
+ f"Resolving inputs for {agent_name} with previous agent {previous_agent_name}"
74
+ )
75
+ agent_inputs = resolve_inputs(agent.input, context, previous_agent_name)
76
+ span.add_event(
77
+ "resolved inputs", attributes={"inputs": str(agent_inputs)}
78
+ )
79
+
80
+ try:
81
+ # Execute just this agent
82
+ result = await agent.run_async(agent_inputs)
83
+ # Avoid logging potentially large results directly to span attributes
84
+ result_str = str(result)
85
+ span.set_attribute("result.type", type(result).__name__)
86
+ span.set_attribute(
87
+ "result.preview",
88
+ result_str[:500] + ("..." if len(result_str) > 500 else ""),
89
+ )
90
+ logger.info("Single agent execution completed", agent=agent_name)
91
+ return result
92
+ except Exception as e:
93
+ logger.error(
94
+ "Single agent execution failed",
95
+ agent=agent_name,
96
+ error=str(e),
97
+ exc_info=True,
98
+ )
99
+ span.record_exception(e)
100
+ # Re-raise the exception for Temporal to handle based on retry policy
101
+ raise
102
+
103
+
104
+ @activity.defn
105
+ async def determine_next_agent(
106
+ current_agent_name: str, result: dict, context: FlockContext
107
+ ) -> dict | None:
108
+ """Determines the next agent using the current agent's handoff router.
109
+
110
+ Args:
111
+ current_agent_name: The name of the agent that just ran.
112
+ result: The result produced by the current agent.
113
+ context: The current FlockContext.
114
+
115
+ Returns:
116
+ A dictionary representing the HandOffRequest (serialized via model_dump),
117
+ or None if no handoff occurs or router doesn't specify a next agent.
118
+
119
+ Raises:
120
+ ValueError: If the current agent cannot be found.
121
+ Exception: Propagates exceptions from router execution for Temporal retries.
122
+ """
123
+ with tracer.start_as_current_span("determine_next_agent") as span:
124
+ span.set_attribute("agent.name", current_agent_name)
125
+ logger.info("Determining next agent after", agent=current_agent_name)
126
+
127
+ agent = registry.get_agent(current_agent_name)
128
+ if not agent:
129
+ logger.error(
130
+ "Agent not found for routing", agent=current_agent_name
131
+ )
132
+ raise ValueError(
133
+ f"Agent '{current_agent_name}' not found for routing."
134
+ )
135
+
136
+ if not agent.handoff_router:
137
+ logger.info(
138
+ "No handoff router defined for agent", agent=current_agent_name
139
+ )
140
+ span.add_event("no_router")
141
+ return None # Indicate no handoff
142
+
143
+ logger.debug(
144
+ f"Using router {agent.handoff_router.__class__.__name__}",
145
+ agent=agent.name,
146
+ )
147
+ try:
148
+ # Execute the routing logic
149
+ handoff_data: (
150
+ HandOffRequest | Callable
151
+ ) = await agent.handoff_router.route(agent, result, context)
152
+
153
+ # Handle callable handoff functions - This is complex in distributed systems.
154
+ # Consider if this pattern should be supported or if routing should always
155
+ # return serializable data directly. Executing arbitrary code from context
156
+ # within an activity can have side effects and security implications.
157
+ # Assuming for now it MUST return HandOffRequest or structure convertible to it.
158
+ if callable(handoff_data):
159
+ logger.warning(
160
+ "Callable handoff detected - executing function.",
161
+ agent=agent.name,
162
+ )
163
+ # Ensure context is available if the callable needs it
164
+ try:
165
+ handoff_data = handoff_data(
166
+ context, result
167
+ ) # Potential side effects
168
+ if not isinstance(handoff_data, HandOffRequest):
169
+ logger.error(
170
+ "Handoff function did not return a HandOffRequest object.",
171
+ agent=agent.name,
172
+ )
173
+ raise TypeError(
174
+ "Handoff function must return a HandOffRequest object."
175
+ )
176
+ except Exception as e:
177
+ logger.error(
178
+ "Handoff function execution failed",
179
+ agent=agent.name,
180
+ error=str(e),
181
+ exc_info=True,
182
+ )
183
+ span.record_exception(e)
184
+ raise # Propagate error
185
+
186
+ # Ensure we have a HandOffRequest object after potentially calling function
187
+ if not isinstance(handoff_data, HandOffRequest):
188
+ logger.error(
189
+ "Router returned unexpected type",
190
+ type=type(handoff_data).__name__,
191
+ agent=agent.name,
192
+ )
193
+ raise TypeError(
194
+ f"Router for agent '{agent.name}' did not return a HandOffRequest object."
195
+ )
196
+
197
+ # Ensure agent instance is converted to name for serialization across boundaries
198
+ if isinstance(handoff_data.next_agent, FlockAgent):
199
+ handoff_data.next_agent = handoff_data.next_agent.name
200
+
201
+ # If router logic determines no further agent, return None
202
+ if not handoff_data.next_agent:
203
+ logger.info("Router determined no next agent", agent=agent.name)
204
+ span.add_event("no_next_agent_from_router")
205
+ return None
206
+
207
+ logger.info(
208
+ "Handoff determined",
209
+ next_agent=handoff_data.next_agent,
210
+ agent=agent.name,
211
+ )
212
+ span.set_attribute("next_agent", handoff_data.next_agent)
213
+ # Return the serializable HandOffRequest data using Pydantic's export method
214
+ return handoff_data.model_dump(
215
+ mode="json"
216
+ ) # Ensure JSON-serializable
217
+
218
+ except Exception as e:
219
+ # Catch potential errors during routing execution
220
+ logger.error(
221
+ "Router execution failed",
222
+ agent=agent.name,
223
+ error=str(e),
224
+ exc_info=True,
225
+ )
226
+ span.record_exception(e)
227
+ # Let Temporal handle the activity failure based on retry policy
228
+ raise