agno 2.2.9__py3-none-any.whl → 2.2.11__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 (43) hide show
  1. agno/agent/agent.py +27 -5
  2. agno/db/dynamo/utils.py +1 -1
  3. agno/db/firestore/utils.py +1 -1
  4. agno/db/gcs_json/utils.py +1 -1
  5. agno/db/in_memory/utils.py +1 -1
  6. agno/db/json/utils.py +1 -1
  7. agno/db/mongo/utils.py +3 -3
  8. agno/db/mysql/utils.py +1 -1
  9. agno/db/postgres/utils.py +1 -1
  10. agno/db/redis/utils.py +1 -1
  11. agno/db/singlestore/utils.py +1 -1
  12. agno/db/sqlite/utils.py +1 -1
  13. agno/knowledge/chunking/agentic.py +8 -9
  14. agno/knowledge/chunking/strategy.py +59 -15
  15. agno/knowledge/embedder/sentence_transformer.py +6 -2
  16. agno/knowledge/reader/base.py +6 -2
  17. agno/knowledge/utils.py +20 -0
  18. agno/models/anthropic/claude.py +45 -9
  19. agno/models/base.py +4 -0
  20. agno/os/app.py +35 -19
  21. agno/os/routers/health.py +5 -3
  22. agno/os/routers/knowledge/knowledge.py +43 -17
  23. agno/os/routers/knowledge/schemas.py +4 -3
  24. agno/run/agent.py +11 -1
  25. agno/team/team.py +20 -3
  26. agno/tools/file_generation.py +4 -4
  27. agno/tools/gmail.py +179 -0
  28. agno/tools/parallel.py +314 -0
  29. agno/utils/models/claude.py +2 -1
  30. agno/workflow/agent.py +2 -2
  31. agno/workflow/condition.py +26 -4
  32. agno/workflow/loop.py +9 -0
  33. agno/workflow/parallel.py +39 -16
  34. agno/workflow/router.py +25 -4
  35. agno/workflow/step.py +163 -91
  36. agno/workflow/steps.py +9 -0
  37. agno/workflow/types.py +20 -1
  38. agno/workflow/workflow.py +117 -30
  39. {agno-2.2.9.dist-info → agno-2.2.11.dist-info}/METADATA +4 -1
  40. {agno-2.2.9.dist-info → agno-2.2.11.dist-info}/RECORD +43 -42
  41. {agno-2.2.9.dist-info → agno-2.2.11.dist-info}/WHEEL +0 -0
  42. {agno-2.2.9.dist-info → agno-2.2.11.dist-info}/licenses/LICENSE +0 -0
  43. {agno-2.2.9.dist-info → agno-2.2.11.dist-info}/top_level.txt +0 -0
agno/workflow/step.py CHANGED
@@ -11,7 +11,10 @@ from agno.agent import Agent
11
11
  from agno.media import Audio, Image, Video
12
12
  from agno.models.metrics import Metrics
13
13
  from agno.run import RunContext
14
- from agno.run.agent import RunOutput
14
+ from agno.run.agent import RunCompletedEvent, RunContentEvent, RunOutput
15
+ from agno.run.base import BaseRunOutputEvent
16
+ from agno.run.team import RunCompletedEvent as TeamRunCompletedEvent
17
+ from agno.run.team import RunContentEvent as TeamRunContentEvent
15
18
  from agno.run.team import TeamRunOutput
16
19
  from agno.run.workflow import (
17
20
  StepCompletedEvent,
@@ -179,31 +182,37 @@ class Step:
179
182
  func: Callable,
180
183
  step_input: StepInput,
181
184
  session_state: Optional[Dict[str, Any]] = None,
185
+ run_context: Optional[RunContext] = None,
182
186
  ) -> Any:
183
187
  """Call custom function with session_state support if the function accepts it"""
188
+
189
+ kwargs: Dict[str, Any] = {}
190
+ if run_context is not None and self._function_has_run_context_param():
191
+ kwargs["run_context"] = run_context
184
192
  if session_state is not None and self._function_has_session_state_param():
185
- return func(step_input, session_state)
186
- else:
187
- return func(step_input)
193
+ kwargs["session_state"] = session_state
194
+
195
+ return func(step_input, **kwargs)
188
196
 
