agno 2.1.2__py3-none-any.whl → 2.3.13__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 (314) hide show
  1. agno/agent/agent.py +5540 -2273
  2. agno/api/api.py +2 -0
  3. agno/api/os.py +1 -1
  4. agno/compression/__init__.py +3 -0
  5. agno/compression/manager.py +247 -0
  6. agno/culture/__init__.py +3 -0
  7. agno/culture/manager.py +956 -0
  8. agno/db/async_postgres/__init__.py +3 -0
  9. agno/db/base.py +689 -6
  10. agno/db/dynamo/dynamo.py +933 -37
  11. agno/db/dynamo/schemas.py +174 -10
  12. agno/db/dynamo/utils.py +63 -4
  13. agno/db/firestore/firestore.py +831 -9
  14. agno/db/firestore/schemas.py +51 -0
  15. agno/db/firestore/utils.py +102 -4
  16. agno/db/gcs_json/gcs_json_db.py +660 -12
  17. agno/db/gcs_json/utils.py +60 -26
  18. agno/db/in_memory/in_memory_db.py +287 -14
  19. agno/db/in_memory/utils.py +60 -2
  20. agno/db/json/json_db.py +590 -14
  21. agno/db/json/utils.py +60 -26
  22. agno/db/migrations/manager.py +199 -0
  23. agno/db/migrations/v1_to_v2.py +43 -13
  24. agno/db/migrations/versions/__init__.py +0 -0
  25. agno/db/migrations/versions/v2_3_0.py +938 -0
  26. agno/db/mongo/__init__.py +15 -1
  27. agno/db/mongo/async_mongo.py +2760 -0
  28. agno/db/mongo/mongo.py +879 -11
  29. agno/db/mongo/schemas.py +42 -0
  30. agno/db/mongo/utils.py +80 -8
  31. agno/db/mysql/__init__.py +2 -1
  32. agno/db/mysql/async_mysql.py +2912 -0
  33. agno/db/mysql/mysql.py +946 -68
  34. agno/db/mysql/schemas.py +72 -10
  35. agno/db/mysql/utils.py +198 -7
  36. agno/db/postgres/__init__.py +2 -1
  37. agno/db/postgres/async_postgres.py +2579 -0
  38. agno/db/postgres/postgres.py +942 -57
  39. agno/db/postgres/schemas.py +81 -18
  40. agno/db/postgres/utils.py +164 -2
  41. agno/db/redis/redis.py +671 -7
  42. agno/db/redis/schemas.py +50 -0
  43. agno/db/redis/utils.py +65 -7
  44. agno/db/schemas/__init__.py +2 -1
  45. agno/db/schemas/culture.py +120 -0
  46. agno/db/schemas/evals.py +1 -0
  47. agno/db/schemas/memory.py +17 -2
  48. agno/db/singlestore/schemas.py +63 -0
  49. agno/db/singlestore/singlestore.py +949 -83
  50. agno/db/singlestore/utils.py +60 -2
  51. agno/db/sqlite/__init__.py +2 -1
  52. agno/db/sqlite/async_sqlite.py +2911 -0
  53. agno/db/sqlite/schemas.py +62 -0
  54. agno/db/sqlite/sqlite.py +965 -46
  55. agno/db/sqlite/utils.py +169 -8
  56. agno/db/surrealdb/__init__.py +3 -0
  57. agno/db/surrealdb/metrics.py +292 -0
  58. agno/db/surrealdb/models.py +334 -0
  59. agno/db/surrealdb/queries.py +71 -0
  60. agno/db/surrealdb/surrealdb.py +1908 -0
  61. agno/db/surrealdb/utils.py +147 -0
  62. agno/db/utils.py +2 -0
  63. agno/eval/__init__.py +10 -0
  64. agno/eval/accuracy.py +75 -55
  65. agno/eval/agent_as_judge.py +861 -0
  66. agno/eval/base.py +29 -0
  67. agno/eval/performance.py +16 -7
  68. agno/eval/reliability.py +28 -16
  69. agno/eval/utils.py +35 -17
  70. agno/exceptions.py +27 -2
  71. agno/filters.py +354 -0
  72. agno/guardrails/prompt_injection.py +1 -0
  73. agno/hooks/__init__.py +3 -0
  74. agno/hooks/decorator.py +164 -0
  75. agno/integrations/discord/client.py +1 -1
  76. agno/knowledge/chunking/agentic.py +13 -10
  77. agno/knowledge/chunking/fixed.py +4 -1
  78. agno/knowledge/chunking/semantic.py +9 -4
  79. agno/knowledge/chunking/strategy.py +59 -15
  80. agno/knowledge/embedder/fastembed.py +1 -1
  81. agno/knowledge/embedder/nebius.py +1 -1
  82. agno/knowledge/embedder/ollama.py +8 -0
  83. agno/knowledge/embedder/openai.py +8 -8
  84. agno/knowledge/embedder/sentence_transformer.py +6 -2
  85. agno/knowledge/embedder/vllm.py +262 -0
  86. agno/knowledge/knowledge.py +1618 -318
  87. agno/knowledge/reader/base.py +6 -2
  88. agno/knowledge/reader/csv_reader.py +8 -10
  89. agno/knowledge/reader/docx_reader.py +5 -6
  90. agno/knowledge/reader/field_labeled_csv_reader.py +16 -20
  91. agno/knowledge/reader/json_reader.py +5 -4
  92. agno/knowledge/reader/markdown_reader.py +8 -8
  93. agno/knowledge/reader/pdf_reader.py +17 -19
  94. agno/knowledge/reader/pptx_reader.py +101 -0
  95. agno/knowledge/reader/reader_factory.py +32 -3
  96. agno/knowledge/reader/s3_reader.py +3 -3
  97. agno/knowledge/reader/tavily_reader.py +193 -0
  98. agno/knowledge/reader/text_reader.py +22 -10
  99. agno/knowledge/reader/web_search_reader.py +1 -48
  100. agno/knowledge/reader/website_reader.py +10 -10
  101. agno/knowledge/reader/wikipedia_reader.py +33 -1
  102. agno/knowledge/types.py +1 -0
  103. agno/knowledge/utils.py +72 -7
  104. agno/media.py +22 -6
  105. agno/memory/__init__.py +14 -1
  106. agno/memory/manager.py +544 -83
  107. agno/memory/strategies/__init__.py +15 -0
  108. agno/memory/strategies/base.py +66 -0
  109. agno/memory/strategies/summarize.py +196 -0
  110. agno/memory/strategies/types.py +37 -0
  111. agno/models/aimlapi/aimlapi.py +17 -0
  112. agno/models/anthropic/claude.py +515 -40
  113. agno/models/aws/bedrock.py +102 -21
  114. agno/models/aws/claude.py +131 -274
  115. agno/models/azure/ai_foundry.py +41 -19
  116. agno/models/azure/openai_chat.py +39 -8
  117. agno/models/base.py +1249 -525
  118. agno/models/cerebras/cerebras.py +91 -21
  119. agno/models/cerebras/cerebras_openai.py +21 -2
  120. agno/models/cohere/chat.py +40 -6
  121. agno/models/cometapi/cometapi.py +18 -1
  122. agno/models/dashscope/dashscope.py +2 -3
  123. agno/models/deepinfra/deepinfra.py +18 -1
  124. agno/models/deepseek/deepseek.py +69 -3
  125. agno/models/fireworks/fireworks.py +18 -1
  126. agno/models/google/gemini.py +877 -80
  127. agno/models/google/utils.py +22 -0
  128. agno/models/groq/groq.py +51 -18
  129. agno/models/huggingface/huggingface.py +17 -6
  130. agno/models/ibm/watsonx.py +16 -6
  131. agno/models/internlm/internlm.py +18 -1
  132. agno/models/langdb/langdb.py +13 -1
  133. agno/models/litellm/chat.py +44 -9
  134. agno/models/litellm/litellm_openai.py +18 -1
  135. agno/models/message.py +28 -5
  136. agno/models/meta/llama.py +47 -14
  137. agno/models/meta/llama_openai.py +22 -17
  138. agno/models/mistral/mistral.py +8 -4
  139. agno/models/nebius/nebius.py +6 -7
  140. agno/models/nvidia/nvidia.py +20 -3
  141. agno/models/ollama/chat.py +24 -8
  142. agno/models/openai/chat.py +104 -29
  143. agno/models/openai/responses.py +101 -81
  144. agno/models/openrouter/openrouter.py +60 -3
  145. agno/models/perplexity/perplexity.py +17 -1
  146. agno/models/portkey/portkey.py +7 -6
  147. agno/models/requesty/requesty.py +24 -4
  148. agno/models/response.py +73 -2
  149. agno/models/sambanova/sambanova.py +20 -3
  150. agno/models/siliconflow/siliconflow.py +19 -2
  151. agno/models/together/together.py +20 -3
  152. agno/models/utils.py +254 -8
  153. agno/models/vercel/v0.py +20 -3
  154. agno/models/vertexai/__init__.py +0 -0
  155. agno/models/vertexai/claude.py +190 -0
  156. agno/models/vllm/vllm.py +19 -14
  157. agno/models/xai/xai.py +19 -2
  158. agno/os/app.py +549 -152
  159. agno/os/auth.py +190 -3
  160. agno/os/config.py +23 -0
  161. agno/os/interfaces/a2a/router.py +8 -11
  162. agno/os/interfaces/a2a/utils.py +1 -1
  163. agno/os/interfaces/agui/router.py +18 -3
  164. agno/os/interfaces/agui/utils.py +152 -39
  165. agno/os/interfaces/slack/router.py +55 -37
  166. agno/os/interfaces/slack/slack.py +9 -1
  167. agno/os/interfaces/whatsapp/router.py +0 -1
  168. agno/os/interfaces/whatsapp/security.py +3 -1
  169. agno/os/mcp.py +110 -52
  170. agno/os/middleware/__init__.py +2 -0
  171. agno/os/middleware/jwt.py +676 -112
  172. agno/os/router.py +40 -1478
  173. agno/os/routers/agents/__init__.py +3 -0
  174. agno/os/routers/agents/router.py +599 -0
  175. agno/os/routers/agents/schema.py +261 -0
  176. agno/os/routers/evals/evals.py +96 -39
  177. agno/os/routers/evals/schemas.py +65 -33
  178. agno/os/routers/evals/utils.py +80 -10
  179. agno/os/routers/health.py +10 -4
  180. agno/os/routers/knowledge/knowledge.py +196 -38
  181. agno/os/routers/knowledge/schemas.py +82 -22
  182. agno/os/routers/memory/memory.py +279 -52
  183. agno/os/routers/memory/schemas.py +46 -17
  184. agno/os/routers/metrics/metrics.py +20 -8
  185. agno/os/routers/metrics/schemas.py +16 -16
  186. agno/os/routers/session/session.py +462 -34
  187. agno/os/routers/teams/__init__.py +3 -0
  188. agno/os/routers/teams/router.py +512 -0
  189. agno/os/routers/teams/schema.py +257 -0
  190. agno/os/routers/traces/__init__.py +3 -0
  191. agno/os/routers/traces/schemas.py +414 -0
  192. agno/os/routers/traces/traces.py +499 -0
  193. agno/os/routers/workflows/__init__.py +3 -0
  194. agno/os/routers/workflows/router.py +624 -0
  195. agno/os/routers/workflows/schema.py +75 -0
  196. agno/os/schema.py +256 -693
  197. agno/os/scopes.py +469 -0
  198. agno/os/utils.py +514 -36
  199. agno/reasoning/anthropic.py +80 -0
  200. agno/reasoning/gemini.py +73 -0
  201. agno/reasoning/openai.py +5 -0
  202. agno/reasoning/vertexai.py +76 -0
  203. agno/run/__init__.py +6 -0
  204. agno/run/agent.py +155 -32
  205. agno/run/base.py +55 -3
  206. agno/run/requirement.py +181 -0
  207. agno/run/team.py +125 -38
  208. agno/run/workflow.py +72 -18
  209. agno/session/agent.py +102 -89
  210. agno/session/summary.py +56 -15
  211. agno/session/team.py +164 -90
  212. agno/session/workflow.py +405 -40
  213. agno/table.py +10 -0
  214. agno/team/team.py +3974 -1903
  215. agno/tools/dalle.py +2 -4
  216. agno/tools/eleven_labs.py +23 -25
  217. agno/tools/exa.py +21 -16
  218. agno/tools/file.py +153 -23
  219. agno/tools/file_generation.py +16 -10
  220. agno/tools/firecrawl.py +15 -7
  221. agno/tools/function.py +193 -38
  222. agno/tools/gmail.py +238 -14
  223. agno/tools/google_drive.py +271 -0
  224. agno/tools/googlecalendar.py +36 -8
  225. agno/tools/googlesheets.py +20 -5
  226. agno/tools/jira.py +20 -0
  227. agno/tools/mcp/__init__.py +10 -0
  228. agno/tools/mcp/mcp.py +331 -0
  229. agno/tools/mcp/multi_mcp.py +347 -0
  230. agno/tools/mcp/params.py +24 -0
  231. agno/tools/mcp_toolbox.py +3 -3
  232. agno/tools/models/nebius.py +5 -5
  233. agno/tools/models_labs.py +20 -10
  234. agno/tools/nano_banana.py +151 -0
  235. agno/tools/notion.py +204 -0
  236. agno/tools/parallel.py +314 -0
  237. agno/tools/postgres.py +76 -36
  238. agno/tools/redshift.py +406 -0
  239. agno/tools/scrapegraph.py +1 -1
  240. agno/tools/shopify.py +1519 -0
  241. agno/tools/slack.py +18 -3
  242. agno/tools/spotify.py +919 -0
  243. agno/tools/tavily.py +146 -0
  244. agno/tools/toolkit.py +25 -0
  245. agno/tools/workflow.py +8 -1
  246. agno/tools/yfinance.py +12 -11
  247. agno/tracing/__init__.py +12 -0
  248. agno/tracing/exporter.py +157 -0
  249. agno/tracing/schemas.py +276 -0
  250. agno/tracing/setup.py +111 -0
  251. agno/utils/agent.py +938 -0
  252. agno/utils/cryptography.py +22 -0
  253. agno/utils/dttm.py +33 -0
  254. agno/utils/events.py +151 -3
  255. agno/utils/gemini.py +15 -5
  256. agno/utils/hooks.py +118 -4
  257. agno/utils/http.py +113 -2
  258. agno/utils/knowledge.py +12 -5
  259. agno/utils/log.py +1 -0
  260. agno/utils/mcp.py +92 -2
  261. agno/utils/media.py +187 -1
  262. agno/utils/merge_dict.py +3 -3
  263. agno/utils/message.py +60 -0
  264. agno/utils/models/ai_foundry.py +9 -2
  265. agno/utils/models/claude.py +49 -14
  266. agno/utils/models/cohere.py +9 -2
  267. agno/utils/models/llama.py +9 -2
  268. agno/utils/models/mistral.py +4 -2
  269. agno/utils/print_response/agent.py +109 -16
  270. agno/utils/print_response/team.py +223 -30
  271. agno/utils/print_response/workflow.py +251 -34
  272. agno/utils/streamlit.py +1 -1
  273. agno/utils/team.py +98 -9
  274. agno/utils/tokens.py +657 -0
  275. agno/vectordb/base.py +39 -7
  276. agno/vectordb/cassandra/cassandra.py +21 -5
  277. agno/vectordb/chroma/chromadb.py +43 -12
  278. agno/vectordb/clickhouse/clickhousedb.py +21 -5
  279. agno/vectordb/couchbase/couchbase.py +29 -5
  280. agno/vectordb/lancedb/lance_db.py +92 -181
  281. agno/vectordb/langchaindb/langchaindb.py +24 -4
  282. agno/vectordb/lightrag/lightrag.py +17 -3
  283. agno/vectordb/llamaindex/llamaindexdb.py +25 -5
  284. agno/vectordb/milvus/milvus.py +50 -37
  285. agno/vectordb/mongodb/__init__.py +7 -1
  286. agno/vectordb/mongodb/mongodb.py +36 -30
  287. agno/vectordb/pgvector/pgvector.py +201 -77
  288. agno/vectordb/pineconedb/pineconedb.py +41 -23
  289. agno/vectordb/qdrant/qdrant.py +67 -54
  290. agno/vectordb/redis/__init__.py +9 -0
  291. agno/vectordb/redis/redisdb.py +682 -0
  292. agno/vectordb/singlestore/singlestore.py +50 -29
  293. agno/vectordb/surrealdb/surrealdb.py +31 -41
  294. agno/vectordb/upstashdb/upstashdb.py +34 -6
  295. agno/vectordb/weaviate/weaviate.py +53 -14
  296. agno/workflow/__init__.py +2 -0
  297. agno/workflow/agent.py +299 -0
  298. agno/workflow/condition.py +120 -18
  299. agno/workflow/loop.py +77 -10
  300. agno/workflow/parallel.py +231 -143
  301. agno/workflow/router.py +118 -17
  302. agno/workflow/step.py +609 -170
  303. agno/workflow/steps.py +73 -6
  304. agno/workflow/types.py +96 -21
  305. agno/workflow/workflow.py +2039 -262
  306. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/METADATA +201 -66
  307. agno-2.3.13.dist-info/RECORD +613 -0
  308. agno/tools/googlesearch.py +0 -98
  309. agno/tools/mcp.py +0 -679
  310. agno/tools/memori.py +0 -339
  311. agno-2.1.2.dist-info/RECORD +0 -543
  312. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/WHEEL +0 -0
  313. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/licenses/LICENSE +0 -0
  314. {agno-2.1.2.dist-info → agno-2.3.13.dist-info}/top_level.txt +0 -0
