flock-core 0.5.0b16__py3-none-any.whl → 0.5.0b18__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.

@@ -1,5 +1,10 @@
1
1
  # src/flock/core/mixin/dspy_integration.py
2
- """Mixin class for integrating with the dspy library."""
2
+ """Mixin class for integrating with the dspy library.
3
+
4
+ This mixin centralizes Flock ↔ DSPy interop. It intentionally
5
+ delegates more to DSPy’s native builders (Signature, settings.context,
6
+ modules) to reduce custom glue and stay aligned with DSPy updates.
7
+ """
3
8
 
4
9
  import ast
5
10
  import re # Import re for parsing
@@ -145,114 +150,70 @@ def _resolve_type_string(type_str: str) -> type:
145
150
  class DSPyIntegrationMixin:
146
151
  """Mixin class for integrating with the dspy library."""
147
152
 
148
- def create_dspy_signature_class(
149
- self, agent_name, description_spec, fields_spec
150
- ) -> Any:
151
- """Creates a dynamic DSPy Signature class from string specifications,
152
- resolving types using the registry.
153
+ def create_dspy_signature_class(self, agent_name: str, description_spec: str, fields_spec: str) -> Any:
154
+ """Create a DSPy Signature using DSPy's native builder.
155
+
156
+ We support the Flock spec format: "field: type | description, ... -> ...".
157
+ This converts to the dict-based make_signature format with
158
+ InputField/OutputField and resolved Python types.
153
159
  """
154
160
  try:
155
161
  import dspy
156
- except ImportError:
157
- logger.error(
158
- "DSPy library is not installed. Cannot create DSPy signature. "
159
- "Install with: pip install dspy-ai"
160
- )
161
- raise ImportError("DSPy is required for this functionality.")
162
-
163
- base_class = dspy.Signature
164
- class_dict = {"__doc__": description_spec, "__annotations__": {}}
162
+ except ImportError as exc:
163
+ logger.error("DSPy is not installed. Install with: pip install dspy-ai")
164
+ raise
165
165
 
166
+ # Split input/output part
166
167
  if "->" in fields_spec:
167
168
  inputs_spec, outputs_spec = fields_spec.split("->", 1)
168
169
  else:
169
- inputs_spec, outputs_spec = (
170
- fields_spec,
171
- "",
172
- ) # Assume only inputs if no '->'
170
+ inputs_spec, outputs_spec = fields_spec, ""
173
171
 
174
- def parse_field(field_str):
175
- """Parses 'name: type_str | description' using _resolve_type_string."""
172
+ def parse_field(field_str: str) -> tuple[str, type, str | None] | None:
176
173
  field_str = field_str.strip()
177
174
  if not field_str:
178
175
  return None
179
-
180
176
  parts = field_str.split("|", 1)
181
177
  main_part = parts[0].strip()
182
178
  desc = parts[1].strip() if len(parts) > 1 else None
183
-
184
179
  if ":" in main_part:
185
180
  name, type_str = [s.strip() for s in main_part.split(":", 1)]
186
181
  else:
187
- name = main_part
188
- type_str = "str" # Default type
189
-
182
+ name, type_str = main_part, "str"
190
183
  try:
191
- field_type = _resolve_type_string(type_str)
192
- except Exception as e: # Catch resolution errors
193
- logger.error(
194
- f"Failed to resolve type '{type_str}' for field '{name}': {e}. Defaulting to str."
184
+ py_type = _resolve_type_string(type_str)
185
+ except Exception as e:
186
+ logger.warning(
187
+ f"Type resolution failed for '{type_str}' in field '{name}': {e}. Falling back to str."
195
188
  )
196
- field_type = str
197
-
198
- return name, field_type, desc
199
-
200
- def process_fields(fields_string, field_kind):
201
- """Process fields and add to class_dict."""
202
- if not fields_string or not fields_string.strip():
203
- return
204
-
205
- split_fields = split_top_level(fields_string)
206
- for field in split_fields:
207
- if field.strip():
208
- parsed = parse_field(field)
209
- if not parsed:
210
- continue
211
- name, field_type, desc = parsed
212
- class_dict["__annotations__"][name] = (
213
- field_type # Use resolved type
214
- )
215
-
216
- FieldClass = (
217
- dspy.InputField
218
- if field_kind == "input"
219
- else dspy.OutputField
220
- )
221
- # DSPy Fields use 'desc' for description
222
- class_dict[name] = (
223
- FieldClass(desc=desc)
224
- if desc is not None
225
- else FieldClass()
226
- )
189
+ py_type = str
190
+ return name, py_type, desc
191
+
192
+ def to_field_tuples(spec: str, kind: str) -> dict[str, tuple[type, Any]]:
193
+ mapping: dict[str, tuple[type, Any]] = {}
194
+ if not spec.strip():
195
+ return mapping
196
+ for raw in split_top_level(spec):
197
+ parsed = parse_field(raw)
198
+ if not parsed:
199
+ continue
200
+ fname, ftype, fdesc = parsed
201
+ FieldClass = dspy.InputField if kind == "input" else dspy.OutputField
202
+ finfo = FieldClass(desc=fdesc) if fdesc is not None else FieldClass()
203
+ mapping[fname] = (ftype, finfo)
204
+ return mapping
227
205
 
228
206
  try:
229
- process_fields(inputs_spec, "input")
230
- process_fields(outputs_spec, "output")
231
- except Exception as e:
232
- logger.error(
233
- f"Error processing fields for DSPy signature '{agent_name}': {e}",
234
- exc_info=True,
235
- )
236
- raise ValueError(
237
- f"Could not process fields for signature: {e}"
238
- ) from e
239
-
240
- # Create and return the dynamic class
241
- try:
242
- DynamicSignature = type(
243
- "dspy_" + agent_name, (base_class,), class_dict
244
- )
245
- logger.info(
246
- f"Successfully created DSPy Signature: {DynamicSignature.__name__} "
247
- f"with fields: {DynamicSignature.__annotations__}"
248
- )
249
- return DynamicSignature
250
- except Exception as e:
251
- logger.error(
252
- f"Failed to create dynamic type 'dspy_{agent_name}': {e}",
253
- exc_info=True,
254
- )
255
- raise TypeError(f"Could not create DSPy signature type: {e}") from e
207
+ fields: dict[str, tuple[type, Any]] = {
208
+ **to_field_tuples(inputs_spec, "input"),
209
+ **to_field_tuples(outputs_spec, "output"),
210
+ }
211
+ sig = dspy.Signature(fields, description_spec or None, signature_name=f"dspy_{agent_name}")
212
+ logger.info("Created DSPy Signature %s", sig.__name__)
213
+ return sig
214
+ except Exception as e: # pragma: no cover - defensive
215
+ logger.error("Failed to create DSPy Signature for %s: %s", agent_name, e, exc_info=True)
216
+ raise
256
217
 
257
218
  def _configure_language_model(
258
219
  self,
@@ -275,34 +236,28 @@ class DSPyIntegrationMixin:
275
236
  try:
276
237
  import dspy
277
238
  except ImportError:
278
- logger.error(
279
- "DSPy library is not installed. Cannot configure language model."
280
- )
281
- return # Or raise
239
+ logger.error("DSPy is not installed; cannot configure LM.")
240
+ return
282
241
 
242
+ # Build an LM instance for per-call usage; prefer settings.context over global configure.
283
243
  try:
284
- # Ensure 'cache' parameter is handled correctly (might not exist on dspy.LM directly)
285
- # DSPy handles caching globally or via specific optimizers typically.
286
- # We'll configure the LM without explicit cache control here.
287
244
  lm_instance = dspy.LM(
288
245
  model=model,
289
246
  temperature=temperature,
290
247
  max_tokens=max_tokens,
291
248
  cache=use_cache,
292
- # Add other relevant parameters if needed, e.g., API keys via dspy.settings
293
249
  )
294
- dspy.settings.configure(lm=lm_instance)
250
+ # Do not call settings.configure() here to avoid cross-task/thread conflicts.
251
+ # Callers should pass this LM via dspy.settings.context(lm=...) or program.acall(lm=...)
252
+ dspy.settings # touch to ensure settings is importable
295
253
  logger.info(
296
- f"DSPy LM configured with model: {model}, temp: {temperature}, max_tokens: {max_tokens}"
254
+ "Prepared DSPy LM (defer install to settings.context): model=%s temp=%s max_tokens=%s",
255
+ model,
256
+ temperature,
257
+ max_tokens,
297
258
  )
298
- # Note: DSPy caching is usually configured globally, e.g., dspy.settings.configure(cache=...)
299
- # or handled by optimizers. Setting `cache=use_cache` on dspy.LM might not be standard.
300
259
  except Exception as e:
301
- logger.error(
302
- f"Failed to configure DSPy language model '{model}': {e}",
303
- exc_info=True,
304
- )
305
- # We need to raise this exception, otherwise Flock will trundle on until it needs dspy.settings.lm and can't find it.
260
+ logger.error("Failed to prepare DSPy LM '%s': %s", model, e, exc_info=True)
306
261
  raise
307
262
 
308
263
  def _select_task(
@@ -349,9 +304,17 @@ class DSPyIntegrationMixin:
349
304
 
350
305
  # Determine type if not overridden
351
306
  if not selected_type:
352
- selected_type = (
353
- "ReAct" if processed_tools or processed_mcp_tools else "Predict"
354
- ) # Default logic
307
+ selected_type = "ReAct" if processed_tools or processed_mcp_tools else "Predict"
308
+
309
+ # Normalize common aliases/casing
310
+ sel = selected_type.lower() if isinstance(selected_type, str) else selected_type
311
+ if isinstance(sel, str):
312
+ if sel in {"completion", "predict"}:
313
+ sel = "predict"
314
+ elif sel in {"react"}:
315
+ sel = "react"
316
+ elif sel in {"chainofthought", "cot", "chain_of_thought"}:
317
+ sel = "chain_of_thought"
355
318
 
356
319
  logger.debug(
357
320
  f"Selecting DSPy program type: {selected_type} (Tools provided: {bool(processed_tools)}) (MCP Tools: {bool(processed_mcp_tools)}"
@@ -368,15 +331,15 @@ class DSPyIntegrationMixin:
368
331
  merged_tools = merged_tools + processed_mcp_tools
369
332
 
370
333
  try:
371
- if selected_type == "ChainOfThought":
334
+ if sel == "chain_of_thought":
372
335
  dspy_program = dspy.ChainOfThought(signature, **kwargs)
373
- elif selected_type == "ReAct":
336
+ elif sel == "react":
374
337
  if not kwargs:
375
338
  kwargs = {"max_iters": max_tool_calls}
376
339
  dspy_program = dspy.ReAct(
377
340
  signature, tools=merged_tools or [], **kwargs
378
341
  )
379
- elif selected_type == "Predict": # Default or explicitly Completion
342
+ elif sel == "predict":
380
343
  dspy_program = dspy.Predict(signature)
381
344
  else: # Fallback or handle unknown type
382
345
  logger.warning(
@@ -395,51 +358,46 @@ class DSPyIntegrationMixin:
395
358
  )
396
359
  raise RuntimeError(f"Could not create DSPy program: {e}") from e
397
360
 
398
- def _process_result(
399
- self, result: Any, inputs: dict[str, Any]
400
- ) -> tuple[dict[str, Any], float, list]:
401
- """Convert the DSPy result object to a dictionary."""
402
- import dspy
361
+ def _process_result(self, result: Any, inputs: dict[str, Any]) -> tuple[dict[str, Any], float, list]:
362
+ """Convert a DSPy Prediction or mapping to a plain dict and attach LM history.
363
+
364
+ Returns (result_dict, cost_placeholder, lm_history). The cost is set to 0.0;
365
+ use token usage trackers elsewhere for accurate accounting.
366
+ """
367
+ try:
368
+ import dspy
369
+ except ImportError:
370
+ dspy = None
403
371
 
404
372
  if result is None:
405
373
  logger.warning("DSPy program returned None result.")
406
- return {}
374
+ return {}, 0.0, []
375
+
407
376
  try:
408
- # DSPy Prediction objects often behave like dicts or have .keys() / items()
409
- if hasattr(result, "items") and callable(result.items):
410
- output_dict = dict(result.items())
411
- elif hasattr(result, "__dict__"): # Fallback for other object types
412
- output_dict = {
413
- k: v
414
- for k, v in result.__dict__.items()
415
- if not k.startswith("_")
416
- }
377
+ # Best-effort extraction from DSPy Prediction
378
+ if dspy and isinstance(result, dspy.Prediction):
379
+ output_dict = dict(result.items(include_dspy=False))
380
+ elif isinstance(result, dict):
381
+ output_dict = result
382
+ elif hasattr(result, "items") and callable(result.items):
383
+ try:
384
+ output_dict = dict(result.items())
385
+ except Exception:
386
+ output_dict = {"raw_result": str(result)}
417
387
  else:
418
- # If it's already a dict (less common for DSPy results directly)
419
- if isinstance(result, dict):
420
- output_dict = result
421
- else: # Final fallback
422
- logger.warning(
423
- f"Could not reliably convert DSPy result of type {type(result)} to dict. Returning as is."
424
- )
425
- output_dict = {"raw_result": result}
388
+ output_dict = {"raw_result": str(result)}
426
389
 
427
- logger.debug(f"Processed DSPy result to dict: {output_dict}")
428
- # Optionally merge inputs back if desired (can make result dict large)
429
390
  final_result = {**inputs, **output_dict}
430
391
 
431
- lm = dspy.settings.get("lm")
432
- cost = sum([x["cost"] for x in lm.history if x["cost"] is not None])
433
- lm_history = lm.history
392
+ lm_history = []
393
+ try:
394
+ if dspy and dspy.settings.lm is not None and hasattr(dspy.settings.lm, "history"):
395
+ lm_history = dspy.settings.lm.history
396
+ except Exception:
397
+ lm_history = []
434
398
 
435
- return final_result, cost, lm_history
399
+ return final_result, 0.0, lm_history
436
400
 
437
- except Exception as conv_error:
438
- logger.error(
439
- f"Failed to process DSPy result into dictionary: {conv_error}",
440
- exc_info=True,
441
- )
442
- return {
443
- "error": "Failed to process result",
444
- "raw_result": str(result),
445
- }
401
+ except Exception as conv_error: # pragma: no cover - defensive
402
+ logger.error("Failed to process DSPy result into dictionary: %s", conv_error, exc_info=True)
403
+ return {"error": "Failed to process result", "raw_result": str(result)}, 0.0, []
@@ -124,6 +124,13 @@ class FlockExecution:
124
124
 
125
125
  # Setup execution context and input
126
126
  run_input = input if input is not None else self.flock._start_input
127
+ # Accept Pydantic BaseModel instances as input by converting to dict
128
+ try:
129
+ from pydantic import BaseModel as _BM
130
+ if not isinstance(run_input, dict) and isinstance(run_input, _BM):
131
+ run_input = run_input.model_dump(exclude_none=True) # type: ignore[attr-defined]
132
+ except Exception:
133
+ pass
127
134
  effective_run_id = run_id or f"flockrun_{uuid.uuid4().hex[:8]}"
128
135
 
129
136
  # Set span attributes
@@ -31,6 +31,10 @@ class FlockInitialization:
31
31
  servers: list["FlockMCPServer"] | None = None,
32
32
  ) -> None:
33
33
  """Handle all initialization side effects and setup."""
34
+ # Workaround: newer litellm logging tries to import proxy dependencies (apscheduler)
35
+ # via cold storage logging even for non-proxy usage. Avoid hard dependency by
36
+ # pre-stubbing the `litellm.proxy.proxy_server` module with a minimal object.
37
+ self._patch_litellm_proxy_imports()
34
38
  # Register passed servers first (agents may depend on them)
35
39
  if servers:
36
40
  self._register_servers(servers)
@@ -64,6 +68,26 @@ class FlockInitialization:
64
68
  enable_temporal=self.flock.enable_temporal,
65
69
  )
66
70
 
71
+ def _patch_litellm_proxy_imports(self) -> None:
72
+ """Stub litellm proxy_server to avoid optional proxy deps when not used.
73
+
74
+ Some litellm versions import `litellm.proxy.proxy_server` during standard logging
75
+ to read `general_settings`, which pulls in optional dependencies like `apscheduler`.
76
+ We provide a stub so imports succeed but cold storage remains disabled.
77
+ """
78
+ try:
79
+ import sys
80
+ import types
81
+
82
+ if "litellm.proxy.proxy_server" not in sys.modules:
83
+ stub = types.ModuleType("litellm.proxy.proxy_server")
84
+ # Minimal surface that cold_storage_handler accesses
85
+ setattr(stub, "general_settings", {})
86
+ sys.modules["litellm.proxy.proxy_server"] = stub
87
+ except Exception as e:
88
+ # Safe to ignore; worst case litellm will log a warning
89
+ logger.debug(f"Failed to stub litellm proxy_server: {e}")
90
+
67
91
  def _register_servers(self, servers: list["FlockMCPServer"]) -> None:
68
92
  """Register servers with the Flock instance."""
69
93
  from flock.core.mcp.flock_mcp_server import (
@@ -76,12 +76,14 @@ def _format_type_to_string(type_hint: type) -> str:
76
76
  elif hasattr(type_hint, "__name__"):
77
77
  # Handle custom types registered in registry (get preferred name)
78
78
  registry = get_registry()
79
- for (
80
- name,
81
- reg_type,
82
- ) in registry._types.items(): # Access internal for lookup
83
- if reg_type == type_hint:
84
- return name # Return registered name
79
+ try:
80
+ # Prefer explicit type registration names when available
81
+ for name, reg_type in registry.types.get_all_types().items():
82
+ if reg_type == type_hint:
83
+ return name
84
+ except Exception:
85
+ # Defensive: if registry helper is unavailable, fall back below
86
+ pass
85
87
  return type_hint.__name__ # Fallback to class name if not registered
86
88
  else:
87
89
  # Fallback for complex types or types not handled above
@@ -245,22 +247,20 @@ def serialize_item(item: Any) -> Any:
245
247
  return {key: serialize_item(value) for key, value in item.items()}
246
248
  elif isinstance(item, Sequence) and not isinstance(item, str):
247
249
  return [serialize_item(sub_item) for sub_item in item]
248
- elif isinstance(
249
- item, type
250
- ): # Handle type objects themselves (e.g. if stored directly)
251
- type_name = registry.get_component_type_name(
252
- item
253
- ) # Check components first
250
+ elif isinstance(item, type): # Handle type objects themselves (e.g. if stored directly)
251
+ # Prefer registered component/type names when possible
252
+ type_name = registry.get_component_type_name(item)
254
253
  if type_name:
255
254
  return {"__component_ref__": type_name}
256
- type_name = registry._get_path_string(
257
- item
258
- ) # Check regular types/classes by path
259
- if type_name:
260
- return {"__type_ref__": type_name}
261
- logger.warning(
262
- f"Could not serialize type object {item}, storing as string."
263
- )
255
+ # Fall back to module-qualified path for general types
256
+ try:
257
+ module = getattr(item, "__module__", None)
258
+ name = getattr(item, "__name__", None)
259
+ if module and name:
260
+ return {"__type_ref__": f"{module}.{name}"}
261
+ except Exception:
262
+ pass
263
+ logger.warning(f"Could not serialize type object {item}, storing as string.")
264
264
  return str(item)
265
265
  elif isinstance(item, Enum):
266
266
  return item.value
@@ -23,7 +23,7 @@ registry = get_registry() # Get registry instance once
23
23
 
24
24
 
25
25
  @activity.defn
26
- async def execute_single_agent(agent_name: str, context: FlockContext) -> dict:
26
+ async def execute_single_agent(agent_name: str, context_dict: dict) -> dict:
27
27
  """Executes a single specified agent and returns its result.