189
197
  async def _acall_custom_function(
190
198
  self,
191
199
  func: Callable,
192
200
  step_input: StepInput,
193
201
  session_state: Optional[Dict[str, Any]] = None,
202
+ run_context: Optional[RunContext] = None,
194
203
  ) -> Any:
195
204
  """Call custom async function with session_state support if the function accepts it"""
196
205
 
206
+ kwargs: Dict[str, Any] = {}
207
+ if run_context is not None and self._function_has_run_context_param():
208
+ kwargs["run_context"] = run_context
209
+ if session_state is not None and self._function_has_session_state_param():
210
+ kwargs["session_state"] = session_state
211
+
197
212
  if _is_async_generator_function(func):
198
- if session_state is not None and self._function_has_session_state_param():
199
- return func(step_input, session_state)
200
- else:
201
- return func(step_input)
213
+ return func(step_input, **kwargs)
202
214
  else:
203
- if session_state is not None and self._function_has_session_state_param():
204
- return await func(step_input, session_state)
205
- else:
206
- return await func(step_input)
215
+ return await func(step_input, **kwargs)
207
216
 
208
217
  def execute(
209
218
  self,
@@ -227,8 +236,10 @@ class Step:
227
236
  if workflow_session:
228
237
  step_input.workflow_session = workflow_session
229
238
 
239
+ # Create session_state copy once to avoid duplication.
240
+ # Consider both run_context.session_state and session_state.
230
241
  if run_context is not None and run_context.session_state is not None:
231
- session_state_copy = copy(run_context.session_state)
242
+ session_state_copy = run_context.session_state
232
243
  else:
233
244
  session_state_copy = copy(session_state) if session_state is not None else {}
234
245
 
@@ -247,13 +258,13 @@ class Step:
247
258
  self.active_executor,
248
259
  step_input,
249
260
  session_state_copy, # type: ignore[arg-type]
261
+ run_context,
250
262
  ): # type: ignore
251
- if (
252
- hasattr(chunk, "content")
253
- and chunk.content is not None
254
- and isinstance(chunk.content, str)
255
- ):
256
- content += chunk.content
263
+ if isinstance(chunk, (BaseRunOutputEvent)):
264
+ if isinstance(chunk, (RunContentEvent, TeamRunContentEvent)):
265
+ content += chunk.content if chunk.content is not None else ""
266
+ elif isinstance(chunk, (RunCompletedEvent, TeamRunCompletedEvent)):
267
+ content = chunk.content if chunk.content is not None else ""
257
268
  else:
258
269
  content += str(chunk)
259
270
  if isinstance(chunk, StepOutput):
@@ -264,7 +275,7 @@ class Step:
264
275
  final_response = e.value
265
276
 
266
277
  # Merge session_state changes back
267
- if session_state is not None:
278
+ if run_context is None and session_state is not None:
268
279
  merge_dictionaries(session_state, session_state_copy)
269
280
 
270
281
  if final_response is not None:
@@ -273,10 +284,15 @@ class Step:
273
284
  response = StepOutput(content=content)
274
285
  else:
275
286
  # Execute function with signature inspection for session_state support
276
- result = self._call_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
287
+ result = self._call_custom_function(
288
+ self.active_executor, # type: ignore[arg-type]
289
+ step_input,
290
+ session_state_copy,
291
+ run_context,
292
+ )
277
293
 
278
294
  # Merge session_state changes back
279
- if session_state is not None:
295
+ if run_context is None and session_state is not None:
280
296
  merge_dictionaries(session_state, session_state_copy)
281
297
 
282
298
  # If function returns StepOutput, use it directly
@@ -334,12 +350,13 @@ class Step:
334
350
  session_id=session_id,
335
351
  user_id=user_id,
336
352
  session_state=session_state_copy, # Send a copy to the executor
353
+ run_context=run_context,
337
354
  **kwargs,
338
355
  )
339
356
 
340
- if session_state is not None:
341
- # Update workflow session state
342
- merge_dictionaries(session_state, session_state_copy) # type: ignore
357
+ # Update workflow session state
358
+ if run_context is None and session_state is not None:
359
+ merge_dictionaries(session_state, session_state_copy)
343
360
 
