agno 2.2.5__py3-none-any.whl → 2.2.6__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.
agno/workflow/workflow.py CHANGED
@@ -29,7 +29,7 @@ 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
32
+ from agno.run.agent import RunContentEvent, RunEvent, RunOutput
33
33
  from agno.run.base import RunStatus
34
34
  from agno.run.cancel import (
35
35
  cancel_run as cancel_run_global,
@@ -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
 
@@ -2285,14 +2309,25 @@ class Workflow:
2285
2309
  else:
2286
2310
  self.save_session(session=workflow_session)
2287
2311
 
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
- )
2312
+ if self.agent is not None:
2313
+ self._aexecute_workflow_agent(
2314
+ user_input=input, # type: ignore
2315
+ session_id=session_id,
2316
+ user_id=user_id,
2317
+ execution_input=inputs,
2318
+ session_state=session_state,
2319
+ stream=False,
2320
+ **kwargs,
2321
+ )
2322
+ else:
2323
+ await self._aexecute(
2324
+ session_id=session_id,
2325
+ user_id=user_id,
2326
+ execution_input=inputs,
2327
+ workflow_run_response=workflow_run_response,
2328
+ session_state=session_state,
2329
+ **kwargs,
2330
+ )
2296
2331
 
2297
2332
  log_debug(f"Background execution completed with status: {workflow_run_response.status}")
2298
2333
 
@@ -2353,13 +2388,6 @@ class Workflow:
2353
2388
  status=RunStatus.pending,
2354
2389
  )
2355
2390
 
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
2391
  # Prepare execution input
2364
2392
  inputs = WorkflowExecutionInput(
2365
2393
  input=input,
@@ -2375,27 +2403,47 @@ class Workflow:
2375
2403
  async def execute_workflow_background_stream():
2376
2404
  """Background execution with streaming and WebSocket broadcasting"""
2377
2405
  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)
2406
+ if self.agent is not None:
2407
+ result = self._aexecute_workflow_agent(
2408
+ user_input=input, # type: ignore
2409
+ session_id=session_id,
2410
+ user_id=user_id,
2411
+ execution_input=inputs,
2412
+ session_state=session_state,
2413
+ stream=True,
2414
+ websocket_handler=websocket_handler,
2415
+ **kwargs,
2416
+ )
2417
+ # For streaming, result is an async iterator
2418
+ async for event in result: # type: ignore
2419
+ # Events are automatically broadcast by _handle_event in the agent execution
2420
+ # We just consume them here to drive the execution
2421
+ pass
2422
+ log_debug(
2423
+ f"Background streaming execution (workflow agent) completed with status: {workflow_run_response.status}"
2424
+ )
2382
2425
  else:
2383
- self.save_session(session=workflow_session)
2426
+ # Update status to RUNNING and save
2427
+ workflow_run_response.status = RunStatus.running
2428
+ if self._has_async_db():
2429
+ await self.asave_session(session=workflow_session)
2430
+ else:
2431
+ self.save_session(session=workflow_session)
2384
2432
 
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
2433
+ # Execute with streaming - consume all events (they're auto-broadcast via _handle_event)
2434
+ async for event in self._aexecute_stream(
2435
+ session_id=session_id,
2436
+ user_id=user_id,
2437
+ execution_input=inputs,
2438
+ workflow_run_response=workflow_run_response,
2439
+ stream_events=stream_events,
2440
+ session_state=session_state,
2441
+ websocket_handler=websocket_handler,
2442
+ **kwargs,
2443
+ ):
2444
+ # Events are automatically broadcast by _handle_event
2445
+ # We just consume them here to drive the execution
2446
+ pass
2399
2447
 
2400
2448
  log_debug(f"Background streaming execution completed with status: {workflow_run_response.status}")
2401
2449
 
@@ -2439,6 +2487,795 @@ class Workflow:
2439
2487
 
2440
2488
  return None
2441
2489
 
2490
+ def _initialize_workflow_agent(
2491
+ self,
2492
+ session: WorkflowSession,
2493
+ execution_input: WorkflowExecutionInput,
2494
+ session_state: Optional[Dict[str, Any]],
2495
+ stream: bool = False,
2496
+ ) -> None:
2497
+ """Initialize the workflow agent with tools (but NOT context - that's passed per-run)"""
2498
+ from agno.tools.function import Function
2499
+
2500
+ workflow_tool_func = self.agent.create_workflow_tool( # type: ignore
2501
+ workflow=self,
2502
+ session=session,
2503
+ execution_input=execution_input,
2504
+ session_state=session_state,
2505
+ stream=stream,
2506
+ )
2507
+ workflow_tool = Function.from_callable(workflow_tool_func)
2508
+
2509
+ self.agent.tools = [workflow_tool] # type: ignore
2510
+ self.agent._rebuild_tools = True # type: ignore
2511
+
2512
+ log_debug("Workflow agent initialized with run_workflow tool")
2513
+
2514
+ def _get_workflow_agent_dependencies(self, session: WorkflowSession) -> Dict[str, Any]:
2515
+ """Build dependencies dict with workflow context to pass to agent.run()"""
2516
+ # Get configuration from the WorkflowAgent instance
2517
+ add_history = True
2518
+ num_runs = 5
2519
+
2520
+ if self.agent and isinstance(self.agent, WorkflowAgent):
2521
+ add_history = self.agent.add_workflow_history
2522
+ num_runs = self.agent.num_history_runs or 5
2523
+
2524
+ if add_history:
2525
+ history_context = (
2526
+ session.get_workflow_history_context(num_runs=num_runs) or "No previous workflow runs in this session."
2527
+ )
2528
+ else:
2529
+ history_context = "No workflow history available."
2530
+
2531
+ # Build workflow context with description and history
2532
+ workflow_context = ""
2533
+ if self.description:
2534
+ workflow_context += f"Workflow Description: {self.description}\n\n"
2535
+
2536
+ workflow_context += history_context
2537
+
2538
+ return {
2539
+ "workflow_context": workflow_context,
2540
+ }
2541
+
2542
+ def _execute_workflow_agent(
2543
+ self,
2544
+ user_input: Union[str, Dict[str, Any], List[Any], BaseModel],
2545
+ session: WorkflowSession,
2546
+ execution_input: WorkflowExecutionInput,
2547
+ session_state: Optional[Dict[str, Any]],
2548
+ stream: bool = False,
2549
+ **kwargs: Any,
2550
+ ) -> Union[WorkflowRunOutput, Iterator[WorkflowRunOutputEvent]]:
2551
+ """
2552
+ Execute the workflow agent in streaming or non-streaming mode.
2553
+
2554
+ The agent decides whether to run the workflow or answer directly from history.
2555
+
2556
+ Args:
2557
+ user_input: The user's input
2558
+ session: The workflow session
2559
+ execution_input: The execution input
2560
+ session_state: The session state
2561
+ stream: Whether to stream the response
2562
+ stream_intermediate_steps: Whether to stream intermediate steps
2563
+
2564
+ Returns:
2565
+ WorkflowRunOutput if stream=False, Iterator[WorkflowRunOutputEvent] if stream=True
2566
+ """
2567
+ if stream:
2568
+ return self._run_workflow_agent_stream(
2569
+ agent_input=user_input,
2570
+ session=session,
2571
+ execution_input=execution_input,
2572
+ session_state=session_state,
2573
+ stream=stream,
2574
+ **kwargs,
2575
+ )
2576
+ else:
2577
+ return self._run_workflow_agent(
2578
+ agent_input=user_input,
2579
+ session=session,
2580
+ execution_input=execution_input,
2581
+ session_state=session_state,
2582
+ stream=stream,
2583
+ )
2584
+
2585
+ def _run_workflow_agent_stream(
2586
+ self,
2587
+ agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
2588
+ session: WorkflowSession,
2589
+ execution_input: WorkflowExecutionInput,
2590
+ session_state: Optional[Dict[str, Any]],
2591
+ stream: bool = False,
2592
+ **kwargs: Any,
2593
+ ) -> Iterator[WorkflowRunOutputEvent]:
2594
+ """
2595
+ Execute the workflow agent in streaming mode.
2596
+
2597
+ The agent's tool (run_workflow) is a generator that yields workflow events directly.
2598
+ These events bubble up through the agent's streaming and are yielded here.
2599
+ We filter to only yield WorkflowRunOutputEvent to the CLI.
2600
+
2601
+ Yields:
2602
+ WorkflowRunOutputEvent: Events from workflow execution (agent events are filtered)
2603
+ """
2604
+ from typing import get_args
2605
+
2606
+ from agno.run.workflow import WorkflowCompletedEvent, WorkflowRunOutputEvent
2607
+
2608
+ # Initialize agent with stream_intermediate_steps=True so tool yields events
2609
+ self._initialize_workflow_agent(session, execution_input, session_state, stream=stream)
2610
+
2611
+ # Build dependencies with workflow context
2612
+ dependencies = self._get_workflow_agent_dependencies(session)
2613
+
2614
+ # Run agent with streaming - workflow events will bubble up from the tool
2615
+ agent_response: Optional[RunOutput] = None
2616
+ workflow_executed = False
2617
+
2618
+ from agno.run.agent import RunContentEvent
2619
+ from agno.run.team import RunContentEvent as TeamRunContentEvent
2620
+ from agno.run.workflow import WorkflowAgentCompletedEvent, WorkflowAgentStartedEvent
2621
+
2622
+ log_debug(f"Executing workflow agent with streaming - input: {agent_input}...")
2623
+
2624
+ # Create a workflow run response upfront for potential direct answer (will be used only if workflow is not executed)
2625
+ run_id = str(uuid4())
2626
+ direct_reply_run_response = WorkflowRunOutput(
2627
+ run_id=run_id,
2628
+ input=execution_input.input,
2629
+ session_id=session.session_id,
2630
+ workflow_id=self.id,
2631
+ workflow_name=self.name,
2632
+ created_at=int(datetime.now().timestamp()),
2633
+ )
2634
+
2635
+ # Yield WorkflowAgentStartedEvent at the beginning (stored in direct_reply_run_response)
2636
+ agent_started_event = WorkflowAgentStartedEvent(
2637
+ workflow_name=self.name,
2638
+ workflow_id=self.id,
2639
+ session_id=session.session_id,
2640
+ )
2641
+ yield agent_started_event
2642
+
2643
+ # Run the agent in streaming mode and yield all events
2644
+ for event in self.agent.run( # type: ignore[union-attr]
2645
+ input=agent_input,
2646
+ stream=True,
2647
+ stream_intermediate_steps=True,
2648
+ yield_run_response=True,
2649
+ session_id=session.session_id,
2650
+ dependencies=dependencies, # Pass context dynamically per-run
2651
+ ): # type: ignore
2652
+ if isinstance(event, tuple(get_args(WorkflowRunOutputEvent))):
2653
+ yield event # type: ignore[misc]
2654
+
2655
+ # Track if workflow was executed by checking for WorkflowCompletedEvent
2656
+ if isinstance(event, WorkflowCompletedEvent):
2657
+ workflow_executed = True
2658
+ elif isinstance(event, (RunContentEvent, TeamRunContentEvent)):
2659
+ if event.step_name is None:
2660
+ # This is from the workflow agent itself
2661
+ # Enrich with metadata to mark it as a workflow agent event
2662
+
2663
+ if workflow_executed:
2664
+ continue # Skip if workflow was already executed
2665
+
2666
+ # workflow_agent field is used by consumers of the events to distinguish between workflow agent and regular agent
2667
+ event.workflow_agent = True # type: ignore
2668
+ yield event # type: ignore[misc]
2669
+
2670
+ # Capture the final RunOutput (but don't yield it)
2671
+ if isinstance(event, RunOutput):
2672
+ agent_response = event
2673
+
2674
+ # Handle direct answer case (no workflow execution)
2675
+ if not workflow_executed:
2676
+ # Update the pre-created workflow run response with the direct answer
2677
+ direct_reply_run_response.content = agent_response.content if agent_response else ""
2678
+ direct_reply_run_response.status = RunStatus.completed
2679
+ direct_reply_run_response.workflow_agent_run = agent_response
2680
+
2681
+ workflow_run_response = direct_reply_run_response
2682
+
2683
+ # Store the full agent RunOutput and establish parent-child relationship
2684
+ if agent_response:
2685
+ agent_response.parent_run_id = workflow_run_response.run_id
2686
+ agent_response.workflow_id = workflow_run_response.workflow_id
2687
+
2688
+ log_debug(f"Agent decision: workflow_executed={workflow_executed}")
2689
+
2690
+ # Yield WorkflowAgentCompletedEvent (user internally by print_response_stream)
2691
+ agent_completed_event = WorkflowAgentCompletedEvent(
2692
+ run_id=agent_response.run_id if agent_response else None,
2693
+ workflow_name=self.name,
2694
+ workflow_id=self.id,
2695
+ session_id=session.session_id,
2696
+ content=workflow_run_response.content,
2697
+ )
2698
+ yield agent_completed_event
2699
+
2700
+ # Yield a workflow completed event with the agent's direct response
2701
+ completed_event = WorkflowCompletedEvent(
2702
+ run_id=workflow_run_response.run_id or "",
2703
+ content=workflow_run_response.content,
2704
+ workflow_name=workflow_run_response.workflow_name,
2705
+ workflow_id=workflow_run_response.workflow_id,
2706
+ session_id=workflow_run_response.session_id,
2707
+ step_results=[],
2708
+ metadata={"agent_direct_response": True},
2709
+ )
2710
+ yield completed_event
2711
+
2712
+ # Update the run in session
2713
+ session.upsert_run(run=workflow_run_response)
2714
+ # Save session
2715
+ self.save_session(session=session)
2716
+
2717
+ else:
2718
+ # Workflow was executed by the tool
2719
+ reloaded_session = self.get_session(session_id=session.session_id)
2720
+
2721
+ if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
2722
+ # Get the last run (which is the one just created by the tool)
2723
+ last_run = reloaded_session.runs[-1]
2724
+
2725
+ # Yield WorkflowAgentCompletedEvent
2726
+ agent_completed_event = WorkflowAgentCompletedEvent(
2727
+ run_id=agent_response.run_id if agent_response else None,
2728
+ workflow_name=self.name,
2729
+ workflow_id=self.id,
2730
+ session_id=session.session_id,
2731
+ content=agent_response.content if agent_response else None,
2732
+ )
2733
+ yield agent_completed_event
2734
+
2735
+ # Update the last run with workflow_agent_run
2736
+ last_run.workflow_agent_run = agent_response
2737
+
2738
+ # Store the full agent RunOutput and establish parent-child relationship
2739
+ if agent_response:
2740
+ agent_response.parent_run_id = last_run.run_id
2741
+ agent_response.workflow_id = last_run.workflow_id
2742
+
2743
+ # Save the reloaded session (which has the updated run)
2744
+ self.save_session(session=reloaded_session)
2745
+
2746
+ else:
2747
+ log_warning("Could not reload session or no runs found after workflow execution")
2748
+
2749
+ def _run_workflow_agent(
2750
+ self,
2751
+ agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
2752
+ session: WorkflowSession,
2753
+ execution_input: WorkflowExecutionInput,
2754
+ session_state: Optional[Dict[str, Any]],
2755
+ stream: bool = False,
2756
+ ) -> WorkflowRunOutput:
2757
+ """
2758
+ Execute the workflow agent in non-streaming mode.
2759
+
2760
+ The agent decides whether to run the workflow or answer directly from history.
2761
+
2762
+ Returns:
2763
+ WorkflowRunOutput: The workflow run output with agent response
2764
+ """
2765
+
2766
+ # Initialize the agent
2767
+ self._initialize_workflow_agent(session, execution_input, session_state, stream=stream)
2768
+
2769
+ # Build dependencies with workflow context
2770
+ dependencies = self._get_workflow_agent_dependencies(session)
2771
+
2772
+ # Run the agent
2773
+ agent_response: RunOutput = self.agent.run( # type: ignore[union-attr]
2774
+ input=agent_input,
2775
+ session_id=session.session_id,
2776
+ dependencies=dependencies,
2777
+ stream=stream,
2778
+ ) # type: ignore
2779
+
2780
+ # Check if the agent called the workflow tool
2781
+ workflow_executed = False
2782
+ if agent_response.messages:
2783
+ for message in agent_response.messages:
2784
+ if message.role == "assistant" and message.tool_calls:
2785
+ # Check if the tool call is specifically for run_workflow
2786
+ for tool_call in message.tool_calls:
2787
+ # Handle both dict and object formats
2788
+ if isinstance(tool_call, dict):
2789
+ tool_name = tool_call.get("function", {}).get("name", "")
2790
+ else:
2791
+ tool_name = tool_call.function.name if hasattr(tool_call, "function") else ""
2792
+
2793
+ if tool_name == "run_workflow":
2794
+ workflow_executed = True
2795
+ break
2796
+ if workflow_executed:
2797
+ break
2798
+
2799
+ log_debug(f"Workflow agent execution complete. Workflow executed: {workflow_executed}")
2800
+
2801
+ # Handle direct answer case (no workflow execution)
2802
+ if not workflow_executed:
2803
+ # Create a new workflow run output for the direct answer
2804
+ run_id = str(uuid4())
2805
+ workflow_run_response = WorkflowRunOutput(
2806
+ run_id=run_id,
2807
+ input=execution_input.input,
2808
+ session_id=session.session_id,
2809
+ workflow_id=self.id,
2810
+ workflow_name=self.name,
2811
+ created_at=int(datetime.now().timestamp()),
2812
+ content=agent_response.content,
2813
+ status=RunStatus.completed,
2814
+ workflow_agent_run=agent_response,
2815
+ )
2816
+
2817
+ # Store the full agent RunOutput and establish parent-child relationship
2818
+ if agent_response:
2819
+ agent_response.parent_run_id = workflow_run_response.run_id
2820
+ agent_response.workflow_id = workflow_run_response.workflow_id
2821
+
2822
+ # Update the run in session
2823
+ session.upsert_run(run=workflow_run_response)
2824
+ self.save_session(session=session)
2825
+
2826
+ log_debug(f"Agent decision: workflow_executed={workflow_executed}")
2827
+
2828
+ return workflow_run_response
2829
+ else:
2830
+ # Workflow was executed by the tool
2831
+ reloaded_session = self.get_session(session_id=session.session_id)
2832
+
2833
+ if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
2834
+ # Get the last run (which is the one just created by the tool)
2835
+ last_run = reloaded_session.runs[-1]
2836
+
2837
+ # Update the last run directly with workflow_agent_run
2838
+ last_run.workflow_agent_run = agent_response
2839
+
2840
+ # Store the full agent RunOutput and establish parent-child relationship
2841
+ if agent_response:
2842
+ agent_response.parent_run_id = last_run.run_id
2843
+ agent_response.workflow_id = last_run.workflow_id
2844
+
2845
+ # Save the reloaded session (which has the updated run)
2846
+ self.save_session(session=reloaded_session)
2847
+
2848
+ # Return the last run directly (WRO2 from inner workflow)
2849
+ return last_run
2850
+ else:
2851
+ log_warning("Could not reload session or no runs found after workflow execution")
2852
+ # Return a placeholder error response
2853
+ return WorkflowRunOutput(
2854
+ run_id=str(uuid4()),
2855
+ input=execution_input.input,
2856
+ session_id=session.session_id,
2857
+ workflow_id=self.id,
2858
+ workflow_name=self.name,
2859
+ created_at=int(datetime.now().timestamp()),
2860
+ content="Error: Workflow execution failed",
2861
+ status=RunStatus.error,
2862
+ )
2863
+
2864
+ def _async_initialize_workflow_agent(
2865
+ self,
2866
+ session: WorkflowSession,
2867
+ execution_input: WorkflowExecutionInput,
2868
+ session_state: Optional[Dict[str, Any]],
2869
+ websocket_handler: Optional[WebSocketHandler] = None,
2870
+ stream: bool = False,
2871
+ ) -> None:
2872
+ """Initialize the workflow agent with async tools (but NOT context - that's passed per-run)"""
2873
+ from agno.tools.function import Function
2874
+
2875
+ workflow_tool_func = self.agent.async_create_workflow_tool( # type: ignore
2876
+ workflow=self,
2877
+ session=session,
2878
+ execution_input=execution_input,
2879
+ session_state=session_state,
2880
+ stream=stream,
2881
+ websocket_handler=websocket_handler,
2882
+ )
2883
+ workflow_tool = Function.from_callable(workflow_tool_func)
2884
+
2885
+ self.agent.tools = [workflow_tool] # type: ignore
2886
+ self.agent._rebuild_tools = True # type: ignore
2887
+
2888
+ log_debug("Workflow agent initialized with async run_workflow tool")
2889
+
2890
+ async def _aload_session_for_workflow_agent(
2891
+ self,
2892
+ session_id: str,
2893
+ user_id: Optional[str],
2894
+ session_state: Optional[Dict[str, Any]],
2895
+ ) -> Tuple[WorkflowSession, Dict[str, Any]]:
2896
+ """Helper to load or create session for workflow agent execution"""
2897
+ return await self._aload_or_create_session(session_id=session_id, user_id=user_id, session_state=session_state)
2898
+
2899
+ def _aexecute_workflow_agent(
2900
+ self,
2901
+ user_input: Union[str, Dict[str, Any], List[Any], BaseModel],
2902
+ session_id: str,
2903
+ user_id: Optional[str],
2904
+ execution_input: WorkflowExecutionInput,
2905
+ session_state: Optional[Dict[str, Any]],
2906
+ stream: bool = False,
2907
+ websocket_handler: Optional[WebSocketHandler] = None,
2908
+ **kwargs: Any,
2909
+ ):
2910
+ """
2911
+ Execute the workflow agent asynchronously in streaming or non-streaming mode.
2912
+
2913
+ The agent decides whether to run the workflow or answer directly from history.
2914
+
2915
+ Args:
2916
+ user_input: The user's input
2917
+ session_id: The workflow session ID
2918
+ user_id: The user ID
2919
+ execution_input: The execution input
2920
+ session_state: The session state
2921
+ stream: Whether to stream the response
2922
+ websocket_handler: The WebSocket handler
2923
+
2924
+ Returns:
2925
+ Coroutine[WorkflowRunOutput] if stream=False, AsyncIterator[WorkflowRunOutputEvent] if stream=True
2926
+ """
2927
+ if stream:
2928
+
2929
+ async def _stream():
2930
+ session, session_state_loaded = await self._aload_session_for_workflow_agent(
2931
+ session_id, user_id, session_state
2932
+ )
2933
+ async for event in self._arun_workflow_agent_stream(
2934
+ agent_input=user_input,
2935
+ session=session,
2936
+ execution_input=execution_input,
2937
+ session_state=session_state_loaded,
2938
+ stream=stream,
2939
+ websocket_handler=websocket_handler,
2940
+ **kwargs,
2941
+ ):
2942
+ yield event
2943
+
2944
+ return _stream()
2945
+ else:
2946
+
2947
+ async def _execute():
2948
+ session, session_state_loaded = await self._aload_session_for_workflow_agent(
2949
+ session_id, user_id, session_state
2950
+ )
2951
+ return await self._arun_workflow_agent(
2952
+ agent_input=user_input,
2953
+ session=session,
2954
+ execution_input=execution_input,
2955
+ session_state=session_state_loaded,
2956
+ stream=stream,
2957
+ )
2958
+
2959
+ return _execute()
2960
+
2961
+ async def _arun_workflow_agent_stream(
2962
+ self,
2963
+ agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
2964
+ session: WorkflowSession,
2965
+ execution_input: WorkflowExecutionInput,
2966
+ session_state: Optional[Dict[str, Any]],
2967
+ stream: bool = False,
2968
+ websocket_handler: Optional[WebSocketHandler] = None,
2969
+ **kwargs: Any,
2970
+ ) -> AsyncIterator[WorkflowRunOutputEvent]:
2971
+ """
2972
+ Execute the workflow agent asynchronously in streaming mode.
2973
+
2974
+ The agent's tool (run_workflow) is an async generator that yields workflow events directly.
2975
+ These events bubble up through the agent's streaming and are yielded here.
2976
+ We filter to only yield WorkflowRunOutputEvent to the CLI.
2977
+
2978
+ Yields:
2979
+ WorkflowRunOutputEvent: Events from workflow execution (agent events are filtered)
2980
+ """
2981
+ from typing import get_args
2982
+
2983
+ from agno.run.workflow import WorkflowCompletedEvent, WorkflowRunOutputEvent
2984
+
2985
+ logger.info("Workflow agent enabled - async streaming mode")
2986
+ log_debug(f"User input: {agent_input}")
2987
+
2988
+ self._async_initialize_workflow_agent(
2989
+ session, execution_input, session_state, stream=stream, websocket_handler=websocket_handler
2990
+ )
2991
+
2992
+ dependencies = self._get_workflow_agent_dependencies(session)
2993
+
2994
+ agent_response: Optional[RunOutput] = None
2995
+ workflow_executed = False
2996
+
2997
+ from agno.run.agent import RunContentEvent
2998
+ from agno.run.team import RunContentEvent as TeamRunContentEvent
2999
+ from agno.run.workflow import WorkflowAgentCompletedEvent, WorkflowAgentStartedEvent
3000
+
3001
+ log_debug(f"Executing async workflow agent with streaming - input: {agent_input}...")
3002
+
3003
+ # Create a workflow run response upfront for potential direct answer (will be used only if workflow is not executed)
3004
+ run_id = str(uuid4())
3005
+ direct_reply_run_response = WorkflowRunOutput(
3006
+ run_id=run_id,
3007
+ input=execution_input.input,
3008
+ session_id=session.session_id,
3009
+ workflow_id=self.id,
3010
+ workflow_name=self.name,
3011
+ created_at=int(datetime.now().timestamp()),
3012
+ )
3013
+
3014
+ # Yield WorkflowAgentStartedEvent at the beginning (stored in direct_reply_run_response)
3015
+ agent_started_event = WorkflowAgentStartedEvent(
3016
+ workflow_name=self.name,
3017
+ workflow_id=self.id,
3018
+ session_id=session.session_id,
3019
+ )
3020
+ self._broadcast_to_websocket(agent_started_event, websocket_handler)
3021
+ yield agent_started_event
3022
+
3023
+ # Run the agent in streaming mode and yield all events
3024
+ async for event in self.agent.arun( # type: ignore[union-attr]
3025
+ input=agent_input,
3026
+ stream=True,
3027
+ stream_intermediate_steps=True,
3028
+ yield_run_response=True,
3029
+ session_id=session.session_id,
3030
+ dependencies=dependencies, # Pass context dynamically per-run
3031
+ ): # type: ignore
3032
+ if isinstance(event, tuple(get_args(WorkflowRunOutputEvent))):
3033
+ yield event # type: ignore[misc]
3034
+
3035
+ if isinstance(event, WorkflowCompletedEvent):
3036
+ workflow_executed = True
3037
+ log_debug("Workflow execution detected via WorkflowCompletedEvent")
3038
+
3039
+ elif isinstance(event, (RunContentEvent, TeamRunContentEvent)):
3040
+ if event.step_name is None:
3041
+ # This is from the workflow agent itself
3042
+ # Enrich with metadata to mark it as a workflow agent event
3043
+
3044
+ if workflow_executed:
3045
+ continue # Skip if workflow was already executed
3046
+
3047
+ # workflow_agent field is used by consumers of the events to distinguish between workflow agent and regular agent
3048
+ event.workflow_agent = True # type: ignore
3049
+
3050
+ # Broadcast to WebSocket if available (async context only)
3051
+ self._broadcast_to_websocket(event, websocket_handler)
3052
+
3053
+ yield event # type: ignore[misc]
3054
+
3055
+ # Capture the final RunOutput (but don't yield it)
3056
+ if isinstance(event, RunOutput):
3057
+ agent_response = event
3058
+ log_debug(
3059
+ f"Agent response: {str(agent_response.content)[:100] if agent_response.content else 'None'}..."
3060
+ )
3061
+
3062
+ # Handle direct answer case (no workflow execution)
3063
+ if not workflow_executed:
3064
+ # Update the pre-created workflow run response with the direct answer
3065
+ direct_reply_run_response.content = agent_response.content if agent_response else ""
3066
+ direct_reply_run_response.status = RunStatus.completed
3067
+ direct_reply_run_response.workflow_agent_run = agent_response
3068
+
3069
+ workflow_run_response = direct_reply_run_response
3070
+
3071
+ # Store the full agent RunOutput and establish parent-child relationship
3072
+ if agent_response:
3073
+ agent_response.parent_run_id = workflow_run_response.run_id
3074
+ agent_response.workflow_id = workflow_run_response.workflow_id
3075
+
3076
+ # Yield WorkflowAgentCompletedEvent
3077
+ agent_completed_event = WorkflowAgentCompletedEvent(
3078
+ workflow_name=self.name,
3079
+ workflow_id=self.id,
3080
+ run_id=agent_response.run_id if agent_response else None,
3081
+ session_id=session.session_id,
3082
+ content=workflow_run_response.content,
3083
+ )
3084
+ self._broadcast_to_websocket(agent_completed_event, websocket_handler)
3085
+ yield agent_completed_event
3086
+
3087
+ # Yield a workflow completed event with the agent's direct response (user internally by aprint_response_stream)
3088
+ completed_event = WorkflowCompletedEvent(
3089
+ run_id=workflow_run_response.run_id or "",
3090
+ content=workflow_run_response.content,
3091
+ workflow_name=workflow_run_response.workflow_name,
3092
+ workflow_id=workflow_run_response.workflow_id,
3093
+ session_id=workflow_run_response.session_id,
3094
+ step_results=[],
3095
+ metadata={"agent_direct_response": True},
3096
+ )
3097
+ yield completed_event
3098
+
3099
+ # Update the run in session
3100
+ session.upsert_run(run=workflow_run_response)
3101
+ # Save session
3102
+ if self._has_async_db():
3103
+ await self.asave_session(session=session)
3104
+ else:
3105
+ self.save_session(session=session)
3106
+
3107
+ else:
3108
+ # Workflow was executed by the tool
3109
+ if self._has_async_db():
3110
+ reloaded_session = await self.aget_session(session_id=session.session_id)
3111
+ else:
3112
+ reloaded_session = self.get_session(session_id=session.session_id)
3113
+
3114
+ if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
3115
+ # Get the last run (which is the one just created by the tool)
3116
+ last_run = reloaded_session.runs[-1]
3117
+
3118
+ # Yield WorkflowAgentCompletedEvent
3119
+ agent_completed_event = WorkflowAgentCompletedEvent(
3120
+ run_id=agent_response.run_id if agent_response else None,
3121
+ workflow_name=self.name,
3122
+ workflow_id=self.id,
3123
+ session_id=session.session_id,
3124
+ content=agent_response.content if agent_response else None,
3125
+ )
3126
+
3127
+ self._broadcast_to_websocket(agent_completed_event, websocket_handler)
3128
+
3129
+ yield agent_completed_event
3130
+
3131
+ # Update the last run with workflow_agent_run
3132
+ last_run.workflow_agent_run = agent_response
3133
+
3134
+ # Store the full agent RunOutput and establish parent-child relationship
3135
+ if agent_response:
3136
+ agent_response.parent_run_id = last_run.run_id
3137
+ agent_response.workflow_id = last_run.workflow_id
3138
+
3139
+ # Save the reloaded session (which has the updated run)
3140
+ if self._has_async_db():
3141
+ await self.asave_session(session=reloaded_session)
3142
+ else:
3143
+ self.save_session(session=reloaded_session)
3144
+
3145
+ else:
3146
+ log_warning("Could not reload session or no runs found after workflow execution")
3147
+
3148
+ async def _arun_workflow_agent(
3149
+ self,
3150
+ agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
3151
+ session: WorkflowSession,
3152
+ execution_input: WorkflowExecutionInput,
3153
+ session_state: Optional[Dict[str, Any]],
3154
+ stream: bool = False,
3155
+ ) -> WorkflowRunOutput:
3156
+ """
3157
+ Execute the workflow agent asynchronously in non-streaming mode.
3158
+
3159
+ The agent decides whether to run the workflow or answer directly from history.
3160
+
3161
+ Returns:
3162
+ WorkflowRunOutput: The workflow run output with agent response
3163
+ """
3164
+ # Initialize the agent
3165
+ self._async_initialize_workflow_agent(session, execution_input, session_state, stream=stream)
3166
+
3167
+ # Build dependencies with workflow context
3168
+ dependencies = self._get_workflow_agent_dependencies(session)
3169
+
3170
+ # Run the agent
3171
+ agent_response: RunOutput = await self.agent.arun( # type: ignore[union-attr]
3172
+ input=agent_input,
3173
+ session_id=session.session_id,
3174
+ dependencies=dependencies,
3175
+ stream=stream,
3176
+ ) # type: ignore
3177
+
3178
+ # Check if the agent called the workflow tool
3179
+ workflow_executed = False
3180
+ if agent_response.messages:
3181
+ for message in agent_response.messages:
3182
+ if message.role == "assistant" and message.tool_calls:
3183
+ # Check if the tool call is specifically for run_workflow
3184
+ for tool_call in message.tool_calls:
3185
+ # Handle both dict and object formats
3186
+ if isinstance(tool_call, dict):
3187
+ tool_name = tool_call.get("function", {}).get("name", "")
3188
+ else:
3189
+ tool_name = tool_call.function.name if hasattr(tool_call, "function") else ""
3190
+
3191
+ if tool_name == "run_workflow":
3192
+ workflow_executed = True
3193
+ break
3194
+ if workflow_executed:
3195
+ break
3196
+
3197
+ # Handle direct answer case (no workflow execution)
3198
+ if not workflow_executed:
3199
+ # Create a new workflow run output for the direct answer
3200
+ run_id = str(uuid4())
3201
+ workflow_run_response = WorkflowRunOutput(
3202
+ run_id=run_id,
3203
+ input=execution_input.input,
3204
+ session_id=session.session_id,
3205
+ workflow_id=self.id,
3206
+ workflow_name=self.name,
3207
+ created_at=int(datetime.now().timestamp()),
3208
+ content=agent_response.content,
3209
+ status=RunStatus.completed,
3210
+ workflow_agent_run=agent_response,
3211
+ )
3212
+
3213
+ # Store the full agent RunOutput and establish parent-child relationship
3214
+ if agent_response:
3215
+ agent_response.parent_run_id = workflow_run_response.run_id
3216
+ agent_response.workflow_id = workflow_run_response.workflow_id
3217
+
3218
+ # Update the run in session
3219
+ session.upsert_run(run=workflow_run_response)
3220
+ if self._has_async_db():
3221
+ await self.asave_session(session=session)
3222
+ else:
3223
+ self.save_session(session=session)
3224
+
3225
+ log_debug(f"Agent decision: workflow_executed={workflow_executed}")
3226
+
3227
+ return workflow_run_response
3228
+ else:
3229
+ # Workflow was executed by the tool
3230
+ logger.info("=" * 80)
3231
+ logger.info("WORKFLOW AGENT: Called run_workflow tool (async)")
3232
+ logger.info(" ➜ Workflow was executed, retrieving results...")
3233
+ logger.info("=" * 80)
3234
+
3235
+ log_debug("Reloading session from database to get the latest workflow run...")
3236
+ if self._has_async_db():
3237
+ reloaded_session = await self.aget_session(session_id=session.session_id)
3238
+ else:
3239
+ reloaded_session = self.get_session(session_id=session.session_id)
3240
+
3241
+ if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
3242
+ # Get the last run (which is the one just created by the tool)
3243
+ last_run = reloaded_session.runs[-1]
3244
+ log_debug(f"Retrieved latest workflow run: {last_run.run_id}")
3245
+ log_debug(f"Total workflow runs in session: {len(reloaded_session.runs)}")
3246
+
3247
+ # Update the last run with workflow_agent_run
3248
+ last_run.workflow_agent_run = agent_response
3249
+
3250
+ # Store the full agent RunOutput and establish parent-child relationship
3251
+ if agent_response:
3252
+ agent_response.parent_run_id = last_run.run_id
3253
+ agent_response.workflow_id = last_run.workflow_id
3254
+
3255
+ # Save the reloaded session (which has the updated run)
3256
+ if self._has_async_db():
3257
+ await self.asave_session(session=reloaded_session)
3258
+ else:
3259
+ self.save_session(session=reloaded_session)
3260
+
3261
+ log_debug(f"Agent decision: workflow_executed={workflow_executed}")
3262
+
3263
+ # Return the last run directly (WRO2 from inner workflow)
3264
+ return last_run
3265
+ else:
3266
+ log_warning("Could not reload session or no runs found after workflow execution")
3267
+ # Return a placeholder error response
3268
+ return WorkflowRunOutput(
3269
+ run_id=str(uuid4()),
3270
+ input=execution_input.input,
3271
+ session_id=session.session_id,
3272
+ workflow_id=self.id,
3273
+ workflow_name=self.name,
3274
+ created_at=int(datetime.now().timestamp()),
3275
+ content="Error: Workflow execution failed",
3276
+ status=RunStatus.error,
3277
+ )
3278
+
2442
3279
  def cancel_run(self, run_id: str) -> bool:
2443
3280
  """Cancel a running workflow execution.
2444
3281
 
@@ -2547,16 +3384,6 @@ class Workflow:
2547
3384
  # Prepare steps
2548
3385
  self._prepare_steps()
2549
3386
 
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
3387
  inputs = WorkflowExecutionInput(
2561
3388
  input=input,
2562
3389
  additional_data=additional_data,
@@ -2571,6 +3398,27 @@ class Workflow:
2571
3398
 
2572
3399
  self.update_agents_and_teams_session_info()
2573
3400
 
3401
+ # Execute workflow agent if configured
3402
+ if self.agent is not None:
3403
+ return self._execute_workflow_agent(
3404
+ user_input=input, # type: ignore
3405
+ session=workflow_session,
3406
+ execution_input=inputs,
3407
+ session_state=session_state,
3408
+ stream=stream,
3409
+ **kwargs,
3410
+ )
3411
+
3412
+ # Create workflow run response for regular workflow execution
3413
+ workflow_run_response = WorkflowRunOutput(
3414
+ run_id=run_id,
3415
+ input=input,
3416
+ session_id=session_id,
3417
+ workflow_id=self.id,
3418
+ workflow_name=self.name,
3419
+ created_at=int(datetime.now().timestamp()),
3420
+ )
3421
+
2574
3422
  if stream:
2575
3423
  return self._execute_stream(
2576
3424
  session=workflow_session,
@@ -2717,16 +3565,6 @@ class Workflow:
2717
3565
  # Prepare steps
2718
3566
  self._prepare_steps()
2719
3567
 
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
3568
  inputs = WorkflowExecutionInput(
2731
3569
  input=input,
2732
3570
  additional_data=additional_data,
@@ -2741,6 +3579,27 @@ class Workflow:
2741
3579
 
2742
3580
  self.update_agents_and_teams_session_info()
2743
3581
 
3582
+ if self.agent is not None:
3583
+ return self._aexecute_workflow_agent( # type: ignore
3584
+ user_input=input, # type: ignore
3585
+ session_id=session_id,
3586
+ user_id=user_id,
3587
+ execution_input=inputs,
3588
+ session_state=session_state,
3589
+ stream=stream,
3590
+ **kwargs,
3591
+ )
3592
+
3593
+ # Create workflow run response for regular workflow execution
3594
+ workflow_run_response = WorkflowRunOutput(
3595
+ run_id=run_id,
3596
+ input=input,
3597
+ session_id=session_id,
3598
+ workflow_id=self.id,
3599
+ workflow_name=self.name,
3600
+ created_at=int(datetime.now().timestamp()),
3601
+ )
3602
+
2744
3603
  if stream:
2745
3604
  return self._aexecute_stream( # type: ignore
2746
3605
  execution_input=inputs,
@@ -2782,6 +3641,12 @@ class Workflow:
2782
3641
  step_name = step.name or f"step_{i + 1}"
2783
3642
  log_debug(f"Step {i + 1}: Team '{step_name}' with {len(step.members)} members")
2784
3643
  prepared_steps.append(Step(name=step_name, description=step.description, team=step))
3644
+ elif isinstance(step, Step) and step.add_workflow_history is True and self.db is None:
3645
+ log_warning(
3646
+ f"Step '{step.name or f'step_{i + 1}'}' has add_workflow_history=True "
3647
+ "but no database is configured in the Workflow. "
3648
+ "History won't be persisted. Add a database to persist runs across executions."
3649
+ )
2785
3650
  elif isinstance(step, (Step, Steps, Loop, Parallel, Condition, Router)):
2786
3651
  step_type = type(step).__name__
2787
3652
  step_name = getattr(step, "name", f"unnamed_{step_type.lower()}")