agno/workflow/step.py CHANGED
@@ -1,15 +1,21 @@
1
1
  import inspect
2
+ import warnings
2
3
  from copy import copy
3
4
  from dataclasses import dataclass
4
- from typing import Any, AsyncIterator, Awaitable, Callable, Dict, Iterator, List, Optional, Union
5
+ from typing import Any, AsyncIterator, Awaitable, Callable, Dict, Iterator, List, Optional, Union, cast
5
6
  from uuid import uuid4
6
7
 
7
8
  from pydantic import BaseModel
9
+ from typing_extensions import TypeGuard
8
10
 
9
11
  from agno.agent import Agent
10
12
  from agno.media import Audio, Image, Video
13
+ from agno.models.message import Message
11
14
  from agno.models.metrics import Metrics
12
- from agno.run.agent import RunOutput
15
+ from agno.run import RunContext
16
+ from agno.run.agent import RunContentEvent, RunOutput
17
+ from agno.run.base import BaseRunOutputEvent
18
+ from agno.run.team import RunContentEvent as TeamRunContentEvent
13
19
  from agno.run.team import TeamRunOutput
14
20
  from agno.run.workflow import (
15
21
  StepCompletedEvent,
@@ -17,8 +23,11 @@ from agno.run.workflow import (
17
23
  WorkflowRunOutput,
18
24
  WorkflowRunOutputEvent,
19
25
  )
26
+ from agno.session.agent import AgentSession
27
+ from agno.session.team import TeamSession
28
+ from agno.session.workflow import WorkflowSession
20
29
  from agno.team import Team
21
- from agno.utils.log import log_debug, logger, use_agent_logger, use_team_logger, use_workflow_logger
30
+ from agno.utils.log import log_debug, log_warning, logger, use_agent_logger, use_team_logger, use_workflow_logger
22
31
  from agno.utils.merge_dict import merge_dictionaries
23
32
  from agno.workflow.types import StepInput, StepOutput, StepType
24
33
 
@@ -60,6 +69,9 @@ class Step:
60
69
  # If False, only warn about missing inputs
61
70
  strict_input_validation: bool = False
62
71
 
72
+ add_workflow_history: Optional[bool] = None
73
+ num_history_runs: int = 3
74
+
63
75
  _retry_count: int = 0
64
76
 
65
77
  def __init__(
@@ -74,6 +86,8 @@ class Step:
74
86
  timeout_seconds: Optional[int] = None,
75
87
  skip_on_failure: bool = False,
76
88
  strict_input_validation: bool = False,
89
+ add_workflow_history: Optional[bool] = None,
90
+ num_history_runs: int = 3,
77
91
  ):
78
92
  # Auto-detect name for function executors if not provided
79
93
  if name is None and executor is not None:
@@ -93,6 +107,8 @@ class Step:
93
107
  self.timeout_seconds = timeout_seconds
94
108
  self.skip_on_failure = skip_on_failure
95
109
  self.strict_input_validation = strict_input_validation
110
+ self.add_workflow_history = add_workflow_history
111
+ self.num_history_runs = num_history_runs
96
112
  self.step_id = step_id
97
113
 
98
114
  if step_id is None:
@@ -169,32 +185,37 @@ class Step:
169
185
  func: Callable,
170
186
  step_input: StepInput,
171
187
  session_state: Optional[Dict[str, Any]] = None,
188
+ run_context: Optional[RunContext] = None,
172
189
  ) -> Any:
173
190
  """Call custom function with session_state support if the function accepts it"""
191
+
192
+ kwargs: Dict[str, Any] = {}
193
+ if run_context is not None and self._function_has_run_context_param():
194
+ kwargs["run_context"] = run_context
174
195
  if session_state is not None and self._function_has_session_state_param():
175
- return func(step_input, session_state)
176
- else:
177
- return func(step_input)
196
+ kwargs["session_state"] = session_state
197
+
198
+ return func(step_input, **kwargs)
178
199
 
179
200
  async def _acall_custom_function(
180
201
  self,
181
202
  func: Callable,
182
203
  step_input: StepInput,
183
204
  session_state: Optional[Dict[str, Any]] = None,
205
+ run_context: Optional[RunContext] = None,
184
206
  ) -> Any:
185
207
  """Call custom async function with session_state support if the function accepts it"""
186
- import inspect
187
208
 
188
- if inspect.isasyncgenfunction(func):
189
- if session_state is not None and self._function_has_session_state_param():
190
- return func(step_input, session_state)
191
- else:
192
- return func(step_input)
209
+ kwargs: Dict[str, Any] = {}
210
+ if run_context is not None and self._function_has_run_context_param():
211
+ kwargs["run_context"] = run_context
212
+ if session_state is not None and self._function_has_session_state_param():
213
+ kwargs["session_state"] = session_state
214
+
215
+ if _is_async_generator_function(func):
216
+ return func(step_input, **kwargs)
193
217
  else:
194
- if session_state is not None and self._function_has_session_state_param():
195
- return await func(step_input, session_state)
196
- else:
197
- return await func(step_input)
218
+ return await func(step_input, **kwargs)
198
219
 
199
220
  def execute(
200
221
  self,
@@ -202,8 +223,13 @@ class Step:
202
223
  session_id: Optional[str] = None,
203
224
  user_id: Optional[str] = None,
204
225
  workflow_run_response: Optional["WorkflowRunOutput"] = None,
226
+ run_context: Optional[RunContext] = None,
205
227
  session_state: Optional[Dict[str, Any]] = None,
206
228
  store_executor_outputs: bool = True,
229
+ workflow_session: Optional[WorkflowSession] = None,
230
+ add_workflow_history_to_steps: Optional[bool] = False,
231
+ num_history_runs: int = 3,
232
+ background_tasks: Optional[Any] = None,
207
233
  ) -> StepOutput:
208
234
  """Execute the step with StepInput, returning final StepOutput (non-streaming)"""
209
235
  log_debug(f"Executing step: {self.name}")
@@ -211,41 +237,65 @@ class Step:
211
237
  if step_input.previous_step_outputs:
212
238
  step_input.previous_step_content = step_input.get_last_step_content()
213
239
 
214
- session_state_copy = copy(session_state) if session_state is not None else {}
240
+ if workflow_session:
241
+ step_input.workflow_session = workflow_session
242
+
243
+ # Create session_state copy once to avoid duplication.
244
+ # Consider both run_context.session_state and session_state.
245
+ if run_context is not None and run_context.session_state is not None:
246
+ session_state_copy = run_context.session_state
247
+ else:
248
+ session_state_copy = copy(session_state) if session_state is not None else {}
215
249
 
216
250
  # Execute with retries
217
251
  for attempt in range(self.max_retries + 1):
218
252
  try:
219
253
  response: Union[RunOutput, TeamRunOutput, StepOutput]
220
254
  if self._executor_type == "function":
221
- if inspect.iscoroutinefunction(self.active_executor) or inspect.isasyncgenfunction(
222
- self.active_executor
223
- ):
255
+ if _is_async_callable(self.active_executor) or _is_async_generator_function(self.active_executor):
224
256
  raise ValueError("Cannot use async function with synchronous execution")
225
- if inspect.isgeneratorfunction(self.active_executor):
257
+ if _is_generator_function(self.active_executor):
226
258
  content = ""
227
259
  final_response = None
228
260
  try:
229
261
  for chunk in self._call_custom_function(
230
- self.active_executor, step_input, session_state_copy
262
+ self.active_executor,
263
+ step_input,
264
+ session_state_copy, # type: ignore[arg-type]
265
+ run_context,
231
266
  ): # type: ignore