344
361
  if store_executor_outputs and workflow_run_response is not None:
345
362
  self._store_executor_response(workflow_run_response, response) # type: ignore
@@ -368,6 +385,17 @@ class Step:
368
385
 
369
386
  return StepOutput(content=f"Step {self.name} failed but skipped", success=False)
370
387
 
388
+ def _function_has_run_context_param(self) -> bool:
389
+ """Check if the custom function has a run_context parameter"""
390
+ if self._executor_type != "function":
391
+ return False
392
+
393
+ try:
394
+ sig = inspect.signature(self.active_executor) # type: ignore
395
+ return "run_context" in sig.parameters
396
+ except Exception:
397
+ return False
398
+
371
399
  def _function_has_session_state_param(self) -> bool:
372
400
  """Check if the custom function has a session_state parameter"""
373
401
  if self._executor_type != "function":
@@ -430,9 +458,10 @@ class Step:
430
458
  if workflow_session:
431
459
  step_input.workflow_session = workflow_session
432
460
 
433
- # Create session_state copy once to avoid duplication
461
+ # Create session_state copy once to avoid duplication.
462
+ # Consider both run_context.session_state and session_state.
434
463
  if run_context is not None and run_context.session_state is not None:
435
- session_state_copy = copy(run_context.session_state)
464
+ session_state_copy = run_context.session_state
436
465
  else:
437
466
  session_state_copy = copy(session_state) if session_state is not None else {}
438
467
 
@@ -460,22 +489,24 @@ class Step:
460
489
 
461
490
  if self._executor_type == "function":
462
491
  log_debug(f"Executing function executor for step: {self.name}")
463
-
464
492
  if _is_async_callable(self.active_executor) or _is_async_generator_function(self.active_executor):
465
493
  raise ValueError("Cannot use async function with synchronous execution")
466
-
467
494
  if _is_generator_function(self.active_executor):
468
495
  log_debug("Function returned iterable, streaming events")
469
496
  content = ""
470
497
  try:
471
- iterator = self._call_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
498
+ iterator = self._call_custom_function(
499
+ self.active_executor,
500
+ step_input,
501
+ session_state_copy,
502
+ run_context,
503
+ )
472
504
  for event in iterator: # type: ignore
473
- if (
474
- hasattr(event, "content")
475
- and event.content is not None
476
- and isinstance(event.content, str)
477
- ):
478
- content += event.content
505
+ if isinstance(event, (BaseRunOutputEvent)):
506
+ if isinstance(event, (RunContentEvent, TeamRunContentEvent)):
507
+ content += event.content if event.content is not None else ""
508
+ elif isinstance(event, (RunCompletedEvent, TeamRunCompletedEvent)):
509
+ content = event.content if event.content is not None else ""
479
510
  else:
480
511
  content += str(event)
481
512
  if isinstance(event, StepOutput):
@@ -491,7 +522,7 @@ class Step:
491
522
  yield enriched_event # type: ignore[misc]
492
523
 
493
524
  # Merge session_state changes back
494
- if session_state is not None:
525
+ if run_context is None and session_state is not None:
495
526
  merge_dictionaries(session_state, session_state_copy)
496
527
 
497
528
  if not final_response:
@@ -501,10 +532,15 @@ class Step:
501
532
  final_response = e.value
502
533
 
503
534
  else:
504
- result = self._call_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
535
+ result = self._call_custom_function(
536
+ self.active_executor, # type: ignore[arg-type]
537
+ step_input,
538
+ session_state_copy,
539
+ run_context,
540
+ )
505
541
 
506
542
  # Merge session_state changes back
507
- if session_state is not None:
543
+ if run_context is None and session_state is not None:
508
544
  merge_dictionaries(session_state, session_state_copy)
509
545
 
510
546
  if isinstance(result, StepOutput):
@@ -564,6 +600,7 @@ class Step:
564
600
  stream=True,
565
601
  stream_events=stream_events,
566
602
  yield_run_response=True,
603
+ run_context=run_context,
567
604
  **kwargs,
568
605
  )
569
606
 
@@ -577,9 +614,9 @@ class Step:
577
614
  if stream_executor_events:
578
615
  yield enriched_event # type: ignore[misc]
579
616
 
580
- if session_state is not None:
581
- # Update workflow session state
582
- merge_dictionaries(session_state, session_state_copy) # type: ignore
617
+ # Update workflow session state
618
+ if run_context is None and session_state is not None:
619
+ merge_dictionaries(session_state, session_state_copy)
583
620
 
584
621
  if store_executor_outputs and workflow_run_response is not None:
585
622
  self._store_executor_response(workflow_run_response, active_executor_run_response) # type: ignore
@@ -640,6 +677,7 @@ class Step:
640
677
  session_id: Optional[str] = None,
641
678
  user_id: Optional[str] = None,
642
679
  workflow_run_response: Optional["WorkflowRunOutput"] = None,
680
+ run_context: Optional[RunContext] = None,
643
681
  session_state: Optional[Dict[str, Any]] = None,
644
682
  store_executor_outputs: bool = True,
645
683
  workflow_session: Optional["WorkflowSession"] = None,
@@ -655,8 +693,13 @@ class Step:
655
693
 
656
694
  if workflow_session:
657
695
  step_input.workflow_session = workflow_session
658
- # Create session_state copy once to avoid duplication
659
- session_state_copy = copy(session_state) if session_state is not None else {}
696
+
697
+ # Create session_state copy once to avoid duplication.
698
+ # Consider both run_context.session_state and session_state.
699
+ if run_context is not None and run_context.session_state is not None:
700
+ session_state_copy = run_context.session_state
701
+ else:
702
+ session_state_copy = copy(session_state) if session_state is not None else {}
660
703
 
661
704
  # Execute with retries
662
705
  for attempt in range(self.max_retries + 1):
@@ -672,15 +715,15 @@ class Step:
672
715
  iterator = self._call_custom_function(
673
716
  self.active_executor,
674
717
  step_input,
675
- session_state_copy, # type: ignore[arg-type]
676
- ) # type: ignore
718
+ session_state_copy,
719
+ run_context,
720
+ )
677
721
  for chunk in iterator: # type: ignore
678
- if (
679
- hasattr(chunk, "content")
680
- and chunk.content is not None
681
- and isinstance(chunk.content, str)
682
- ):
683
- content += chunk.content
722
+ if isinstance(chunk, (BaseRunOutputEvent)):
723
+ if isinstance(chunk, (RunContentEvent, TeamRunContentEvent)):
724
+ content += chunk.content if chunk.content is not None else ""
725
+ elif isinstance(chunk, (RunCompletedEvent, TeamRunCompletedEvent)):
726
+ content = chunk.content if chunk.content is not None else ""
684
727
  else:
685
728
  content += str(chunk)
686
729
  if isinstance(chunk, StepOutput):
@@ -690,15 +733,15 @@ class Step:
690
733
  iterator = await self._acall_custom_function(
691
734
  self.active_executor,
692
735
  step_input,
693
- session_state_copy, # type: ignore[arg-type]
694
- ) # type: ignore
736
+ session_state_copy,
737
+ run_context,
738
+ )
695
739
  async for chunk in iterator: # type: ignore
696
- if (
697
- hasattr(chunk, "content")
698
- and chunk.content is not None
699
- and isinstance(chunk.content, str)
700
- ):
701
- content += chunk.content
740
+ if isinstance(chunk, (BaseRunOutputEvent)):
741
+ if isinstance(chunk, (RunContentEvent, TeamRunContentEvent)):
742
+ content += chunk.content if chunk.content is not None else ""
743
+ elif isinstance(chunk, (RunCompletedEvent, TeamRunCompletedEvent)):
744
+ content = chunk.content if chunk.content is not None else ""
702
745
  else:
703
746
  content += str(chunk)
704
747
  if isinstance(chunk, StepOutput):
@@ -709,7 +752,7 @@ class Step:
709
752
  final_response = e.value
710
753
 
711
754
  # Merge session_state changes back
712
- if session_state is not None:
755
+ if run_context is None and session_state is not None:
713
756
  merge_dictionaries(session_state, session_state_copy)
714
757
 
715
758
  if final_response is not None:
@@ -719,13 +762,21 @@ class Step:
719
762
  else:
720
763
  if _is_async_callable(self.active_executor):
721
764
  result = await self._acall_custom_function(
722
- self.active_executor, step_input, session_state_copy
723
- ) # type: ignore
765
+ self.active_executor,
766
+ step_input,
767
+ session_state_copy,
768
+ run_context,
769
+ )
724
770
  else:
725
- result = self._call_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
771
+ result = self._call_custom_function(
772
+ self.active_executor, # type: ignore[arg-type]
773
+ step_input,
774
+ session_state_copy,
775
+ run_context,
776
+ )
726
777
 
727
778
  # Merge session_state changes back
728
- if session_state is not None:
779
+ if run_context is None and session_state is not None:
729
780
  merge_dictionaries(session_state, session_state_copy)
730
781
 
731
782
  # If function returns StepOutput, use it directly
@@ -784,12 +835,13 @@ class Step:
784
835
  session_id=session_id,
785
836
  user_id=user_id,
786
837
  session_state=session_state_copy,
838
+ run_context=run_context,
787
839
  **kwargs,
788
840
  )
789
841
 
790
- if session_state is not None:
791
- # Update workflow session state
792
- merge_dictionaries(session_state, session_state_copy) # type: ignore
842
+ # Update workflow session state
843
+ if run_context is None and session_state is not None:
844
+ merge_dictionaries(session_state, session_state_copy)
793
845
 
794
846
  if store_executor_outputs and workflow_run_response is not None:
795
847
  self._store_executor_response(workflow_run_response, response) # type: ignore
@@ -827,6 +879,7 @@ class Step:
827
879
  stream_intermediate_steps: bool = False,
828
880
  stream_executor_events: bool = True,
829
881
  workflow_run_response: Optional["WorkflowRunOutput"] = None,
882
+ run_context: Optional[RunContext] = None,
830
883
  session_state: Optional[Dict[str, Any]] = None,
831
884
  step_index: Optional[Union[int, tuple]] = None,
832
885
  store_executor_outputs: bool = True,
@@ -843,8 +896,12 @@ class Step:
843
896
  if workflow_session:
844
897
  step_input.workflow_session = workflow_session
845
898
 
846
- # Create session_state copy once to avoid duplication
847
- session_state_copy = copy(session_state) if session_state is not None else {}
899
+ # Create session_state copy once to avoid duplication.
900
+ # Consider both run_context.session_state and session_state.
901
+ if run_context is not None and run_context.session_state is not None:
902
+ session_state_copy = run_context.session_state
903
+ else:
904
+ session_state_copy = copy(session_state) if session_state is not None else {}
848
905
 
849
906
  # Considering both stream_events and stream_intermediate_steps (deprecated)
850
907
  stream_events = stream_events or stream_intermediate_steps
@@ -878,15 +935,15 @@ class Step:
878
935
  iterator = await self._acall_custom_function(
879
936
  self.active_executor,
880
937
  step_input,
881
- session_state_copy, # type: ignore[arg-type]
882
- ) # type: ignore
938
+ session_state_copy,
939
+ run_context,
940
+ )
883
941
  async for event in iterator: # type: ignore
884
- if (
885
- hasattr(event, "content")
886
- and event.content is not None
887
- and isinstance(event.content, str)
888
- ):
889
- content += event.content
942
+ if isinstance(event, (BaseRunOutputEvent)):
943
+ if isinstance(event, (RunContentEvent, TeamRunContentEvent)):
944
+ content += event.content if event.content is not None else ""
945
+ elif isinstance(event, (RunCompletedEvent, TeamRunCompletedEvent)):
946
+ content = event.content if event.content is not None else ""
890
947
  else:
891
948
  content += str(event)
892
949
  if isinstance(event, StepOutput):
@@ -904,7 +961,12 @@ class Step:
904
961
  final_response = StepOutput(content=content)
905
962
  elif _is_async_callable(self.active_executor):
906
963
  # It's a regular async function - await it
907
- result = await self._acall_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
964
+ result = await self._acall_custom_function(
965
+ self.active_executor,
966
+ step_input,
967
+ session_state_copy,
968
+ run_context,
969
+ )
908
970
  if isinstance(result, StepOutput):
909
971
  final_response = result