28
28
 
29
29
  Args:
@@ -47,7 +47,8 @@ async def execute_single_agent(agent_name: str, context: FlockContext) -> dict:
47
47
  # Raise error for Temporal to potentially retry/fail the activity
48
48
  raise ValueError(f"Agent '{agent_name}' not found in registry.")
49
49
 
50
- # Set agent's context reference (transient, for this execution)
50
+ # Rehydrate context from dict and set on agent (transient for this execution)
51
+ context = FlockContext.from_dict(context_dict)
51
52
  agent.context = context
52
53
 
53
54
  # Ensure model is set (using context value if needed)
@@ -66,13 +67,39 @@ async def execute_single_agent(agent_name: str, context: FlockContext) -> dict:
66
67
  # agent.resolve_callables(context=context)
67
68
 
68
69
  # Resolve inputs for this specific agent run
69
- previous_agent_name = (
70
- context.get_last_agent_name()
71
- ) # Relies on context method
70
+ previous_agent_name = context.get_last_agent_name() # May be None on first agent
71
+ prev_def = (
72
+ context.get_agent_definition(previous_agent_name)
73
+ if previous_agent_name
74
+ else None
75
+ )
76
+ prev_out_spec = (
77
+ (prev_def.agent_data.get("output_spec") if isinstance(prev_def, type(prev_def)) else None)
78
+ if prev_def and isinstance(prev_def.agent_data, dict)
79
+ else None
80
+ )
81
+ prev_cfg = (
82
+ prev_def.agent_data.get("config")
83
+ if prev_def and isinstance(prev_def.agent_data, dict)
84
+ else {}
85
+ )
86
+ prev_strategy = (
87
+ prev_cfg.get("handoff_strategy") if isinstance(prev_cfg, dict) else None
88
+ ) or "static"
89
+ prev_map = (
90
+ prev_cfg.get("handoff_map") if isinstance(prev_cfg, dict) else None
91
+ ) or {}
72
92
  logger.debug(
73
93
  f"Resolving inputs for {agent_name} with previous agent {previous_agent_name}"
74
94
  )
