agno 2.2.5__py3-none-any.whl → 2.2.7__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.
Files changed (57) hide show
  1. agno/agent/agent.py +500 -423
  2. agno/api/os.py +1 -1
  3. agno/culture/manager.py +12 -8
  4. agno/guardrails/prompt_injection.py +1 -0
  5. agno/knowledge/chunking/agentic.py +6 -2
  6. agno/knowledge/embedder/vllm.py +262 -0
  7. agno/knowledge/knowledge.py +37 -5
  8. agno/memory/manager.py +9 -4
  9. agno/models/anthropic/claude.py +1 -2
  10. agno/models/azure/ai_foundry.py +31 -14
  11. agno/models/azure/openai_chat.py +12 -4
  12. agno/models/base.py +106 -65
  13. agno/models/cerebras/cerebras.py +11 -6
  14. agno/models/groq/groq.py +7 -4
  15. agno/models/meta/llama.py +12 -6
  16. agno/models/meta/llama_openai.py +5 -1
  17. agno/models/openai/chat.py +26 -17
  18. agno/models/openai/responses.py +11 -63
  19. agno/models/requesty/requesty.py +5 -2
  20. agno/models/utils.py +254 -8
  21. agno/models/vertexai/claude.py +9 -13
  22. agno/os/app.py +13 -12
  23. agno/os/routers/evals/evals.py +8 -8
  24. agno/os/routers/evals/utils.py +1 -0
  25. agno/os/schema.py +56 -38
  26. agno/os/utils.py +27 -0
  27. agno/run/__init__.py +6 -0
  28. agno/run/agent.py +5 -0
  29. agno/run/base.py +18 -1
  30. agno/run/team.py +13 -9
  31. agno/run/workflow.py +39 -0
  32. agno/session/summary.py +8 -2
  33. agno/session/workflow.py +4 -3
  34. agno/team/team.py +302 -369
  35. agno/tools/exa.py +21 -16
  36. agno/tools/file.py +153 -25
  37. agno/tools/function.py +98 -17
  38. agno/tools/mcp/mcp.py +8 -1
  39. agno/tools/notion.py +204 -0
  40. agno/utils/agent.py +78 -0
  41. agno/utils/events.py +2 -0
  42. agno/utils/hooks.py +1 -1
  43. agno/utils/models/claude.py +25 -8
  44. agno/utils/print_response/workflow.py +115 -16
  45. agno/vectordb/__init__.py +2 -1
  46. agno/vectordb/milvus/milvus.py +5 -0
  47. agno/vectordb/redis/__init__.py +5 -0
  48. agno/vectordb/redis/redisdb.py +687 -0
  49. agno/workflow/__init__.py +2 -0
  50. agno/workflow/agent.py +299 -0
  51. agno/workflow/step.py +13 -2
  52. agno/workflow/workflow.py +969 -72
  53. {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/METADATA +10 -3
  54. {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/RECORD +57 -52
  55. {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/WHEEL +0 -0
  56. {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/licenses/LICENSE +0 -0
  57. {agno-2.2.5.dist-info → agno-2.2.7.dist-info}/top_level.txt +0 -0
agno/workflow/workflow.py CHANGED
@@ -29,8 +29,8 @@ from agno.exceptions import InputCheckError, OutputCheckError, RunCancelledExcep
29
29
  from agno.media import Audio, File, Image, Video
30
30
  from agno.models.message import Message
31
31
  from agno.models.metrics import Metrics
32
- from agno.run.agent import RunContentEvent, RunEvent
33
- from agno.run.base import RunStatus
32
+ from agno.run import RunContext, RunStatus
33
+ from agno.run.agent import RunContentEvent, RunEvent, RunOutput
34
34
  from agno.run.cancel import (
35
35
  cancel_run as cancel_run_global,
36
36
  )
@@ -68,6 +68,7 @@ from agno.utils.print_response.workflow import (
68
68
  print_response,
69
69
  print_response_stream,
70
70
  )
71
+ from agno.workflow import WorkflowAgent
71
72
  from agno.workflow.condition import Condition
72
73
  from agno.workflow.loop import Loop
73
74
  from agno.workflow.parallel import Parallel
@@ -132,6 +133,9 @@ class Workflow:
132
133
  # Database to use for this workflow
133
134
  db: Optional[Union[BaseDb, AsyncBaseDb]] = None
134
135
 
136
+ # Agentic Workflow - WorkflowAgent that decides when to run the workflow
137
+ agent: Optional[WorkflowAgent] = None # type: ignore
138
+
135
139
  # Default session_id to use for this workflow (autogenerated if not set)
136
140
  session_id: Optional[str] = None
137
141
  # Default user_id to use for this workflow
@@ -187,6 +191,7 @@ class Workflow:
187
191
  description: Optional[str] = None,
188
192
  db: Optional[Union[BaseDb, AsyncBaseDb]] = None,
189
193
  steps: Optional[WorkflowSteps] = None,
194
+ agent: Optional[WorkflowAgent] = None,
190
195
  session_id: Optional[str] = None,
191
196
  session_state: Optional[Dict[str, Any]] = None,
192
197
  overwrite_db_session_state: bool = False,
@@ -210,6 +215,7 @@ class Workflow:
210
215
  self.name = name
211
216
  self.description = description
212
217
  self.steps = steps
218
+ self.agent = agent
213
219
  self.session_id = session_id
214
220
  self.session_state = session_state
215
221
  self.overwrite_db_session_state = overwrite_db_session_state
@@ -231,6 +237,13 @@ class Workflow:
231
237
  self.num_history_runs = num_history_runs
232
238
  self._workflow_session: Optional[WorkflowSession] = None
233
239
 
240
+ # Warn if workflow history is enabled without a database
241
+ if self.add_workflow_history_to_steps and self.db is None:
242
+ log_warning(
243
+ "Workflow history is enabled (add_workflow_history_to_steps=True) but no database is configured. "
244
+ "History won't be persisted. Add a database to persist runs across executions. "
245
+ )
246
+
234
247
  def set_id(self) -> None:
235
248
  if self.id is None:
236
249
  if self.name is not None:
@@ -940,6 +953,20 @@ class Workflow:
940
953
 
941
954
  return workflow_data
942
955
 
956
+ def _broadcast_to_websocket(
957
+ self,
958
+ event: Any,
959
+ websocket_handler: Optional[WebSocketHandler] = None,
960
+ ) -> None:
961
+ """Broadcast events to WebSocket if available (async context only)"""
962
+ if websocket_handler:
963
+ try:
964
+ loop = asyncio.get_running_loop()
965
+ if loop:
966
+ asyncio.create_task(websocket_handler.handle_event(event))
967
+ except RuntimeError:
968
+ pass
969
+
943
970
  def _handle_event(
944
971
  self,
945
972
  event: "WorkflowRunOutputEvent",
@@ -947,6 +974,11 @@ class Workflow:
947
974
  websocket_handler: Optional[WebSocketHandler] = None,
948
975
  ) -> "WorkflowRunOutputEvent":
949
976
  """Handle workflow events for storage - similar to Team._handle_event"""
977
+ from agno.run.agent import RunOutput
978
+ from agno.run.team import TeamRunOutput
979
+
980
+ if isinstance(event, (RunOutput, TeamRunOutput)):
981
+ return event
950
982
  if self.store_events:
951
983
  # Check if this event type should be skipped
952
984
  if self.events_to_skip:
@@ -967,15 +999,7 @@ class Workflow:
967
999
  workflow_run_response.events.append(event)
968
1000
 
969
1001
  # Broadcast to WebSocket if available (async context only)
970
- if websocket_handler:
971
- import asyncio
972
-
973
- try:
974
- loop = asyncio.get_running_loop()
975
- if loop:
976
- asyncio.create_task(websocket_handler.handle_event(event))
977
- except RuntimeError:
978
- pass
1002
+ self._broadcast_to_websocket(event, websocket_handler)
979
1003
 
980
1004
  return event
981
1005
 
@@ -1192,7 +1216,7 @@ class Workflow:
1192
1216
  session: WorkflowSession,
1193
1217
  execution_input: WorkflowExecutionInput,
1194
1218
  workflow_run_response: WorkflowRunOutput,
1195
- session_state: Optional[Dict[str, Any]] = None,
1219
+ run_context: RunContext,
1196
1220
  **kwargs: Any,
1197
1221
  ) -> WorkflowRunOutput:
1198
1222
  """Execute a specific pipeline by name synchronously"""
@@ -1259,7 +1283,7 @@ class Workflow:
1259
1283
  session_id=session.session_id,
1260
1284
  user_id=self.user_id,
1261
1285
  workflow_run_response=workflow_run_response,
1262
- session_state=session_state,
1286
+ session_state=run_context.session_state,
1263
1287
  store_executor_outputs=self.store_executor_outputs,
1264
1288
  workflow_session=session,
1265
1289
  add_workflow_history_to_steps=self.add_workflow_history_to_steps
@@ -1353,7 +1377,7 @@ class Workflow:
1353
1377
  session: WorkflowSession,
1354
1378
  execution_input: WorkflowExecutionInput,
1355
1379
  workflow_run_response: WorkflowRunOutput,
1356
- session_state: Optional[Dict[str, Any]] = None,
1380
+ run_context: RunContext,
1357
1381
  stream_events: bool = False,
1358
1382
  **kwargs: Any,
1359
1383
  ) -> Iterator[WorkflowRunOutputEvent]:
@@ -1444,7 +1468,7 @@ class Workflow:
1444
1468
  stream_events=stream_events,
1445
1469
  stream_executor_events=self.stream_executor_events,
1446
1470
  workflow_run_response=workflow_run_response,
1447
- session_state=session_state,
1471
+ session_state=run_context.session_state,
1448
1472
  step_index=i,
1449
1473
  store_executor_outputs=self.store_executor_outputs,
1450
1474
  workflow_session=session,
@@ -2243,6 +2267,13 @@ class Workflow:
2243
2267
  session_id=session_id, user_id=user_id, session_state=session_state
2244
2268
  )
2245
2269
 
2270
+ run_context = RunContext(
2271
+ run_id=run_id,
2272
+ session_id=session_id,
2273
+ user_id=user_id,
2274
+ session_state=session_state,
2275
+ )
2276
+
2246
2277
  self._prepare_steps()
2247
2278
 
2248
2279
  # Create workflow run response with PENDING status
@@ -2285,14 +2316,23 @@ class Workflow:
2285
2316
  else:
2286
2317
  self.save_session(session=workflow_session)
2287
2318
 
2288
- await self._aexecute(
2289
- session_id=session_id,
2290
- user_id=user_id,
2291
- execution_input=inputs,
2292
- workflow_run_response=workflow_run_response,
2293
- session_state=session_state,
2294
- **kwargs,
2295
- )
2319
+ if self.agent is not None:
2320
+ self._aexecute_workflow_agent(
2321
+ user_input=input, # type: ignore
2322
+ execution_input=inputs,
2323
+ run_context=run_context,
2324
+ stream=False,
2325
+ **kwargs,
2326
+ )
2327
+ else:
2328
+ await self._aexecute(
2329
+ session_id=session_id,
2330
+ user_id=user_id,
2331
+ execution_input=inputs,
2332
+ workflow_run_response=workflow_run_response,
2333
+ session_state=session_state,
2334
+ **kwargs,
2335
+ )
2296
2336
 
2297
2337
  log_debug(f"Background execution completed with status: {workflow_run_response.status}")
2298
2338
 
@@ -2340,6 +2380,13 @@ class Workflow:
2340
2380
  session_id=session_id, user_id=user_id, session_state=session_state
2341
2381
  )