232
- if (
233
- hasattr(chunk, "content")
234
- and chunk.content is not None
235
- and isinstance(chunk.content, str)
236
- ):
237
- content += chunk.content
267
+ if isinstance(chunk, (BaseRunOutputEvent)):
268
+ if (
269
+ isinstance(chunk, (RunContentEvent, TeamRunContentEvent))
270
+ and chunk.content is not None
271
+ ):
272
+ # Its a regular chunk of content
273
+ if isinstance(chunk.content, str):
274
+ content += chunk.content
275
+ # Its the BaseModel object, set it as the content. Replace any previous content.
276
+ # There should be no previous str content at this point
277
+ elif isinstance(chunk.content, BaseModel):
278
+ content = chunk.content # type: ignore[assignment]
279
+ else:
280
+ # Case when parse_response is False and the content is a dict
281
+ content += str(chunk.content)
282
+ elif isinstance(chunk, (RunOutput, TeamRunOutput)):
283
+ # This is the final response from the agent/team
284
+ content = chunk.content # type: ignore[assignment]
285
+ # If the chunk is a StepOutput, use it as the final response
286
+ elif isinstance(chunk, StepOutput):
287
+ final_response = chunk
288
+ break
289
+ # Non Agent/Team data structure that was yielded
238
290
  else:
239
291
  content += str(chunk)
240
- if isinstance(chunk, StepOutput):
241
- final_response = chunk
242
292
 
243
293
  except StopIteration as e:
244
294
  if hasattr(e, "value") and isinstance(e.value, StepOutput):
245
295
  final_response = e.value
246
296
 
247
297
  # Merge session_state changes back
248
- if session_state is not None:
298
+ if run_context is None and session_state is not None:
249
299
  merge_dictionaries(session_state, session_state_copy)
250
300
 
251
301
  if final_response is not None:
@@ -254,15 +304,22 @@ class Step:
254
304
  response = StepOutput(content=content)
255
305
  else:
256
306
  # Execute function with signature inspection for session_state support
257
- result = self._call_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
307
+ result = self._call_custom_function(
308
+ self.active_executor, # type: ignore[arg-type]
309
+ step_input,
310
+ session_state_copy,
311
+ run_context,
312
+ )
258
313
 
259
314
  # Merge session_state changes back
260
- if session_state is not None:
315
+ if run_context is None and session_state is not None:
261
316
  merge_dictionaries(session_state, session_state_copy)
262
317
 
263
318
  # If function returns StepOutput, use it directly
264
319
  if isinstance(result, StepOutput):
265
320
  response = result
321
+ elif isinstance(result, (RunOutput, TeamRunOutput)):
322
+ response = StepOutput(content=result.content)
266
323
  else:
267
324
  response = StepOutput(content=str(result))
268
325
  else:
@@ -292,8 +349,26 @@ class Step:
292
349
  if isinstance(self.active_executor, Team):
293
350
  kwargs["store_member_responses"] = True
294
351
 
352
+ # Forward background_tasks if provided
353
+ if background_tasks is not None:
354
+ kwargs["background_tasks"] = background_tasks
355
+
356
+ num_history_runs = self.num_history_runs if self.num_history_runs else num_history_runs
357
+
358
+ use_history = (
359
+ self.add_workflow_history
360
+ if self.add_workflow_history is not None
361
+ else add_workflow_history_to_steps
362
+ )
363
+
364
+ final_message = message
365
+ if use_history and workflow_session:
366
+ history_messages = workflow_session.get_workflow_history_context(num_runs=num_history_runs)
367
+ if history_messages:
368
+ final_message = f"{history_messages}{message}"
369
+
295
370
  response = self.active_executor.run( # type: ignore
296
- input=message, # type: ignore
371
+ input=final_message, # type: ignore
297
372
  images=images,
298
373
  videos=videos,
299
374
  audio=audios,
@@ -301,12 +376,13 @@ class Step:
301
376
  session_id=session_id,
302
377
  user_id=user_id,
303
378
  session_state=session_state_copy, # Send a copy to the executor
379
+ run_context=run_context,
304
380
  **kwargs,
305
381
  )
306
382
 
307
- if session_state is not None:
308
- # Update workflow session state
309
- merge_dictionaries(session_state, session_state_copy) # type: ignore
383
+ # Update workflow session state
384
+ if run_context is None and session_state is not None:
385
+ merge_dictionaries(session_state, session_state_copy)
310
386
 
311
387
  if store_executor_outputs and workflow_run_response is not None:
312
388
  self._store_executor_response(workflow_run_response, response) # type: ignore
@@ -335,41 +411,98 @@ class Step:
335
411
 
336
412
  return StepOutput(content=f"Step {self.name} failed but skipped", success=False)
337
413
 
414
+ def _function_has_run_context_param(self) -> bool:
415
+ """Check if the custom function has a run_context parameter"""
416
+ if self._executor_type != "function":
417
+ return False
418
+
419
+ try:
420
+ sig = inspect.signature(self.active_executor) # type: ignore
421
+ return "run_context" in sig.parameters
422
+ except Exception:
423
+ return False
424
+
338
425
  def _function_has_session_state_param(self) -> bool:
339
426
  """Check if the custom function has a session_state parameter"""
340
427
  if self._executor_type != "function":
341
428
  return False
342
429
 
343
430
  try:
344
- from inspect import signature
345
-
346
- sig = signature(self.active_executor) # type: ignore
431
+ sig = inspect.signature(self.active_executor) # type: ignore
347
432
  return "session_state" in sig.parameters
348
433
  except Exception:
349
434
  return False
350
435
 
436
+ def _enrich_event_with_context(
437
+ self,
438
+ event: Any,
439
+ workflow_run_response: Optional["WorkflowRunOutput"] = None,
440
+ step_index: Optional[Union[int, tuple]] = None,
441
+ ) -> Any:
442
+ """Enrich event with step and workflow context information"""
443
+ if workflow_run_response is None:
444
+ return event
445
+ if hasattr(event, "workflow_id"):
446
+ event.workflow_id = workflow_run_response.workflow_id
447
+ if hasattr(event, "workflow_run_id"):
448
+ event.workflow_run_id = workflow_run_response.run_id
449
+ if hasattr(event, "step_id"):
450
+ event.step_id = self.step_id
451
+ if hasattr(event, "step_name") and self.name is not None:
452
+ if getattr(event, "step_name", None) is None:
453
+ event.step_name = self.name
454
+ # Only set step_index if it's not already set (preserve parallel.py's tuples)
455
+ if hasattr(event, "step_index") and step_index is not None:
456
+ if event.step_index is None:
457
+ event.step_index = step_index
458
+
459
+ return event
460
+
351
461
  def execute_stream(
352
462
  self,
353
463
  step_input: StepInput,
354
464
  session_id: Optional[str] = None,
355
465
  user_id: Optional[str] = None,
466
+ stream_events: bool = False,
356
467
  stream_intermediate_steps: bool = False,
468
+ stream_executor_events: bool = True,
357
469
  workflow_run_response: Optional["WorkflowRunOutput"] = None,
470
+ run_context: Optional[RunContext] = None,
358
471
  session_state: Optional[Dict[str, Any]] = None,
359
472
  step_index: Optional[Union[int, tuple]] = None,
360
473
  store_executor_outputs: bool = True,
361
474
  parent_step_id: Optional[str] = None,
475
+ workflow_session: Optional["WorkflowSession"] = None,
476
+ add_workflow_history_to_steps: Optional[bool] = False,
477
+ num_history_runs: int = 3,
478
+ background_tasks: Optional[Any] = None,
362
479
  ) -> Iterator[Union[WorkflowRunOutputEvent, StepOutput]]:
363
480
  """Execute the step with event-driven streaming support"""
364
481
 
365
482
  if step_input.previous_step_outputs:
366
483
  step_input.previous_step_content = step_input.get_last_step_content()
367
484
 
368
- # Create session_state copy once to avoid duplication
369
- session_state_copy = copy(session_state) if session_state is not None else {}
485
+ if workflow_session:
486
+ step_input.workflow_session = workflow_session
487
+
488
+ # Create session_state copy once to avoid duplication.
489
+ # Consider both run_context.session_state and session_state.
490
+ if run_context is not None and run_context.session_state is not None:
491
+ session_state_copy = run_context.session_state
492
+ else:
493
+ session_state_copy = copy(session_state) if session_state is not None else {}
494
+
495
+ # Considering both stream_events and stream_intermediate_steps (deprecated)
496
+ if stream_intermediate_steps is not None:
497
+ warnings.warn(
498
+ "The 'stream_intermediate_steps' parameter is deprecated and will be removed in future versions. Use 'stream_events' instead.",
499
+ DeprecationWarning,
500
+ stacklevel=2,
501
+ )
502
+ stream_events = stream_events or stream_intermediate_steps
370
503
 
371
504
  # Emit StepStartedEvent
372
- if stream_intermediate_steps and workflow_run_response:
505
+ if stream_events and workflow_run_response:
373
506
  yield StepStartedEvent(
374
507
  run_id=workflow_run_response.run_id or "",
375
508
  workflow_name=workflow_run_response.workflow_name or "",
@@ -389,34 +522,46 @@ class Step:
389
522
 
390
523
  if self._executor_type == "function":
391
524
  log_debug(f"Executing function executor for step: {self.name}")
392
-
393
- if inspect.iscoroutinefunction(self.active_executor) or inspect.isasyncgenfunction(
394
- self.active_executor
395
- ):
525
+ if _is_async_callable(self.active_executor) or _is_async_generator_function(self.active_executor):
396
526
  raise ValueError("Cannot use async function with synchronous execution")
397
-
398
- if inspect.isgeneratorfunction(self.active_executor):
527
+ if _is_generator_function(self.active_executor):
399
528
  log_debug("Function returned iterable, streaming events")
400
529
  content = ""
401
530
  try:
402
- iterator = self._call_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
531
+ iterator = self._call_custom_function(
532
+ self.active_executor,
533
+ step_input,
534
+ session_state_copy,
535
+ run_context,
536
+ )
403
537
  for event in iterator: # type: ignore
404
- if (
405
- hasattr(event, "content")
406
- and event.content is not None
407
- and isinstance(event.content, str)
408
- ):
409
- content += event.content
410
- else:
411
- content += str(event)
412
- if isinstance(event, StepOutput):
538
+ if isinstance(event, (BaseRunOutputEvent)):
539
+ if (
540
+ isinstance(event, (RunContentEvent, TeamRunContentEvent))
541
+ and event.content is not None
542
+ ):
543
+ if isinstance(event.content, str):
544
+ content += event.content
545
+ elif isinstance(event.content, BaseModel):
546
+ content = event.content # type: ignore[assignment]
547
+ else:
548
+ content = str(event.content)
549
+ # Only yield executor events if stream_executor_events is True
550
+ if stream_executor_events:
551
+ enriched_event = self._enrich_event_with_context(
552
+ event, workflow_run_response, step_index
553
+ )
554
+ yield enriched_event # type: ignore[misc]
555
+ elif isinstance(event, (RunOutput, TeamRunOutput)):
556
+ content = event.content # type: ignore[assignment]
557
+ elif isinstance(event, StepOutput):
413
558
  final_response = event
414
559
  break
415
560
  else:
416
- yield event # type: ignore[misc]
561
+ content += str(event)
417
562
 
418
563
  # Merge session_state changes back
419
- if session_state is not None:
564
+ if run_context is None and session_state is not None:
420
565
  merge_dictionaries(session_state, session_state_copy)
421
566
 
422
567
  if not final_response:
@@ -426,14 +571,21 @@ class Step:
426
571
  final_response = e.value
427
572
 
428
573
  else:
429
- result = self._call_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
574
+ result = self._call_custom_function(
575
+ self.active_executor, # type: ignore[arg-type]
576
+ step_input,
577
+ session_state_copy,
578
+ run_context,
579
+ )
430
580
 
431
581
  # Merge session_state changes back
432
- if session_state is not None:
582
+ if run_context is None and session_state is not None:
433
583
  merge_dictionaries(session_state, session_state_copy)
434
584
 
435
585
  if isinstance(result, StepOutput):
436
586
  final_response = result
587
+ elif isinstance(result, (RunOutput, TeamRunOutput)):
588
+ final_response = StepOutput(content=result.content)
437
589
  else:
438
590
  final_response = StepOutput(content=str(result))
439
591
  log_debug("Function returned non-iterable, created StepOutput")
@@ -463,8 +615,26 @@ class Step:
463
615
  if isinstance(self.active_executor, Team):
464
616
  kwargs["store_member_responses"] = True
465
617
 
618
+ # Forward background_tasks if provided
619
+ if background_tasks is not None:
620
+ kwargs["background_tasks"] = background_tasks
621
+
622
+ num_history_runs = self.num_history_runs if self.num_history_runs else num_history_runs
623
+
624
+ use_history = (
625
+ self.add_workflow_history
626
+ if self.add_workflow_history is not None
627
+ else add_workflow_history_to_steps
628
+ )
629
+
630
+ final_message = message
631
+ if use_history and workflow_session:
632
+ history_messages = workflow_session.get_workflow_history_context(num_runs=num_history_runs)
633
+ if history_messages:
634
+ final_message = f"{history_messages}{message}"
635
+
466
636
  response_stream = self.active_executor.run( # type: ignore[call-overload, misc]
467
- input=message,
637
+ input=final_message,
468
638
  images=images,
469
639
  videos=videos,
470
640
  audio=audios,
@@ -473,16 +643,9 @@ class Step:
473
643
  user_id=user_id,
474
644
  session_state=session_state_copy, # Send a copy to the executor
475
645
  stream=True,
476
- stream_intermediate_steps=stream_intermediate_steps,
477
- # Pass workflow context directly via kwargs
478
- workflow_context={
479
- "workflow_id": workflow_run_response.workflow_id if workflow_run_response else None,
480
- "workflow_run_id": workflow_run_response.run_id if workflow_run_response else None,
481
- "step_id": self.step_id,
482
- "step_name": self.name,
483
- "step_index": step_index,
484
- },
485
- yield_run_response=True,
646
+ stream_events=stream_events,
647
+ yield_run_output=True,
648
+ run_context=run_context,
486
649
  **kwargs,
487
650
  )
488
651
 
@@ -490,12 +653,17 @@ class Step:
490
653
  for event in response_stream:
491
654
  if isinstance(event, RunOutput) or isinstance(event, TeamRunOutput):
492
655
  active_executor_run_response = event
493
- break
494
- yield event # type: ignore[misc]
495
-
496
- if session_state is not None:
497
- # Update workflow session state
498
- merge_dictionaries(session_state, session_state_copy) # type: ignore
656
+ continue
657
+ # Only yield executor events if stream_executor_events is True
658
+ if stream_executor_events:
659
+ enriched_event = self._enrich_event_with_context(
660
+ event, workflow_run_response, step_index
661
+ )
662
+ yield enriched_event # type: ignore[misc]
663
+
664
+ # Update workflow session state
665
+ if run_context is None and session_state is not None:
666
+ merge_dictionaries(session_state, session_state_copy)
499
667
 
500
668
  if store_executor_outputs and workflow_run_response is not None:
501
669
  self._store_executor_response(workflow_run_response, active_executor_run_response) # type: ignore
@@ -518,7 +686,7 @@ class Step:
518
686
  yield final_response
519
687
 
520
688
  # Emit StepCompletedEvent
521
- if stream_intermediate_steps and workflow_run_response:
689
+ if stream_events and workflow_run_response:
522
690
  yield StepCompletedEvent(
523
691
  run_id=workflow_run_response.run_id or "",
524
692
  workflow_name=workflow_run_response.workflow_name or "",
@@ -556,8 +724,13 @@ class Step:
556
724
  session_id: Optional[str] = None,
557
725
  user_id: Optional[str] = None,
558
726
  workflow_run_response: Optional["WorkflowRunOutput"] = None,
727
+ run_context: Optional[RunContext] = None,
559
728
  session_state: Optional[Dict[str, Any]] = None,
560
729
  store_executor_outputs: bool = True,
730
+ workflow_session: Optional["WorkflowSession"] = None,
731
+ add_workflow_history_to_steps: Optional[bool] = False,
732
+ num_history_runs: int = 3,
733
+ background_tasks: Optional[Any] = None,
561
734
  ) -> StepOutput:
562
735
  """Execute the step with StepInput, returning final StepOutput (non-streaming)"""
563
736
  logger.info(f"Executing async step (non-streaming): {self.name}")
@@ -566,59 +739,87 @@ class Step:
566
739
  if step_input.previous_step_outputs:
567
740
  step_input.previous_step_content = step_input.get_last_step_content()
568
741
 
569
- # Create session_state copy once to avoid duplication
570
- session_state_copy = copy(session_state) if session_state is not None else {}
742
+ if workflow_session:
743
+ step_input.workflow_session = workflow_session
744
+
745
+ # Create session_state copy once to avoid duplication.
746
+ # Consider both run_context.session_state and session_state.
747
+ if run_context is not None and run_context.session_state is not None:
748
+ session_state_copy = run_context.session_state
749
+ else:
750
+ session_state_copy = copy(session_state) if session_state is not None else {}
571
751
 
572
752
  # Execute with retries
573
753
  for attempt in range(self.max_retries + 1):
574
754
  try:
575
755
  if self._executor_type == "function":
576
- import inspect
577
-
578
- if inspect.isgeneratorfunction(self.active_executor) or inspect.isasyncgenfunction(
756
+ if _is_generator_function(self.active_executor) or _is_async_generator_function(
579
757
  self.active_executor
580
758
  ):
581
759
  content = ""
582
760
  final_response = None
583
761
  try:
584
- if inspect.isgeneratorfunction(self.active_executor):
762
+ if _is_generator_function(self.active_executor):
585
763
  iterator = self._call_custom_function(
586
- self.active_executor, step_input, session_state_copy
587
- ) # type: ignore
764
+ self.active_executor,
765
+ step_input,
766
+ session_state_copy,
767
+ run_context,
768
+ )
588
769
  for chunk in iterator: # type: ignore
589
- if (
590
- hasattr(chunk, "content")
591
- and chunk.content is not None
592
- and isinstance(chunk.content, str)
593
- ):
594
- content += chunk.content
770
+ if isinstance(chunk, (BaseRunOutputEvent)):
771
+ if (
772
+ isinstance(chunk, (RunContentEvent, TeamRunContentEvent))
773
+ and chunk.content is not None
774
+ ):
775
+ if isinstance(chunk.content, str):
776
+ content += chunk.content
777
+ elif isinstance(chunk.content, BaseModel):
778
+ content = chunk.content # type: ignore[assignment]
779
+ else:
780
+ content = str(chunk.content)
781
+ elif isinstance(chunk, (RunOutput, TeamRunOutput)):
782
+ content = chunk.content # type: ignore[assignment]
783
+ elif isinstance(chunk, StepOutput):
784
+ final_response = chunk
785
+ break
595
786
  else:
596
787
  content += str(chunk)
597
- if isinstance(chunk, StepOutput):
598
- final_response = chunk
788
+
599
789
  else:
600
- if inspect.isasyncgenfunction(self.active_executor):
790
+ if _is_async_generator_function(self.active_executor):
601
791
  iterator = await self._acall_custom_function(
602
- self.active_executor, step_input, session_state_copy
603
- ) # type: ignore
792
+ self.active_executor,
793
+ step_input,
794
+ session_state_copy,
795
+ run_context,
796
+ )
604
797
  async for chunk in iterator: # type: ignore
605
- if (
606
- hasattr(chunk, "content")
607
- and chunk.content is not None
608
- and isinstance(chunk.content, str)
609
- ):
610
- content += chunk.content
798
+ if isinstance(chunk, (BaseRunOutputEvent)):
799
+ if (
800
+ isinstance(chunk, (RunContentEvent, TeamRunContentEvent))
801
+ and chunk.content is not None
802
+ ):
803
+ if isinstance(chunk.content, str):
804
+ content += chunk.content
805
+ elif isinstance(chunk.content, BaseModel):
806
+ content = chunk.content # type: ignore[assignment]
807
+ else:
808
+ content = str(chunk.content)
809
+ elif isinstance(chunk, (RunOutput, TeamRunOutput)):
810
+ content = chunk.content # type: ignore[assignment]
811
+ elif isinstance(chunk, StepOutput):
812
+ final_response = chunk
813
+ break
611
814
  else:
612
815
  content += str(chunk)
613
- if isinstance(chunk, StepOutput):
614
- final_response = chunk
615
816
 
616
817
  except StopIteration as e:
617
818
  if hasattr(e, "value") and isinstance(e.value, StepOutput):
618
819
  final_response = e.value
619
820
 
620
821
  # Merge session_state changes back
621
- if session_state is not None:
822
+ if run_context is None and session_state is not None:
622
823
  merge_dictionaries(session_state, session_state_copy)
623
824
 
624
825
  if final_response is not None:
@@ -626,20 +827,30 @@ class Step:
626
827
  else:
627
828
  response = StepOutput(content=content)
628
829
  else:
629
- if inspect.iscoroutinefunction(self.active_executor):
830
+ if _is_async_callable(self.active_executor):
630
831
  result = await self._acall_custom_function(
631
- self.active_executor, step_input, session_state_copy
632
- ) # type: ignore
832
+ self.active_executor,
833
+ step_input,
834
+ session_state_copy,
835
+ run_context,
836
+ )
633
837
  else:
634
- result = self._call_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
838
+ result = self._call_custom_function(
839
+ self.active_executor, # type: ignore[arg-type]
840
+ step_input,
841
+ session_state_copy,
842
+ run_context,
843
+ )
635
844
 
636
845
  # Merge session_state changes back
637
- if session_state is not None:
846
+ if run_context is None and session_state is not None:
638
847
  merge_dictionaries(session_state, session_state_copy)
639
848
 
640
849
  # If function returns StepOutput, use it directly
641
850
  if isinstance(result, StepOutput):
642
851
  response = result
852
+ elif isinstance(result, (RunOutput, TeamRunOutput)):
853
+ response = StepOutput(content=result.content)
643
854
  else:
644
855
  response = StepOutput(content=str(result))
645
856
 
@@ -670,8 +881,26 @@ class Step:
670
881
  if isinstance(self.active_executor, Team):
671
882
  kwargs["store_member_responses"] = True
672
883
 
884
+ # Forward background_tasks if provided
885
+ if background_tasks is not None:
886
+ kwargs["background_tasks"] = background_tasks
887
+
888
+ num_history_runs = self.num_history_runs if self.num_history_runs else num_history_runs
889
+
890
+ use_history = (
891
+ self.add_workflow_history
892
+ if self.add_workflow_history is not None
893
+ else add_workflow_history_to_steps
894
+ )
895
+
896
+ final_message = message
897
+ if use_history and workflow_session:
898
+ history_messages = workflow_session.get_workflow_history_context(num_runs=num_history_runs)
899
+ if history_messages:
900
+ final_message = f"{history_messages}{message}"
901
+
673
902
  response = await self.active_executor.arun( # type: ignore
674
- input=message, # type: ignore
903
+ input=final_message, # type: ignore
675
904
  images=images,
676
905
  videos=videos,
677
906
  audio=audios,
@@ -679,12 +908,13 @@ class Step:
679
908
  session_id=session_id,
680
909
  user_id=user_id,
681
910
  session_state=session_state_copy,
911
+ run_context=run_context,
682
912
  **kwargs,
683
913
  )
684
914
 
685
- if session_state is not None:
686
- # Update workflow session state
687
- merge_dictionaries(session_state, session_state_copy) # type: ignore
915
+ # Update workflow session state
916
+ if run_context is None and session_state is not None:
917
+ merge_dictionaries(session_state, session_state_copy)
688
918
 
689
919
  if store_executor_outputs and workflow_run_response is not None:
690
920
  self._store_executor_response(workflow_run_response, response) # type: ignore
@@ -718,22 +948,45 @@ class Step:
718
948
  step_input: StepInput,
719
949
  session_id: Optional[str] = None,
720
950
  user_id: Optional[str] = None,
951
+ stream_events: bool = False,
721
952
  stream_intermediate_steps: bool = False,
953
+ stream_executor_events: bool = True,
722
954
  workflow_run_response: Optional["WorkflowRunOutput"] = None,
955
+ run_context: Optional[RunContext] = None,
723
956
  session_state: Optional[Dict[str, Any]] = None,
724
957
  step_index: Optional[Union[int, tuple]] = None,
725
958
  store_executor_outputs: bool = True,
726
959
  parent_step_id: Optional[str] = None,
960
+ workflow_session: Optional["WorkflowSession"] = None,
961
+ add_workflow_history_to_steps: Optional[bool] = False,
962
+ num_history_runs: int = 3,
963
+ background_tasks: Optional[Any] = None,
727
964
  ) -> AsyncIterator[Union[WorkflowRunOutputEvent, StepOutput]]:
728
965
  """Execute the step with event-driven streaming support"""
729
966
 
730
967
  if step_input.previous_step_outputs:
731
968
  step_input.previous_step_content = step_input.get_last_step_content()
732
969
 
733
- # Create session_state copy once to avoid duplication
734
- session_state_copy = copy(session_state) if session_state is not None else {}
970
+ if workflow_session:
971
+ step_input.workflow_session = workflow_session
972
+
973
+ # Create session_state copy once to avoid duplication.
974
+ # Consider both run_context.session_state and session_state.
975
+ if run_context is not None and run_context.session_state is not None:
976
+ session_state_copy = run_context.session_state
977
+ else:
978
+ session_state_copy = copy(session_state) if session_state is not None else {}
979
+
980
+ # Considering both stream_events and stream_intermediate_steps (deprecated)
981
+ if stream_intermediate_steps is not None:
982
+ warnings.warn(
983
+ "The 'stream_intermediate_steps' parameter is deprecated and will be removed in future versions. Use 'stream_events' instead.",
984
+ DeprecationWarning,
985
+ stacklevel=2,
986
+ )
987
+ stream_events = stream_events or stream_intermediate_steps
735
988
 
736
- if stream_intermediate_steps and workflow_run_response:
989
+ if stream_events and workflow_run_response:
737
990
  # Emit StepStartedEvent
738
991
  yield StepStartedEvent(
739
992
  run_id=workflow_run_response.run_id or "",
@@ -754,68 +1007,116 @@ class Step:
754
1007
 
755
1008
  if self._executor_type == "function":
756
1009
  log_debug(f"Executing async function executor for step: {self.name}")
757
- import inspect
758
1010
 
759
1011
  # Check if the function is an async generator
760
- if inspect.isasyncgenfunction(self.active_executor):
1012
+ if _is_async_generator_function(self.active_executor):
761
1013
  content = ""
762
1014
  # It's an async generator - iterate over it
763
1015
  iterator = await self._acall_custom_function(
764
- self.active_executor, step_input, session_state_copy
765
- ) # type: ignore
1016
+ self.active_executor,
1017
+ step_input,
1018
+ session_state_copy,
1019
+ run_context,
1020
+ )
766
1021
  async for event in iterator: # type: ignore
767
- if (
768
- hasattr(event, "content")
769
- and event.content is not None
770
- and isinstance(event.content, str)
771
- ):
772
- content += event.content
773
- else:
774
- content += str(event)
775
- if isinstance(event, StepOutput):
1022
+ if isinstance(event, (BaseRunOutputEvent)):
1023
+ if (
1024
+ isinstance(event, (RunContentEvent, TeamRunContentEvent))
1025
+ and event.content is not None
1026
+ ):
1027
+ if isinstance(event.content, str):
1028
+ content += event.content
1029
+ elif isinstance(event.content, BaseModel):
1030
+ content = event.content # type: ignore[assignment]
1031
+ else:
1032
+ content = str(event.content)
1033
+
1034
+ # Only yield executor events if stream_executor_events is True
1035
+ if stream_executor_events:
1036
+ enriched_event = self._enrich_event_with_context(
1037
+ event, workflow_run_response, step_index
1038
+ )
1039
+ yield enriched_event # type: ignore[misc]
1040
+ elif isinstance(event, (RunOutput, TeamRunOutput)):
1041
+ content = event.content # type: ignore[assignment]
1042
+ elif isinstance(event, StepOutput):
776
1043
  final_response = event
777
1044
  break
778
1045
  else:
779
- yield event # type: ignore[misc]
1046
+ content += str(event)
780
1047
  if not final_response:
781
1048
  final_response = StepOutput(content=content)
782
- elif inspect.iscoroutinefunction(self.active_executor):
1049
+ elif _is_async_callable(self.active_executor):
783
1050
  # It's a regular async function - await it
784
- result = await self._acall_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
1051
+ result = await self._acall_custom_function(
1052
+ self.active_executor,
1053
+ step_input,
1054
+ session_state_copy,
1055
+ run_context,
1056
+ )
785
1057
  if isinstance(result, StepOutput):
786
1058
  final_response = result
1059
+ elif isinstance(result, (RunOutput, TeamRunOutput)):
1060
+ final_response = StepOutput(content=result.content)
787
1061
  else:
788
1062
  final_response = StepOutput(content=str(result))
789
- elif inspect.isgeneratorfunction(self.active_executor):
1063
+ elif _is_generator_function(self.active_executor):
790
1064
  content = ""
791
1065
  # It's a regular generator function - iterate over it
792
- iterator = self._call_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
1066
+ iterator = self._call_custom_function(
1067
+ self.active_executor,
1068
+ step_input,
1069
+ session_state_copy,
1070
+ run_context,
1071
+ )
793
1072
  for event in iterator: # type: ignore
794
- if (
795
- hasattr(event, "content")
796
- and event.content is not None
797
- and isinstance(event.content, str)
798
- ):
799
- content += event.content
800
- else:
801
- content += str(event)
802
- if isinstance(event, StepOutput):
1073
+ if isinstance(event, (BaseRunOutputEvent)):
1074
+ if (
1075
+ isinstance(event, (RunContentEvent, TeamRunContentEvent))
1076
+ and event.content is not None
1077
+ ):
1078
+ if isinstance(event.content, str):
1079
+ content += event.content
1080
+ elif isinstance(event.content, BaseModel):
1081
+ content = event.content # type: ignore[assignment]
1082
+ else:
1083
+ content = str(event.content)
1084
+
1085
+ # Only yield executor events if stream_executor_events is True
1086
+ if stream_executor_events:
1087
+ enriched_event = self._enrich_event_with_context(
1088
+ event, workflow_run_response, step_index
1089
+ )
1090
+ yield enriched_event # type: ignore[misc]
1091
+ elif isinstance(event, (RunOutput, TeamRunOutput)):
1092
+ content = event.content # type: ignore[assignment]
1093
+ elif isinstance(event, StepOutput):
803
1094
  final_response = event
804
1095
  break
805
1096
  else:
806
- yield event # type: ignore[misc]
1097
+ if isinstance(content, str):
1098
+ content += str(event)
1099
+ else:
1100
+ content = str(event)
807
1101
  if not final_response:
808
1102
  final_response = StepOutput(content=content)
809
1103
  else:
810
1104
  # It's a regular function - call it directly
811
- result = self._call_custom_function(self.active_executor, step_input, session_state_copy) # type: ignore
1105
+ result = self._call_custom_function(
1106
+ self.active_executor, # type: ignore[arg-type]
1107
+ step_input,
1108
+ session_state_copy,
1109
+ run_context,
1110
+ )
812
1111
  if isinstance(result, StepOutput):
813
1112
  final_response = result
1113
+ elif isinstance(result, (RunOutput, TeamRunOutput)):
1114
+ final_response = StepOutput(content=result.content)
814
1115
  else:
815
1116
  final_response = StepOutput(content=str(result))
816
1117
 
817
1118
  # Merge session_state changes back
818
- if session_state is not None:
1119
+ if run_context is None and session_state is not None:
819
1120
  merge_dictionaries(session_state, session_state_copy)
820
1121
  else:
821
1122
  # For agents and teams, prepare message with context
@@ -843,8 +1144,26 @@ class Step:
843
1144
  if isinstance(self.active_executor, Team):
844
1145
  kwargs["store_member_responses"] = True
845
1146
 
1147
+ # Forward background_tasks if provided
1148
+ if background_tasks is not None:
1149
+ kwargs["background_tasks"] = background_tasks
1150
+
1151
+ num_history_runs = self.num_history_runs if self.num_history_runs else num_history_runs
1152
+
1153
+ use_history = (
1154
+ self.add_workflow_history
1155
+ if self.add_workflow_history is not None
1156
+ else add_workflow_history_to_steps
1157
+ )
1158
+
1159
+ final_message = message
1160
+ if use_history and workflow_session:
1161
+ history_messages = workflow_session.get_workflow_history_context(num_runs=num_history_runs)
1162
+ if history_messages:
1163
+ final_message = f"{history_messages}{message}"
1164
+
846
1165
  response_stream = self.active_executor.arun( # type: ignore
847
- input=message,
1166
+ input=final_message,
848
1167
  images=images,
849
1168
  videos=videos,
850
1169
  audio=audios,
@@ -853,16 +1172,9 @@ class Step:
853
1172
  user_id=user_id,
854
1173
  session_state=session_state_copy,
855
1174
  stream=True,
856
- stream_intermediate_steps=stream_intermediate_steps,
857
- # Pass workflow context directly via kwargs
858
- workflow_context={
859
- "workflow_id": workflow_run_response.workflow_id if workflow_run_response else None,
860
- "workflow_run_id": workflow_run_response.run_id if workflow_run_response else None,
861
- "step_id": self.step_id,
862
- "step_name": self.name,
863
- "step_index": step_index,
864
- },
865
- yield_run_response=True,
1175
+ stream_events=stream_events,
1176
+ run_context=run_context,
1177
+ yield_run_output=True,
866
1178
  **kwargs,
867
1179
  )
868
1180
 
@@ -871,11 +1183,16 @@ class Step:
871
1183
  if isinstance(event, RunOutput) or isinstance(event, TeamRunOutput):
872
1184
  active_executor_run_response = event
873
1185
  break
874
- yield event # type: ignore[misc]
875
-
876
- if session_state is not None:
877
- # Update workflow session state
878
- merge_dictionaries(session_state, session_state_copy) # type: ignore
1186
+ # Only yield executor events if stream_executor_events is True
1187
+ if stream_executor_events:
1188
+ enriched_event = self._enrich_event_with_context(
1189
+ event, workflow_run_response, step_index
1190
+ )
1191
+ yield enriched_event # type: ignore[misc]
1192
+
1193
+ # Update workflow session state
1194
+ if run_context is None and session_state is not None:
1195
+ merge_dictionaries(session_state, session_state_copy)
879
1196
 
880
1197
  if store_executor_outputs and workflow_run_response is not None:
881
1198
  self._store_executor_response(workflow_run_response, active_executor_run_response) # type: ignore
@@ -895,7 +1212,7 @@ class Step:
895
1212
  final_response = self._process_step_output(final_response)
896
1213
  yield final_response
897
1214
 
898
- if stream_intermediate_steps and workflow_run_response:
1215
+ if stream_events and workflow_run_response:
899
1216
  # Emit StepCompletedEvent
900
1217
  yield StepCompletedEvent(
901
1218
  run_id=workflow_run_response.run_id or "",
@@ -928,6 +1245,83 @@ class Step:
928
1245
 
929
1246
  return
930
1247
 
1248
+ def get_chat_history(self, session_id: str, last_n_runs: Optional[int] = None) -> List[Message]:
1249
+ """Return the step's Agent or Team chat history for the given session.
1250
+
1251
+ Args:
1252
+ session_id: The session ID to get the chat history for. If not provided, the current cached session ID is used.
1253
+ last_n_runs: Number of recent runs to include. If None, all runs will be considered.
1254
+
1255
+ Returns:
1256
+ List[Message]: The step's Agent or Team chat history for the given session.
1257
+ """
1258
+ session: Union[AgentSession, TeamSession, WorkflowSession, None] = None
1259
+
1260
+ if self.agent:
1261
+ session = self.agent.get_session(session_id=session_id)
1262
+ if not session:
1263
+ log_warning("Session not found")
1264
+ return []
1265
+
1266
+ if not isinstance(session, WorkflowSession):
1267
+ raise ValueError("The provided session is not a WorkflowSession")
1268
+
1269
+ session = cast(WorkflowSession, session)
1270
+ return session.get_messages(last_n_runs=last_n_runs, agent_id=self.agent.id)
1271
+
1272
+ elif self.team:
1273
+ session = self.team.get_session(session_id=session_id)
1274
+ if not session:
1275
+ log_warning("Session not found")
1276
+ return []
1277
+
1278
+ if not isinstance(session, WorkflowSession):
1279
+ raise ValueError("The provided session is not a WorkflowSession")
1280
+
1281
+ session = cast(WorkflowSession, session)
1282
+ return session.get_messages(last_n_runs=last_n_runs, team_id=self.team.id)
1283
+
1284
+ return []
1285
+
1286
+ async def aget_chat_history(
1287
+ self, session_id: Optional[str] = None, last_n_runs: Optional[int] = None
1288
+ ) -> List[Message]:
1289
+ """Return the step's Agent or Team chat history for the given session.
1290
+
1291
+ Args:
1292
+ session_id: The session ID to get the chat history for. If not provided, the current cached session ID is used.
1293
+ last_n_runs: Number of recent runs to include. If None, all runs will be considered.
1294
+
1295
+ Returns:
1296
+ List[Message]: The step's Agent or Team chat history for the given session.
1297
+ """
1298
+ session: Union[AgentSession, TeamSession, WorkflowSession, None] = None
1299
+
1300
+ if self.agent:
1301
+ session = await self.agent.aget_session(session_id=session_id)
1302
+ if not session:
1303
+ log_warning("Session not found")
1304
+ return []
1305
+
1306
+ if not isinstance(session, WorkflowSession):
1307
+ raise ValueError("The provided session is not a WorkflowSession")
1308
+
1309
+ session = cast(WorkflowSession, session)
1310
+ return session.get_messages(last_n_runs=last_n_runs, agent_id=self.agent.id)
1311
+
1312
+ elif self.team:
1313
+ session = await self.team.aget_session(session_id=session_id)
1314
+ if not session:
1315
+ log_warning("Session not found")
1316
+ return []
1317
+
1318
+ if not isinstance(session, WorkflowSession):
1319
+ raise ValueError("The provided session is not a WorkflowSession")
1320
+
1321
+ return session.get_messages(last_n_runs=last_n_runs, team_id=self.team.id)
1322
+
1323
+ return []
1324
+
931
1325
  def _store_executor_response(
932
1326
  self, workflow_run_response: "WorkflowRunOutput", executor_run_response: Union[RunOutput, TeamRunOutput]
933
1327
  ) -> None:
@@ -937,6 +1331,14 @@ class Step:
937
1331
  executor_run_response.parent_run_id = workflow_run_response.run_id
938
1332
  executor_run_response.workflow_step_id = self.step_id
939
1333
 
1334
+ # Scrub the executor response based on the executor's storage flags before storing
1335
+ if (
1336
+ not self.active_executor.store_media
1337
+ or not self.active_executor.store_tool_messages
1338
+ or not self.active_executor.store_history_messages
1339
+ ): # type: ignore
1340
+ self.active_executor._scrub_run_output_for_storage(executor_run_response) # type: ignore
1341
+
940
1342
  # Get the raw response from the step's active executor
941
1343
  raw_response = executor_run_response
942
1344
  if raw_response and isinstance(raw_response, (RunOutput, TeamRunOutput)):
@@ -961,10 +1363,22 @@ class Step:
961
1363
 
962
1364
  For container steps (Steps, Router, Loop, etc.), this will recursively find the content from the
963
1365
  last actual step rather than using the generic container message.
1366
+
1367
+ For Parallel steps, aggregates content from ALL inner steps (not just the last one).
964
1368
  """
965
- # If this step has nested steps (like Steps, Condition, Router, Loop, etc.)
1369
+ # If this step has nested steps (like Steps, Condition, Router, Loop, Parallel, etc.)
966
1370
  if hasattr(step_output, "steps") and step_output.steps and len(step_output.steps) > 0:
967
- # Recursively get content from the last nested step
1371
+ # For Parallel steps, aggregate content from ALL inner steps
1372
+ if step_output.step_type == StepType.PARALLEL:
1373
+ aggregated_parts = []
1374
+ for i, inner_step in enumerate(step_output.steps):
1375
+ inner_content = self._get_deepest_content_from_step_output(inner_step)
1376
+ if inner_content:
1377
+ step_name = inner_step.step_name or f"Step {i + 1}"
1378
+ aggregated_parts.append(f"=== {step_name} ===\n{inner_content}")
1379
+ return "\n\n".join(aggregated_parts) if aggregated_parts else step_output.content # type: ignore
1380
+
1381
+ # For other nested step types, recursively get content from the last nested step
968
1382
  return self._get_deepest_content_from_step_output(step_output.steps[-1])
969
1383
 
970
1384
  # For regular steps, return their content
@@ -1123,3 +1537,28 @@ class Step:
1123
1537
  continue
1124
1538
 
1125
1539
  return videos
1540
+
1541
+
1542
+ def _is_async_callable(obj: Any) -> TypeGuard[Callable[..., Any]]:
1543
+ """Checks if obj is an async callable (coroutine function or callable with async __call__)"""
1544
+ return inspect.iscoroutinefunction(obj) or (callable(obj) and inspect.iscoroutinefunction(obj.__call__))
1545
+
1546
+
1547
+ def _is_generator_function(obj: Any) -> TypeGuard[Callable[..., Any]]:
1548
+ """Checks if obj is a generator function, including callable class instances with generator __call__ methods"""
1549
+ if inspect.isgeneratorfunction(obj):
1550
+ return True
1551
+ # Check if it's a callable class instance with a generator __call__ method
1552
+ if callable(obj) and hasattr(obj, "__call__"):
1553
+ return inspect.isgeneratorfunction(obj.__call__)
1554
+ return False
1555
+
1556
+
1557
+ def _is_async_generator_function(obj: Any) -> TypeGuard[Callable[..., Any]]:
1558
+ """Checks if obj is an async generator function, including callable class instances"""
1559
+ if inspect.isasyncgenfunction(obj):
1560
+ return True
1561
+ # Check if it's a callable class instance with an async generator __call__ method
1562
+ if callable(obj) and hasattr(obj, "__call__"):
1563
+ return inspect.isasyncgenfunction(obj.__call__)
1564
+ return False