75
- agent_inputs = resolve_inputs(agent.input, context, previous_agent_name)
95
+ agent_inputs = resolve_inputs(
96
+ agent.input,
97
+ context,
98
+ previous_agent_name or "",
99
+ prev_out_spec or "",
100
+ prev_strategy,
101
+ prev_map,
102
+ )
76
103
  span.add_event(
77
104
  "resolved inputs", attributes={"inputs": str(agent_inputs)}
78
105
  )
@@ -96,6 +123,8 @@ async def execute_single_agent(agent_name: str, context: FlockContext) -> dict:
96
123
  error=str(e),
97
124
  exc_info=True,
98
125
  )
126
+ # Debug aid: ensure exception prints even if logger is muted in environment
127
+ print(f"[agent_activity] Single agent execution failed for {agent_name}: {e!r}")
99
128
  span.record_exception(e)
100
129
  # Re-raise the exception for Temporal to handle based on retry policy
101
130
  raise
@@ -103,22 +132,11 @@ async def execute_single_agent(agent_name: str, context: FlockContext) -> dict:
103
132
 
104
133
  @activity.defn
105
134
  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.
135
+ current_agent_name: str, result: dict, context_dict: dict
136
+ ) -> str | None:
137
+ """Determine the next agent using the agent's routing component.
118
138
 
119
- Raises:
120
- ValueError: If the current agent cannot be found.
121
- Exception: Propagates exceptions from router execution for Temporal retries.
139
+ Returns the next agent's name or None if the workflow should terminate.
122
140
  """
123
141
  with tracer.start_as_current_span("determine_next_agent") as span:
124
142
  span.set_attribute("agent.name", current_agent_name)
@@ -145,75 +163,30 @@ async def determine_next_agent(
145
163
  agent=agent.name,
146
164
  )
147
165
  try:
148
- # Execute the routing logic
149
- handoff_data: (
150
- HandOffRequest | Callable
151
- ) = await agent.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:
166
+ # Execute routing logic on the router component (unified architecture)
167
+ context = FlockContext.from_dict(context_dict)
168
+ next_val = await agent.router.determine_next_step(agent, result, context)
169
+
170
+ # Convert to a simple agent name if needed
171
+ if isinstance(next_val, FlockAgent):
172
+ next_name = next_val.name
173
+ elif isinstance(next_val, str):
174
+ next_name = next_val
175
+ else:
176
+ next_name = None
177
+
178
+ if not next_name:
203
179
  logger.info("Router determined no next agent", agent=agent.name)
204
180
  span.add_event("no_next_agent_from_router")
205
181
  return None
206
182
 
207
183
  logger.info(
208
- "Handoff determined",
209
- next_agent=handoff_data.next_agent,
184
+ "Next agent determined",
185
+ next_agent=next_name,
210
186
  agent=agent.name,
211
187
  )
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
188
+ span.set_attribute("next_agent", next_name)
189
+ return next_name
217
190
 
218
191
  except Exception as e:
219
192
  # Catch potential errors during routing execution
@@ -223,6 +196,7 @@ async def determine_next_agent(
223
196
  error=str(e),
224
197
  exc_info=True,
225
198
  )
199
+ print(f"[agent_activity] Router execution failed for {agent.name}: {e!r}")
226
200
  span.record_exception(e)
227
201
  # Let Temporal handle the activity failure based on retry policy
228
202
  raise