2342
2382
 
2383
+ run_context = RunContext(
2384
+ run_id=run_id,
2385
+ session_id=session_id,
2386
+ user_id=user_id,
2387
+ session_state=session_state,
2388
+ )
2389
+
2343
2390
  self._prepare_steps()
2344
2391
 
2345
2392
  # Create workflow run response with PENDING status
@@ -2353,13 +2400,6 @@ class Workflow:
2353
2400
  status=RunStatus.pending,
2354
2401
  )
2355
2402
 
2356
- # Store PENDING response immediately
2357
- workflow_session.upsert_run(run=workflow_run_response)
2358
- if self._has_async_db():
2359
- await self.asave_session(session=workflow_session)
2360
- else:
2361
- self.save_session(session=workflow_session)
2362
-
2363
2403
  # Prepare execution input
2364
2404
  inputs = WorkflowExecutionInput(
2365
2405
  input=input,
@@ -2375,27 +2415,45 @@ class Workflow:
2375
2415
  async def execute_workflow_background_stream():
2376
2416
  """Background execution with streaming and WebSocket broadcasting"""
2377
2417
  try:
2378
- # Update status to RUNNING and save
2379
- workflow_run_response.status = RunStatus.running
2380
- if self._has_async_db():
2381
- await self.asave_session(session=workflow_session)
2418
+ if self.agent is not None:
2419
+ result = self._aexecute_workflow_agent(
2420
+ user_input=input, # type: ignore
2421
+ run_context=run_context,
2422
+ execution_input=inputs,
2423
+ stream=True,
2424
+ websocket_handler=websocket_handler,
2425
+ **kwargs,
2426
+ )
2427
+ # For streaming, result is an async iterator
2428
+ async for event in result: # type: ignore
2429
+ # Events are automatically broadcast by _handle_event in the agent execution
2430
+ # We just consume them here to drive the execution
2431
+ pass
2432
+ log_debug(
2433
+ f"Background streaming execution (workflow agent) completed with status: {workflow_run_response.status}"
2434
+ )
2382
2435
  else:
2383
- self.save_session(session=workflow_session)
2436
+ # Update status to RUNNING and save
2437
+ workflow_run_response.status = RunStatus.running
2438
+ if self._has_async_db():
2439
+ await self.asave_session(session=workflow_session)
2440
+ else:
2441
+ self.save_session(session=workflow_session)
2384
2442
 
2385
- # Execute with streaming - consume all events (they're auto-broadcast via _handle_event)
2386
- async for event in self._aexecute_stream(
2387
- session_id=session_id,
2388
- user_id=user_id,
2389
- execution_input=inputs,
2390
- workflow_run_response=workflow_run_response,
2391
- stream_events=stream_events,
2392
- session_state=session_state,
2393
- websocket_handler=websocket_handler,
2394
- **kwargs,
2395
- ):
2396
- # Events are automatically broadcast by _handle_event
2397
- # We just consume them here to drive the execution
2398
- pass
2443
+ # Execute with streaming - consume all events (they're auto-broadcast via _handle_event)
2444
+ async for event in self._aexecute_stream(
2445
+ session_id=session_id,
2446
+ user_id=user_id,
2447
+ execution_input=inputs,
2448
+ workflow_run_response=workflow_run_response,
2449
+ stream_events=stream_events,
2450
+ run_context=run_context,
2451
+ websocket_handler=websocket_handler,
2452
+ **kwargs,
2453
+ ):
2454
+ # Events are automatically broadcast by _handle_event
2455
+ # We just consume them here to drive the execution
2456
+ pass
2399
2457
 
2400
2458
  log_debug(f"Background streaming execution completed with status: {workflow_run_response.status}")
2401
2459
 
@@ -2439,6 +2497,801 @@ class Workflow:
2439
2497
 
2440
2498
  return None
2441
2499
 
2500
+ def _initialize_workflow_agent(
2501
+ self,
2502
+ session: WorkflowSession,
2503
+ execution_input: WorkflowExecutionInput,
2504
+ run_context: RunContext,
2505
+ stream: bool = False,
2506
+ ) -> None:
2507
+ """Initialize the workflow agent with tools (but NOT context - that's passed per-run)"""
2508
+ from agno.tools.function import Function
2509
+
2510
+ workflow_tool_func = self.agent.create_workflow_tool( # type: ignore
2511
+ workflow=self,
2512
+ session=session,
2513
+ execution_input=execution_input,
2514
+ run_context=run_context,
2515
+ stream=stream,
2516
+ )
2517
+ workflow_tool = Function.from_callable(workflow_tool_func)
2518
+
2519
+ self.agent.tools = [workflow_tool] # type: ignore
2520
+ self.agent._rebuild_tools = True # type: ignore
2521
+
2522
+ log_debug("Workflow agent initialized with run_workflow tool")
2523
+
2524
+ def _get_workflow_agent_dependencies(self, session: WorkflowSession) -> Dict[str, Any]:
2525
+ """Build dependencies dict with workflow context to pass to agent.run()"""
2526
+ # Get configuration from the WorkflowAgent instance
2527
+ add_history = True
2528
+ num_runs = 5
2529
+
2530
+ if self.agent and isinstance(self.agent, WorkflowAgent):
2531
+ add_history = self.agent.add_workflow_history
2532
+ num_runs = self.agent.num_history_runs or 5
2533
+
2534
+ if add_history:
2535
+ history_context = (
2536
+ session.get_workflow_history_context(num_runs=num_runs) or "No previous workflow runs in this session."
2537
+ )
2538
+ else:
2539
+ history_context = "No workflow history available."
2540
+
2541
+ # Build workflow context with description and history
2542
+ workflow_context = ""
2543
+ if self.description:
2544
+ workflow_context += f"Workflow Description: {self.description}\n\n"
2545
+
2546
+ workflow_context += history_context
2547
+
2548
+ return {
2549
+ "workflow_context": workflow_context,
2550
+ }
2551
+
2552
+ def _execute_workflow_agent(
2553
+ self,
2554
+ user_input: Union[str, Dict[str, Any], List[Any], BaseModel],
2555
+ session: WorkflowSession,
2556
+ execution_input: WorkflowExecutionInput,
2557
+ run_context: RunContext,
2558
+ stream: bool = False,
2559
+ **kwargs: Any,
2560
+ ) -> Union[WorkflowRunOutput, Iterator[WorkflowRunOutputEvent]]:
2561
+ """
2562
+ Execute the workflow agent in streaming or non-streaming mode.
2563
+
2564
+ The agent decides whether to run the workflow or answer directly from history.
2565
+
2566
+ Args:
2567
+ user_input: The user's input
2568
+ session: The workflow session
2569
+ execution_input: The execution input
2570
+ run_context: The run context
2571
+ stream: Whether to stream the response
2572
+ stream_intermediate_steps: Whether to stream intermediate steps
2573
+
2574
+ Returns:
2575
+ WorkflowRunOutput if stream=False, Iterator[WorkflowRunOutputEvent] if stream=True
2576
+ """
2577
+ if stream:
2578
+ return self._run_workflow_agent_stream(
2579
+ agent_input=user_input,
2580
+ session=session,
2581
+ execution_input=execution_input,
2582
+ run_context=run_context,
2583
+ stream=stream,
2584
+ **kwargs,
2585
+ )
2586
+ else:
2587
+ return self._run_workflow_agent(
2588
+ agent_input=user_input,
2589
+ session=session,
2590
+ execution_input=execution_input,
2591
+ run_context=run_context,
2592
+ stream=stream,
2593
+ )
2594
+
2595
+ def _run_workflow_agent_stream(
2596
+ self,
2597
+ agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
2598
+ session: WorkflowSession,
2599
+ execution_input: WorkflowExecutionInput,
2600
+ run_context: RunContext,
2601
+ stream: bool = False,
2602
+ **kwargs: Any,
2603
+ ) -> Iterator[WorkflowRunOutputEvent]:
2604
+ """
2605
+ Execute the workflow agent in streaming mode.
2606
+
2607
+ The agent's tool (run_workflow) is a generator that yields workflow events directly.
2608
+ These events bubble up through the agent's streaming and are yielded here.
2609
+ We filter to only yield WorkflowRunOutputEvent to the CLI.
2610
+
2611
+ Yields:
2612
+ WorkflowRunOutputEvent: Events from workflow execution (agent events are filtered)
2613
+ """
2614
+ from typing import get_args
2615
+
2616
+ from agno.run.workflow import WorkflowCompletedEvent, WorkflowRunOutputEvent
2617
+
2618
+ # Initialize agent with stream_intermediate_steps=True so tool yields events
2619
+ self._initialize_workflow_agent(session, execution_input, run_context=run_context, stream=stream)
2620
+
2621
+ # Build dependencies with workflow context
2622
+ run_context.dependencies = self._get_workflow_agent_dependencies(session)
2623
+
2624
+ # Run agent with streaming - workflow events will bubble up from the tool
2625
+ agent_response: Optional[RunOutput] = None
2626
+ workflow_executed = False
2627
+
2628
+ from agno.run.agent import RunContentEvent
2629
+ from agno.run.team import RunContentEvent as TeamRunContentEvent
2630
+ from agno.run.workflow import WorkflowAgentCompletedEvent, WorkflowAgentStartedEvent
2631
+
2632
+ log_debug(f"Executing workflow agent with streaming - input: {agent_input}...")
2633
+
2634
+ # Create a workflow run response upfront for potential direct answer (will be used only if workflow is not executed)
2635
+ run_id = str(uuid4())
2636
+ direct_reply_run_response = WorkflowRunOutput(
2637
+ run_id=run_id,
2638
+ input=execution_input.input,
2639
+ session_id=session.session_id,
2640
+ workflow_id=self.id,
2641
+ workflow_name=self.name,
2642
+ created_at=int(datetime.now().timestamp()),
2643
+ )
2644
+
2645
+ # Yield WorkflowAgentStartedEvent at the beginning (stored in direct_reply_run_response)
2646
+ agent_started_event = WorkflowAgentStartedEvent(
2647
+ workflow_name=self.name,
2648
+ workflow_id=self.id,
2649
+ session_id=session.session_id,
2650
+ )
2651
+ yield agent_started_event
2652
+
2653
+ # Run the agent in streaming mode and yield all events
2654
+ for event in self.agent.run( # type: ignore[union-attr]
2655
+ input=agent_input,
2656
+ stream=True,
2657
+ stream_intermediate_steps=True,
2658
+ yield_run_response=True,
2659
+ session_id=session.session_id,
2660
+ dependencies=run_context.dependencies, # Pass context dynamically per-run
2661
+ session_state=run_context.session_state, # Pass session state dynamically per-run
2662
+ ): # type: ignore
2663
+ if isinstance(event, tuple(get_args(WorkflowRunOutputEvent))):
2664
+ yield event # type: ignore[misc]
2665
+
2666
+ # Track if workflow was executed by checking for WorkflowCompletedEvent
2667
+ if isinstance(event, WorkflowCompletedEvent):
2668
+ workflow_executed = True
2669
+ elif isinstance(event, (RunContentEvent, TeamRunContentEvent)):
2670
+ if event.step_name is None:
2671
+ # This is from the workflow agent itself
2672
+ # Enrich with metadata to mark it as a workflow agent event
2673
+
2674
+ if workflow_executed:
2675
+ continue # Skip if workflow was already executed
2676
+
2677
+ # workflow_agent field is used by consumers of the events to distinguish between workflow agent and regular agent
2678
+ event.workflow_agent = True # type: ignore
2679
+ yield event # type: ignore[misc]
2680
+
2681
+ # Capture the final RunOutput (but don't yield it)
2682
+ if isinstance(event, RunOutput):
2683
+ agent_response = event
2684
+
2685
+ # Handle direct answer case (no workflow execution)
2686
+ if not workflow_executed:
2687
+ # Update the pre-created workflow run response with the direct answer
2688
+ direct_reply_run_response.content = agent_response.content if agent_response else ""
2689
+ direct_reply_run_response.status = RunStatus.completed
2690
+ direct_reply_run_response.workflow_agent_run = agent_response
2691
+
2692
+ workflow_run_response = direct_reply_run_response
2693
+
2694
+ # Store the full agent RunOutput and establish parent-child relationship
2695
+ if agent_response:
2696
+ agent_response.parent_run_id = workflow_run_response.run_id
2697
+ agent_response.workflow_id = workflow_run_response.workflow_id
2698
+
2699
+ log_debug(f"Agent decision: workflow_executed={workflow_executed}")
2700
+
2701
+ # Yield WorkflowAgentCompletedEvent (user internally by print_response_stream)
2702
+ agent_completed_event = WorkflowAgentCompletedEvent(
2703
+ run_id=agent_response.run_id if agent_response else None,
2704
+ workflow_name=self.name,
2705
+ workflow_id=self.id,
2706
+ session_id=session.session_id,
2707
+ content=workflow_run_response.content,
2708
+ )
2709
+ yield agent_completed_event
2710
+
2711
+ # Yield a workflow completed event with the agent's direct response
2712
+ completed_event = WorkflowCompletedEvent(
2713
+ run_id=workflow_run_response.run_id or "",
2714
+ content=workflow_run_response.content,
2715
+ workflow_name=workflow_run_response.workflow_name,
2716
+ workflow_id=workflow_run_response.workflow_id,
2717
+ session_id=workflow_run_response.session_id,
2718
+ step_results=[],
2719
+ metadata={"agent_direct_response": True},
2720
+ )
2721
+ yield completed_event
2722
+
2723
+ # Update the run in session
2724
+ session.upsert_run(run=workflow_run_response)
2725
+ # Save session
2726
+ self.save_session(session=session)
2727
+
2728
+ else:
2729
+ # Workflow was executed by the tool
2730
+ reloaded_session = self.get_session(session_id=session.session_id)
2731
+
2732
+ if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
2733
+ # Get the last run (which is the one just created by the tool)
2734
+ last_run = reloaded_session.runs[-1]
2735
+
2736
+ # Yield WorkflowAgentCompletedEvent
2737
+ agent_completed_event = WorkflowAgentCompletedEvent(
2738
+ run_id=agent_response.run_id if agent_response else None,
2739
+ workflow_name=self.name,
2740
+ workflow_id=self.id,
2741
+ session_id=session.session_id,
2742
+ content=agent_response.content if agent_response else None,
2743
+ )
2744
+ yield agent_completed_event
2745
+
2746
+ # Update the last run with workflow_agent_run
2747
+ last_run.workflow_agent_run = agent_response
2748
+
2749
+ # Store the full agent RunOutput and establish parent-child relationship
2750
+ if agent_response:
2751
+ agent_response.parent_run_id = last_run.run_id
2752
+ agent_response.workflow_id = last_run.workflow_id
2753
+
2754
+ # Save the reloaded session (which has the updated run)
2755
+ self.save_session(session=reloaded_session)
2756
+
2757
+ else:
2758
+ log_warning("Could not reload session or no runs found after workflow execution")
2759
+
2760
+ def _run_workflow_agent(
2761
+ self,
2762
+ agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
2763
+ session: WorkflowSession,
2764
+ execution_input: WorkflowExecutionInput,
2765
+ run_context: RunContext,
2766
+ stream: bool = False,
2767
+ ) -> WorkflowRunOutput:
2768
+ """
2769
+ Execute the workflow agent in non-streaming mode.
2770
+
2771
+ The agent decides whether to run the workflow or answer directly from history.
2772
+
2773
+ Returns:
2774
+ WorkflowRunOutput: The workflow run output with agent response
2775
+ """
2776
+
2777
+ # Initialize the agent
2778
+ self._initialize_workflow_agent(session, execution_input, run_context=run_context, stream=stream)
2779
+
2780
+ # Build dependencies with workflow context
2781
+ run_context.dependencies = self._get_workflow_agent_dependencies(session)
2782
+
2783
+ # Run the agent
2784
+ agent_response: RunOutput = self.agent.run( # type: ignore[union-attr]
2785
+ input=agent_input,
2786
+ session_id=session.session_id,
2787
+ dependencies=run_context.dependencies,
2788
+ session_state=run_context.session_state,
2789
+ stream=stream,
2790
+ ) # type: ignore
2791
+
2792
+ # Check if the agent called the workflow tool
2793
+ workflow_executed = False
2794
+ if agent_response.messages:
2795
+ for message in agent_response.messages:
2796
+ if message.role == "assistant" and message.tool_calls:
2797
+ # Check if the tool call is specifically for run_workflow
2798
+ for tool_call in message.tool_calls:
2799
+ # Handle both dict and object formats
2800
+ if isinstance(tool_call, dict):
2801
+ tool_name = tool_call.get("function", {}).get("name", "")
2802
+ else:
2803
+ tool_name = tool_call.function.name if hasattr(tool_call, "function") else ""
2804
+
2805
+ if tool_name == "run_workflow":
2806
+ workflow_executed = True
2807
+ break
2808
+ if workflow_executed:
2809
+ break
2810
+
2811
+ log_debug(f"Workflow agent execution complete. Workflow executed: {workflow_executed}")
2812
+
2813
+ # Handle direct answer case (no workflow execution)
2814
+ if not workflow_executed:
2815
+ # Create a new workflow run output for the direct answer
2816
+ run_id = str(uuid4())
2817
+ workflow_run_response = WorkflowRunOutput(
2818
+ run_id=run_id,
2819
+ input=execution_input.input,
2820
+ session_id=session.session_id,
2821
+ workflow_id=self.id,
2822
+ workflow_name=self.name,
2823
+ created_at=int(datetime.now().timestamp()),
2824
+ content=agent_response.content,
2825
+ status=RunStatus.completed,
2826
+ workflow_agent_run=agent_response,
2827
+ )
2828
+
2829
+ # Store the full agent RunOutput and establish parent-child relationship
2830
+ if agent_response:
2831
+ agent_response.parent_run_id = workflow_run_response.run_id
2832
+ agent_response.workflow_id = workflow_run_response.workflow_id
2833
+
2834
+ # Update the run in session
2835
+ session.upsert_run(run=workflow_run_response)
2836
+ self.save_session(session=session)
2837
+
2838
+ log_debug(f"Agent decision: workflow_executed={workflow_executed}")
2839
+
2840
+ return workflow_run_response
2841
+ else:
2842
+ # Workflow was executed by the tool
2843
+ reloaded_session = self.get_session(session_id=session.session_id)
2844
+
2845
+ if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
2846
+ # Get the last run (which is the one just created by the tool)
2847
+ last_run = reloaded_session.runs[-1]
2848
+
2849
+ # Update the last run directly with workflow_agent_run
2850
+ last_run.workflow_agent_run = agent_response
2851
+
2852
+ # Store the full agent RunOutput and establish parent-child relationship
2853
+ if agent_response:
2854
+ agent_response.parent_run_id = last_run.run_id
2855
+ agent_response.workflow_id = last_run.workflow_id
2856
+
2857
+ # Save the reloaded session (which has the updated run)
2858
+ self.save_session(session=reloaded_session)
2859
+
2860
+ # Return the last run directly (WRO2 from inner workflow)
2861
+ return last_run
2862
+ else:
2863
+ log_warning("Could not reload session or no runs found after workflow execution")
2864
+ # Return a placeholder error response
2865
+ return WorkflowRunOutput(
2866
+ run_id=str(uuid4()),
2867
+ input=execution_input.input,
2868
+ session_id=session.session_id,
2869
+ workflow_id=self.id,
2870
+ workflow_name=self.name,
2871
+ created_at=int(datetime.now().timestamp()),
2872
+ content="Error: Workflow execution failed",
2873
+ status=RunStatus.error,
2874
+ )
2875
+
2876
+ def _async_initialize_workflow_agent(
2877
+ self,
2878
+ session: WorkflowSession,
2879
+ execution_input: WorkflowExecutionInput,
2880
+ run_context: RunContext,
2881
+ websocket_handler: Optional[WebSocketHandler] = None,
2882
+ stream: bool = False,
2883
+ ) -> None:
2884
+ """Initialize the workflow agent with async tools (but NOT context - that's passed per-run)"""
2885
+ from agno.tools.function import Function
2886
+
2887
+ workflow_tool_func = self.agent.async_create_workflow_tool( # type: ignore
2888
+ workflow=self,
2889
+ session=session,
2890
+ execution_input=execution_input,
2891
+ run_context=run_context,
2892
+ stream=stream,
2893
+ websocket_handler=websocket_handler,
2894
+ )
2895
+ workflow_tool = Function.from_callable(workflow_tool_func)
2896
+
2897
+ self.agent.tools = [workflow_tool] # type: ignore
2898
+ self.agent._rebuild_tools = True # type: ignore
2899
+
2900
+ log_debug("Workflow agent initialized with async run_workflow tool")
2901
+
2902
+ async def _aload_session_for_workflow_agent(
2903
+ self,
2904
+ session_id: str,
2905
+ user_id: Optional[str],
2906
+ session_state: Optional[Dict[str, Any]],
2907
+ ) -> Tuple[WorkflowSession, Dict[str, Any]]:
2908
+ """Helper to load or create session for workflow agent execution"""
2909
+ return await self._aload_or_create_session(session_id=session_id, user_id=user_id, session_state=session_state)
2910
+
2911
+ def _aexecute_workflow_agent(
2912
+ self,
2913
+ user_input: Union[str, Dict[str, Any], List[Any], BaseModel],
2914
+ run_context: RunContext,
2915
+ execution_input: WorkflowExecutionInput,
2916
+ stream: bool = False,
2917
+ websocket_handler: Optional[WebSocketHandler] = None,
2918
+ **kwargs: Any,
2919
+ ):
2920
+ """
2921
+ Execute the workflow agent asynchronously in streaming or non-streaming mode.
2922
+
2923
+ The agent decides whether to run the workflow or answer directly from history.
2924
+
2925
+ Args:
2926
+ user_input: The user's input
2927
+ session: The workflow session
2928
+ run_context: The run context
2929
+ execution_input: The execution input
2930
+ stream: Whether to stream the response
2931
+ websocket_handler: The WebSocket handler
2932
+
2933
+ Returns:
2934
+ Coroutine[WorkflowRunOutput] if stream=False, AsyncIterator[WorkflowRunOutputEvent] if stream=True
2935
+ """
2936
+
2937
+ if stream:
2938
+
2939
+ async def _stream():
2940
+ session, session_state_loaded = await self._aload_session_for_workflow_agent(
2941
+ run_context.session_id, run_context.user_id, run_context.session_state
2942
+ )
2943
+ async for event in self._arun_workflow_agent_stream(
2944
+ agent_input=user_input,
2945
+ session=session,
2946
+ execution_input=execution_input,
2947
+ run_context=run_context,
2948
+ stream=stream,
2949
+ websocket_handler=websocket_handler,
2950
+ **kwargs,
2951
+ ):
2952
+ yield event
2953
+
2954
+ return _stream()
2955
+ else:
2956
+
2957
+ async def _execute():
2958
+ session, session_state_loaded = await self._aload_session_for_workflow_agent(
2959
+ run_context.session_id, run_context.user_id, run_context.session_state
2960
+ )
2961
+ return await self._arun_workflow_agent(
2962
+ agent_input=user_input,
2963
+ session=session,
2964
+ execution_input=execution_input,
2965
+ run_context=run_context,
2966
+ stream=stream,
2967
+ )
2968
+
2969
+ return _execute()
2970
+
2971
+ async def _arun_workflow_agent_stream(
2972
+ self,
2973
+ agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
2974
+ session: WorkflowSession,
2975
+ execution_input: WorkflowExecutionInput,
2976
+ run_context: RunContext,
2977
+ stream: bool = False,
2978
+ websocket_handler: Optional[WebSocketHandler] = None,
2979
+ **kwargs: Any,
2980
+ ) -> AsyncIterator[WorkflowRunOutputEvent]:
2981
+ """
2982
+ Execute the workflow agent asynchronously in streaming mode.
2983
+
2984
+ The agent's tool (run_workflow) is an async generator that yields workflow events directly.
2985
+ These events bubble up through the agent's streaming and are yielded here.
2986
+ We filter to only yield WorkflowRunOutputEvent to the CLI.
2987
+
2988
+ Yields:
2989
+ WorkflowRunOutputEvent: Events from workflow execution (agent events are filtered)
2990
+ """
2991
+ from typing import get_args
2992
+
2993
+ from agno.run.workflow import WorkflowCompletedEvent, WorkflowRunOutputEvent
2994
+
2995
+ logger.info("Workflow agent enabled - async streaming mode")
2996
+ log_debug(f"User input: {agent_input}")
2997
+
2998
+ self._async_initialize_workflow_agent(
2999
+ session,
3000
+ execution_input,
3001
+ run_context=run_context,
3002
+ stream=stream,
3003
+ websocket_handler=websocket_handler,
3004
+ )
3005
+
3006
+ run_context.dependencies = self._get_workflow_agent_dependencies(session)
3007
+
3008
+ agent_response: Optional[RunOutput] = None
3009
+ workflow_executed = False
3010
+
3011
+ from agno.run.agent import RunContentEvent
3012
+ from agno.run.team import RunContentEvent as TeamRunContentEvent
3013
+ from agno.run.workflow import WorkflowAgentCompletedEvent, WorkflowAgentStartedEvent
3014
+
3015
+ log_debug(f"Executing async workflow agent with streaming - input: {agent_input}...")
3016
+
3017
+ # Create a workflow run response upfront for potential direct answer (will be used only if workflow is not executed)
3018
+ run_id = str(uuid4())
3019
+ direct_reply_run_response = WorkflowRunOutput(
3020
+ run_id=run_id,
3021
+ input=execution_input.input,
3022
+ session_id=session.session_id,
3023
+ workflow_id=self.id,
3024
+ workflow_name=self.name,
3025
+ created_at=int(datetime.now().timestamp()),
3026
+ )
3027
+
3028
+ # Yield WorkflowAgentStartedEvent at the beginning (stored in direct_reply_run_response)
3029
+ agent_started_event = WorkflowAgentStartedEvent(
3030
+ workflow_name=self.name,
3031
+ workflow_id=self.id,
3032
+ session_id=session.session_id,
3033
+ )
3034
+ self._broadcast_to_websocket(agent_started_event, websocket_handler)
3035
+ yield agent_started_event
3036
+
3037
+ # Run the agent in streaming mode and yield all events
3038
+ async for event in self.agent.arun( # type: ignore[union-attr]
3039
+ input=agent_input,
3040
+ stream=True,
3041
+ stream_intermediate_steps=True,
3042
+ yield_run_response=True,
3043
+ session_id=session.session_id,
3044
+ dependencies=run_context.dependencies, # Pass context dynamically per-run
3045
+ session_state=run_context.session_state, # Pass session state dynamically per-run
3046
+ ): # type: ignore
3047
+ if isinstance(event, tuple(get_args(WorkflowRunOutputEvent))):
3048
+ yield event # type: ignore[misc]
3049
+
3050
+ if isinstance(event, WorkflowCompletedEvent):
3051
+ workflow_executed = True
3052
+ log_debug("Workflow execution detected via WorkflowCompletedEvent")
3053
+
3054
+ elif isinstance(event, (RunContentEvent, TeamRunContentEvent)):
3055
+ if event.step_name is None:
3056
+ # This is from the workflow agent itself
3057
+ # Enrich with metadata to mark it as a workflow agent event
3058
+
3059
+ if workflow_executed:
3060
+ continue # Skip if workflow was already executed
3061
+
3062
+ # workflow_agent field is used by consumers of the events to distinguish between workflow agent and regular agent
3063
+ event.workflow_agent = True # type: ignore
3064
+
3065
+ # Broadcast to WebSocket if available (async context only)
3066
+ self._broadcast_to_websocket(event, websocket_handler)
3067
+
3068
+ yield event # type: ignore[misc]
3069
+
3070
+ # Capture the final RunOutput (but don't yield it)
3071
+ if isinstance(event, RunOutput):
3072
+ agent_response = event
3073
+ log_debug(
3074
+ f"Agent response: {str(agent_response.content)[:100] if agent_response.content else 'None'}..."
3075
+ )
3076
+
3077
+ # Handle direct answer case (no workflow execution)
3078
+ if not workflow_executed:
3079
+ # Update the pre-created workflow run response with the direct answer
3080
+ direct_reply_run_response.content = agent_response.content if agent_response else ""
3081
+ direct_reply_run_response.status = RunStatus.completed
3082
+ direct_reply_run_response.workflow_agent_run = agent_response
3083
+
3084
+ workflow_run_response = direct_reply_run_response
3085
+
3086
+ # Store the full agent RunOutput and establish parent-child relationship
3087
+ if agent_response:
3088
+ agent_response.parent_run_id = workflow_run_response.run_id
3089
+ agent_response.workflow_id = workflow_run_response.workflow_id
3090
+
3091
+ # Yield WorkflowAgentCompletedEvent
3092
+ agent_completed_event = WorkflowAgentCompletedEvent(
3093
+ workflow_name=self.name,
3094
+ workflow_id=self.id,
3095
+ run_id=agent_response.run_id if agent_response else None,
3096
+ session_id=session.session_id,
3097
+ content=workflow_run_response.content,
3098
+ )
3099
+ self._broadcast_to_websocket(agent_completed_event, websocket_handler)
3100
+ yield agent_completed_event
3101
+
3102
+ # Yield a workflow completed event with the agent's direct response (user internally by aprint_response_stream)
3103
+ completed_event = WorkflowCompletedEvent(
3104
+ run_id=workflow_run_response.run_id or "",
3105
+ content=workflow_run_response.content,
3106
+ workflow_name=workflow_run_response.workflow_name,
3107
+ workflow_id=workflow_run_response.workflow_id,
3108
+ session_id=workflow_run_response.session_id,
3109
+ step_results=[],
3110
+ metadata={"agent_direct_response": True},
3111
+ )
3112
+ yield completed_event
3113
+
3114
+ # Update the run in session
3115
+ session.upsert_run(run=workflow_run_response)
3116
+ # Save session
3117
+ if self._has_async_db():
3118
+ await self.asave_session(session=session)
3119
+ else:
3120
+ self.save_session(session=session)
3121
+
3122
+ else:
3123
+ # Workflow was executed by the tool
3124
+ if self._has_async_db():
3125
+ reloaded_session = await self.aget_session(session_id=session.session_id)
3126
+ else:
3127
+ reloaded_session = self.get_session(session_id=session.session_id)
3128
+
3129
+ if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
3130
+ # Get the last run (which is the one just created by the tool)
3131
+ last_run = reloaded_session.runs[-1]
3132
+
3133
+ # Yield WorkflowAgentCompletedEvent
3134
+ agent_completed_event = WorkflowAgentCompletedEvent(
3135
+ run_id=agent_response.run_id if agent_response else None,
3136
+ workflow_name=self.name,
3137
+ workflow_id=self.id,
3138
+ session_id=session.session_id,
3139
+ content=agent_response.content if agent_response else None,
3140
+ )
3141
+
3142
+ self._broadcast_to_websocket(agent_completed_event, websocket_handler)
3143
+
3144
+ yield agent_completed_event
3145
+
3146
+ # Update the last run with workflow_agent_run
3147
+ last_run.workflow_agent_run = agent_response
3148
+
3149
+ # Store the full agent RunOutput and establish parent-child relationship
3150
+ if agent_response:
3151
+ agent_response.parent_run_id = last_run.run_id
3152
+ agent_response.workflow_id = last_run.workflow_id
3153
+
3154
+ # Save the reloaded session (which has the updated run)
3155
+ if self._has_async_db():
3156
+ await self.asave_session(session=reloaded_session)
3157
+ else:
3158
+ self.save_session(session=reloaded_session)
3159
+
3160
+ else:
3161
+ log_warning("Could not reload session or no runs found after workflow execution")
3162
+
3163
+ async def _arun_workflow_agent(
3164
+ self,
3165
+ agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
3166
+ session: WorkflowSession,
3167
+ execution_input: WorkflowExecutionInput,
3168
+ run_context: RunContext,
3169
+ stream: bool = False,
3170
+ ) -> WorkflowRunOutput:
3171
+ """
3172
+ Execute the workflow agent asynchronously in non-streaming mode.
3173
+
3174
+ The agent decides whether to run the workflow or answer directly from history.
3175
+
3176
+ Returns:
3177
+ WorkflowRunOutput: The workflow run output with agent response
3178
+ """
3179
+ # Initialize the agent
3180
+ self._async_initialize_workflow_agent(session, execution_input, run_context=run_context, stream=stream)
3181
+
3182
+ # Build dependencies with workflow context
3183
+ run_context.dependencies = self._get_workflow_agent_dependencies(session)
3184
+
3185
+ # Run the agent
3186
+ agent_response: RunOutput = await self.agent.arun( # type: ignore[union-attr]
3187
+ input=agent_input,
3188
+ session_id=session.session_id,
3189
+ dependencies=run_context.dependencies,
3190
+ session_state=run_context.session_state,
3191
+ stream=stream,
3192
+ ) # type: ignore
3193
+
3194
+ # Check if the agent called the workflow tool
3195
+ workflow_executed = False
3196
+ if agent_response.messages:
3197
+ for message in agent_response.messages:
3198
+ if message.role == "assistant" and message.tool_calls:
3199
+ # Check if the tool call is specifically for run_workflow
3200
+ for tool_call in message.tool_calls:
3201
+ # Handle both dict and object formats
3202
+ if isinstance(tool_call, dict):
3203
+ tool_name = tool_call.get("function", {}).get("name", "")
3204
+ else:
3205
+ tool_name = tool_call.function.name if hasattr(tool_call, "function") else ""
3206
+
3207
+ if tool_name == "run_workflow":
3208
+ workflow_executed = True
3209
+ break
3210
+ if workflow_executed:
3211
+ break
3212
+
3213
+ # Handle direct answer case (no workflow execution)
3214
+ if not workflow_executed:
3215
+ # Create a new workflow run output for the direct answer
3216
+ run_id = str(uuid4())
3217
+ workflow_run_response = WorkflowRunOutput(
3218
+ run_id=run_id,
3219
+ input=execution_input.input,
3220
+ session_id=session.session_id,
3221
+ workflow_id=self.id,
3222
+ workflow_name=self.name,
3223
+ created_at=int(datetime.now().timestamp()),
3224
+ content=agent_response.content,
3225
+ status=RunStatus.completed,
3226
+ workflow_agent_run=agent_response,
3227
+ )
3228
+
3229
+ # Store the full agent RunOutput and establish parent-child relationship
3230
+ if agent_response:
3231
+ agent_response.parent_run_id = workflow_run_response.run_id
3232
+ agent_response.workflow_id = workflow_run_response.workflow_id
3233
+
3234
+ # Update the run in session
3235
+ session.upsert_run(run=workflow_run_response)
3236
+ if self._has_async_db():
3237
+ await self.asave_session(session=session)
3238
+ else:
3239
+ self.save_session(session=session)
3240
+
3241
+ log_debug(f"Agent decision: workflow_executed={workflow_executed}")
3242
+
3243
+ return workflow_run_response
3244
+ else:
3245
+ # Workflow was executed by the tool
3246
+ logger.info("=" * 80)
3247
+ logger.info("WORKFLOW AGENT: Called run_workflow tool (async)")
3248
+ logger.info(" ➜ Workflow was executed, retrieving results...")
3249
+ logger.info("=" * 80)
3250
+
3251
+ log_debug("Reloading session from database to get the latest workflow run...")
3252
+ if self._has_async_db():
3253
+ reloaded_session = await self.aget_session(session_id=session.session_id)
3254
+ else:
3255
+ reloaded_session = self.get_session(session_id=session.session_id)
3256
+
3257
+ if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
3258
+ # Get the last run (which is the one just created by the tool)
3259
+ last_run = reloaded_session.runs[-1]
3260
+ log_debug(f"Retrieved latest workflow run: {last_run.run_id}")
3261
+ log_debug(f"Total workflow runs in session: {len(reloaded_session.runs)}")
3262
+
3263
+ # Update the last run with workflow_agent_run
3264
+ last_run.workflow_agent_run = agent_response
3265
+
3266
+ # Store the full agent RunOutput and establish parent-child relationship
3267
+ if agent_response:
3268
+ agent_response.parent_run_id = last_run.run_id
3269
+ agent_response.workflow_id = last_run.workflow_id
3270
+
3271
+ # Save the reloaded session (which has the updated run)
3272
+ if self._has_async_db():
3273
+ await self.asave_session(session=reloaded_session)
3274
+ else:
3275
+ self.save_session(session=reloaded_session)
3276
+
3277
+ log_debug(f"Agent decision: workflow_executed={workflow_executed}")
3278
+
3279
+ # Return the last run directly (WRO2 from inner workflow)
3280
+ return last_run
3281
+ else:
3282
+ log_warning("Could not reload session or no runs found after workflow execution")
3283
+ # Return a placeholder error response
3284
+ return WorkflowRunOutput(
3285
+ run_id=str(uuid4()),
3286
+ input=execution_input.input,
3287
+ session_id=session.session_id,
3288
+ workflow_id=self.id,
3289
+ workflow_name=self.name,
3290
+ created_at=int(datetime.now().timestamp()),
3291
+ content="Error: Workflow execution failed",
3292
+ status=RunStatus.error,
3293
+ )
3294
+
2442
3295
  def cancel_run(self, run_id: str) -> bool:
2443
3296
  """Cancel a running workflow execution.
2444
3297
 
@@ -2529,6 +3382,14 @@ class Workflow:
2529
3382
  # Update session state from DB
2530
3383
  session_state = self._load_session_state(session=workflow_session, session_state=session_state)
2531
3384
 
3385
+ # Initialize run context
3386
+ run_context = RunContext(
3387
+ run_id=run_id,
3388
+ session_id=session_id,
3389
+ user_id=user_id,
3390
+ session_state=session_state,
3391
+ )
3392
+
2532
3393
  log_debug(f"Workflow Run Start: {self.name}", center=True)
2533
3394
 
2534
3395
  # Use simple defaults
@@ -2547,16 +3408,6 @@ class Workflow:
2547
3408
  # Prepare steps
2548
3409
  self._prepare_steps()
2549
3410
 
2550
- # Create workflow run response that will be updated by reference
2551
- workflow_run_response = WorkflowRunOutput(
2552
- run_id=run_id,
2553
- input=input,
2554
- session_id=session_id,
2555
- workflow_id=self.id,
2556
- workflow_name=self.name,
2557
- created_at=int(datetime.now().timestamp()),
2558
- )
2559
-
2560
3411
  inputs = WorkflowExecutionInput(
2561
3412
  input=input,
2562
3413
  additional_data=additional_data,
@@ -2571,13 +3422,34 @@ class Workflow:
2571
3422
 
2572
3423
  self.update_agents_and_teams_session_info()
2573
3424
 
3425
+ # Execute workflow agent if configured
3426
+ if self.agent is not None:
3427
+ return self._execute_workflow_agent(
3428
+ user_input=input, # type: ignore
3429
+ session=workflow_session,
3430
+ execution_input=inputs,
3431
+ run_context=run_context,
3432
+ stream=stream,
3433
+ **kwargs,
3434
+ )
3435
+
3436
+ # Create workflow run response for regular workflow execution
3437
+ workflow_run_response = WorkflowRunOutput(
3438
+ run_id=run_id,
3439
+ input=input,
3440
+ session_id=session_id,
3441
+ workflow_id=self.id,
3442
+ workflow_name=self.name,
3443
+ created_at=int(datetime.now().timestamp()),
3444
+ )
3445
+
2574
3446
  if stream:
2575
3447
  return self._execute_stream(
2576
3448
  session=workflow_session,
2577
3449
  execution_input=inputs, # type: ignore[arg-type]
2578
3450
  workflow_run_response=workflow_run_response,
2579
3451
  stream_events=stream_events,
2580
- session_state=session_state,
3452
+ run_context=run_context,
2581
3453
  **kwargs,
2582
3454
  )
2583
3455
  else:
@@ -2585,7 +3457,7 @@ class Workflow:
2585
3457
  session=workflow_session,
2586
3458
  execution_input=inputs, # type: ignore[arg-type]
2587
3459
  workflow_run_response=workflow_run_response,
2588
- session_state=session_state,
3460
+ run_context=run_context,
2589
3461
  **kwargs,
2590
3462
  )
2591
3463
 
@@ -2700,6 +3572,14 @@ class Workflow:
2700
3572
  self.initialize_workflow()
2701
3573
  session_id, user_id = self._initialize_session(session_id=session_id, user_id=user_id)
2702
3574
 
3575
+ # Initialize run context
3576
+ run_context = RunContext(
3577
+ run_id=run_id,
3578
+ session_id=session_id,
3579
+ user_id=user_id,
3580
+ session_state=session_state,
3581
+ )
3582
+
2703
3583
  log_debug(f"Async Workflow Run Start: {self.name}", center=True)
2704
3584
 
2705
3585
  # Use simple defaults
@@ -2717,16 +3597,6 @@ class Workflow:
2717
3597
  # Prepare steps
2718
3598
  self._prepare_steps()
2719
3599
 
2720
- # Create workflow run response that will be updated by reference
2721
- workflow_run_response = WorkflowRunOutput(
2722
- run_id=run_id,
2723
- input=input,
2724
- session_id=session_id,
2725
- workflow_id=self.id,
2726
- workflow_name=self.name,
2727
- created_at=int(datetime.now().timestamp()),
2728
- )
2729
-
2730
3600
  inputs = WorkflowExecutionInput(
2731
3601
  input=input,
2732
3602
  additional_data=additional_data,
@@ -2741,6 +3611,25 @@ class Workflow:
2741
3611
 
2742
3612
  self.update_agents_and_teams_session_info()
2743
3613
 
3614
+ if self.agent is not None:
3615
+ return self._aexecute_workflow_agent( # type: ignore
3616
+ user_input=input, # type: ignore
3617
+ execution_input=inputs,
3618
+ run_context=run_context,
3619
+ stream=stream,
3620
+ **kwargs,
3621
+ )
3622
+
3623
+ # Create workflow run response for regular workflow execution
3624
+ workflow_run_response = WorkflowRunOutput(
3625
+ run_id=run_id,
3626
+ input=input,
3627
+ session_id=session_id,
3628
+ workflow_id=self.id,
3629
+ workflow_name=self.name,
3630
+ created_at=int(datetime.now().timestamp()),
3631
+ )
3632
+
2744
3633
  if stream:
2745
3634
  return self._aexecute_stream( # type: ignore
2746
3635
  execution_input=inputs,
@@ -2782,6 +3671,12 @@ class Workflow:
2782
3671
  step_name = step.name or f"step_{i + 1}"
2783
3672
  log_debug(f"Step {i + 1}: Team '{step_name}' with {len(step.members)} members")
2784
3673
  prepared_steps.append(Step(name=step_name, description=step.description, team=step))
3674
+ elif isinstance(step, Step) and step.add_workflow_history is True and self.db is None:
3675
+ log_warning(
3676
+ f"Step '{step.name or f'step_{i + 1}'}' has add_workflow_history=True "
3677
+ "but no database is configured in the Workflow. "
3678
+ "History won't be persisted. Add a database to persist runs across executions."
3679
+ )
2785
3680
  elif isinstance(step, (Step, Steps, Loop, Parallel, Condition, Router)):
2786
3681
  step_type = type(step).__name__
2787
3682
  step_name = getattr(step, "name", f"unnamed_{step_type.lower()}")
@@ -2822,6 +3717,7 @@ class Workflow:
2822
3717
  audio: Audio input
2823
3718
  images: Image input
2824
3719
  videos: Video input
3720
+ files: File input
2825
3721
  stream: Whether to stream the response content
2826
3722
  stream_events: Whether to stream intermediate steps
2827
3723
  markdown: Whether to render content as markdown
@@ -2915,6 +3811,7 @@ class Workflow:
2915
3811
  audio: Audio input
2916
3812
  images: Image input
2917
3813
  videos: Video input
3814
+ files: Files input
2918
3815
  stream: Whether to stream the response content
2919
3816
  stream_events: Whether to stream intermediate steps
2920
3817
  markdown: Whether to render content as markdown
@@ -3021,7 +3918,7 @@ class Workflow:
3021
3918
  step_dict["team"] = step.team if hasattr(step, "team") else None # type: ignore
3022
3919
 
3023
3920
  # Handle nested steps for Router/Loop
3024
- if isinstance(step, (Router)):
3921
+ if isinstance(step, Router):
3025
3922
  step_dict["steps"] = (
3026
3923
  [serialize_step(step) for step in step.choices] if hasattr(step, "choices") else None
3027
3924
  )