910
972
  else:
@@ -912,14 +974,18 @@ class Step:
912
974
  elif _is_generator_function(self.active_executor):
913
975
  content = ""
914
976
  # It's a regular generator function - iterate over it
915
- iterator = self._call_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
977
+ iterator = self._call_custom_function(
978
+ self.active_executor,
979
+ step_input,
980
+ session_state_copy,
981
+ run_context,
982
+ )
916
983
  for event in iterator: # type: ignore
917
- if (
918
- hasattr(event, "content")
919
- and event.content is not None
920
- and isinstance(event.content, str)
921
- ):
922
- content += event.content
984
+ if isinstance(event, (BaseRunOutputEvent)):
985
+ if isinstance(event, (RunContentEvent, TeamRunContentEvent)):
986
+ content += event.content if event.content is not None else ""
987
+ elif isinstance(event, (RunCompletedEvent, TeamRunCompletedEvent)):
988
+ content = event.content if event.content is not None else ""
923
989
  else:
924
990
  content += str(event)
925
991
  if isinstance(event, StepOutput):
@@ -937,14 +1003,19 @@ class Step:
937
1003
  final_response = StepOutput(content=content)
938
1004
  else:
939
1005
  # It's a regular function - call it directly
940
- result = self._call_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
1006
+ result = self._call_custom_function(
1007
+ self.active_executor, # type: ignore[arg-type]
1008
+ step_input,
1009
+ session_state_copy,
1010
+ run_context,
1011
+ )
941
1012
  if isinstance(result, StepOutput):
942
1013
  final_response = result
943
1014
  else:
944
1015
  final_response = StepOutput(content=str(result))
945
1016
 
946
1017
  # Merge session_state changes back
947
- if session_state is not None:
1018
+ if run_context is None and session_state is not None:
948
1019
  merge_dictionaries(session_state, session_state_copy)
949
1020
  else:
950
1021
  # For agents and teams, prepare message with context
@@ -997,6 +1068,7 @@ class Step:
997
1068
  session_state=session_state_copy,
998
1069
  stream=True,
999
1070
  stream_events=stream_events,
1071
+ run_context=run_context,
1000
1072
  yield_run_response=True,
1001
1073
  **kwargs,
1002
1074
  )
@@ -1011,9 +1083,9 @@ class Step:
1011
1083
  if stream_executor_events:
1012
1084
  yield enriched_event # type: ignore[misc]
1013
1085
 
1014
- if session_state is not None:
1015
- # Update workflow session state
1016
- merge_dictionaries(session_state, session_state_copy) # type: ignore
1086
+ # Update workflow session state
1087
+ if run_context is None and session_state is not None:
1088
+ merge_dictionaries(session_state, session_state_copy)
1017
1089
 
1018
1090
  if store_executor_outputs and workflow_run_response is not None:
1019
1091
  self._store_executor_response(workflow_run_response, active_executor_run_response) # type: ignore
agno/workflow/steps.py CHANGED
@@ -3,6 +3,7 @@ from typing import Any, AsyncIterator, Awaitable, Callable, Dict, Iterator, List
3
3
  from uuid import uuid4
4
4
 
5
5
  from agno.run.agent import RunOutputEvent
6
+ from agno.run.base import RunContext
6
7
  from agno.run.team import TeamRunOutputEvent
7
8
  from agno.run.workflow import (
8
9
  StepsExecutionCompletedEvent,
@@ -118,6 +119,7 @@ class Steps:
118
119
  session_id: Optional[str] = None,
119
120
  user_id: Optional[str] = None,
120
121
  workflow_run_response: Optional[WorkflowRunOutput] = None,
122
+ run_context: Optional[RunContext] = None,
121
123
  session_state: Optional[Dict[str, Any]] = None,
122
124
  store_executor_outputs: bool = True,
123
125
  workflow_session: Optional[WorkflowSession] = None,
@@ -151,6 +153,7 @@ class Steps:
151
153
  user_id=user_id,
152
154
  workflow_run_response=workflow_run_response,
153
155
  store_executor_outputs=store_executor_outputs,
156
+ run_context=run_context,
154
157
  session_state=session_state,
155
158
  workflow_session=workflow_session,
156
159
  add_workflow_history_to_steps=add_workflow_history_to_steps,
@@ -204,6 +207,7 @@ class Steps:
204
207
  self,
205
208
  step_input: StepInput,
206
209
  workflow_run_response: WorkflowRunOutput,
210
+ run_context: Optional[RunContext] = None,
207
211
  session_state: Optional[Dict[str, Any]] = None,
208
212
  session_id: Optional[str] = None,
209
213
  user_id: Optional[str] = None,
@@ -269,6 +273,7 @@ class Steps:
269
273
  current_step_input,
270
274
  session_id=session_id,
271
275
  user_id=user_id,
276
+ run_context=run_context,
272
277
  session_state=session_state,
273
278
  stream_events=stream_events,
274
279
  stream_executor_events=stream_executor_events,
@@ -354,6 +359,7 @@ class Steps:
354
359
  session_id: Optional[str] = None,
355
360
  user_id: Optional[str] = None,
356
361
  workflow_run_response: Optional[WorkflowRunOutput] = None,
362
+ run_context: Optional[RunContext] = None,
357
363
  session_state: Optional[Dict[str, Any]] = None,
358
364
  store_executor_outputs: bool = True,
359
365
  workflow_session: Optional[WorkflowSession] = None,
@@ -387,6 +393,7 @@ class Steps:
387
393
  user_id=user_id,
388
394
  workflow_run_response=workflow_run_response,
389
395
  store_executor_outputs=store_executor_outputs,
396
+ run_context=run_context,
390
397
  session_state=session_state,
391
398
  workflow_session=workflow_session,
392
399
  add_workflow_history_to_steps=add_workflow_history_to_steps,
@@ -439,6 +446,7 @@ class Steps:
439
446
  self,
440
447
  step_input: StepInput,
441
448
  workflow_run_response: WorkflowRunOutput,
449
+ run_context: Optional[RunContext] = None,
442
450
  session_state: Optional[Dict[str, Any]] = None,
443
451
  session_id: Optional[str] = None,
444
452
  user_id: Optional[str] = None,
@@ -504,6 +512,7 @@ class Steps:
504
512
  current_step_input,
505
513
  session_id=session_id,
506
514
  user_id=user_id,
515
+ run_context=run_context,
507
516
  session_state=session_state,
508
517
  stream_events=stream_events,
509
518
  stream_executor_events=stream_executor_events,
agno/workflow/types.py CHANGED
@@ -17,6 +17,7 @@ from agno.utils.media import (
17
17
  reconstruct_videos,
18
18
  )
19
19
  from agno.utils.serialize import json_serializer
20
+ from agno.utils.timer import Timer
20
21
 
21
22
 
22
23
  @dataclass
@@ -405,12 +406,18 @@ class WorkflowMetrics:
405
406
  """Complete metrics for a workflow execution"""
406
407
 
407
408
  steps: Dict[str, StepMetrics]
409
+ # Timer utility for tracking execution time
410
+ timer: Optional[Timer] = None
411
+ # Total workflow execution time
412
+ duration: Optional[float] = None
408
413
 
409
414
  def to_dict(self) -> Dict[str, Any]:
410
415
  """Convert to dictionary"""
411
- return {
416
+ result: Dict[str, Any] = {
412
417
  "steps": {name: step.to_dict() for name, step in self.steps.items()},
418
+ "duration": self.duration,
413
419
  }
420
+ return result
414
421
 
415
422
  @classmethod
416
423
  def from_dict(cls, data: Dict[str, Any]) -> "WorkflowMetrics":
@@ -419,8 +426,20 @@ class WorkflowMetrics:
419
426
 
420
427
  return cls(
421
428
  steps=steps,
429
+ duration=data.get("duration"),
422
430
  )
423
431
 
432
+ def start_timer(self):
433
+ if self.timer is None:
434
+ self.timer = Timer()
435
+ self.timer.start()
436
+
437
+ def stop_timer(self, set_duration: bool = True):
438
+ if self.timer is not None:
439
+ self.timer.stop()
440
+ if set_duration:
441
+ self.duration = self.timer.elapsed
442
+
424
443
 
425
444
  @dataclass
426
445
  class WebSocketHandler: