agno 2.0.1__py3-none-any.whl → 2.3.0__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 +6015 -2823
  2. agno/api/api.py +2 -0
  3. agno/api/os.py +1 -1
  4. agno/culture/__init__.py +3 -0
  5. agno/culture/manager.py +956 -0
  6. agno/db/async_postgres/__init__.py +3 -0
  7. agno/db/base.py +385 -6
  8. agno/db/dynamo/dynamo.py +388 -81
  9. agno/db/dynamo/schemas.py +47 -10
  10. agno/db/dynamo/utils.py +63 -4
  11. agno/db/firestore/firestore.py +435 -64
  12. agno/db/firestore/schemas.py +11 -0
  13. agno/db/firestore/utils.py +102 -4
  14. agno/db/gcs_json/gcs_json_db.py +384 -42
  15. agno/db/gcs_json/utils.py +60 -26
  16. agno/db/in_memory/in_memory_db.py +351 -66
  17. agno/db/in_memory/utils.py +60 -2
  18. agno/db/json/json_db.py +339 -48
  19. agno/db/json/utils.py +60 -26
  20. agno/db/migrations/manager.py +199 -0
  21. agno/db/migrations/v1_to_v2.py +510 -37
  22. agno/db/migrations/versions/__init__.py +0 -0
  23. agno/db/migrations/versions/v2_3_0.py +938 -0
  24. agno/db/mongo/__init__.py +15 -1
  25. agno/db/mongo/async_mongo.py +2036 -0
  26. agno/db/mongo/mongo.py +653 -76
  27. agno/db/mongo/schemas.py +13 -0
  28. agno/db/mongo/utils.py +80 -8
  29. agno/db/mysql/mysql.py +687 -25
  30. agno/db/mysql/schemas.py +61 -37
  31. agno/db/mysql/utils.py +60 -2
  32. agno/db/postgres/__init__.py +2 -1
  33. agno/db/postgres/async_postgres.py +2001 -0
  34. agno/db/postgres/postgres.py +676 -57
  35. agno/db/postgres/schemas.py +43 -18
  36. agno/db/postgres/utils.py +164 -2
  37. agno/db/redis/redis.py +344 -38
  38. agno/db/redis/schemas.py +18 -0
  39. agno/db/redis/utils.py +60 -2
  40. agno/db/schemas/__init__.py +2 -1
  41. agno/db/schemas/culture.py +120 -0
  42. agno/db/schemas/memory.py +13 -0
  43. agno/db/singlestore/schemas.py +26 -1
  44. agno/db/singlestore/singlestore.py +687 -53
  45. agno/db/singlestore/utils.py +60 -2
  46. agno/db/sqlite/__init__.py +2 -1
  47. agno/db/sqlite/async_sqlite.py +2371 -0
  48. agno/db/sqlite/schemas.py +24 -0
  49. agno/db/sqlite/sqlite.py +774 -85
  50. agno/db/sqlite/utils.py +168 -5
  51. agno/db/surrealdb/__init__.py +3 -0
  52. agno/db/surrealdb/metrics.py +292 -0
  53. agno/db/surrealdb/models.py +309 -0
  54. agno/db/surrealdb/queries.py +71 -0
  55. agno/db/surrealdb/surrealdb.py +1361 -0
  56. agno/db/surrealdb/utils.py +147 -0
  57. agno/db/utils.py +50 -22
  58. agno/eval/accuracy.py +50 -43
  59. agno/eval/performance.py +6 -3
  60. agno/eval/reliability.py +6 -3
  61. agno/eval/utils.py +33 -16
  62. agno/exceptions.py +68 -1
  63. agno/filters.py +354 -0
  64. agno/guardrails/__init__.py +6 -0
  65. agno/guardrails/base.py +19 -0
  66. agno/guardrails/openai.py +144 -0
  67. agno/guardrails/pii.py +94 -0
  68. agno/guardrails/prompt_injection.py +52 -0
  69. agno/integrations/discord/client.py +1 -0
  70. agno/knowledge/chunking/agentic.py +13 -10
  71. agno/knowledge/chunking/fixed.py +1 -1
  72. agno/knowledge/chunking/semantic.py +40 -8
  73. agno/knowledge/chunking/strategy.py +59 -15
  74. agno/knowledge/embedder/aws_bedrock.py +9 -4
  75. agno/knowledge/embedder/azure_openai.py +54 -0
  76. agno/knowledge/embedder/base.py +2 -0
  77. agno/knowledge/embedder/cohere.py +184 -5
  78. agno/knowledge/embedder/fastembed.py +1 -1
  79. agno/knowledge/embedder/google.py +79 -1
  80. agno/knowledge/embedder/huggingface.py +9 -4
  81. agno/knowledge/embedder/jina.py +63 -0
  82. agno/knowledge/embedder/mistral.py +78 -11
  83. agno/knowledge/embedder/nebius.py +1 -1
  84. agno/knowledge/embedder/ollama.py +13 -0
  85. agno/knowledge/embedder/openai.py +37 -65
  86. agno/knowledge/embedder/sentence_transformer.py +8 -4
  87. agno/knowledge/embedder/vllm.py +262 -0
  88. agno/knowledge/embedder/voyageai.py +69 -16
  89. agno/knowledge/knowledge.py +594 -186
  90. agno/knowledge/reader/base.py +9 -2
  91. agno/knowledge/reader/csv_reader.py +8 -10
  92. agno/knowledge/reader/docx_reader.py +5 -6
  93. agno/knowledge/reader/field_labeled_csv_reader.py +290 -0
  94. agno/knowledge/reader/json_reader.py +6 -5
  95. agno/knowledge/reader/markdown_reader.py +13 -13
  96. agno/knowledge/reader/pdf_reader.py +43 -68
  97. agno/knowledge/reader/pptx_reader.py +101 -0
  98. agno/knowledge/reader/reader_factory.py +51 -6
  99. agno/knowledge/reader/s3_reader.py +3 -15
  100. agno/knowledge/reader/tavily_reader.py +194 -0
  101. agno/knowledge/reader/text_reader.py +13 -13
  102. agno/knowledge/reader/web_search_reader.py +2 -43
  103. agno/knowledge/reader/website_reader.py +43 -25
  104. agno/knowledge/reranker/__init__.py +2 -8
  105. agno/knowledge/types.py +9 -0
  106. agno/knowledge/utils.py +20 -0
  107. agno/media.py +72 -0
  108. agno/memory/manager.py +336 -82
  109. agno/models/aimlapi/aimlapi.py +2 -2
  110. agno/models/anthropic/claude.py +183 -37
  111. agno/models/aws/bedrock.py +52 -112
  112. agno/models/aws/claude.py +33 -1
  113. agno/models/azure/ai_foundry.py +33 -15
  114. agno/models/azure/openai_chat.py +25 -8
  115. agno/models/base.py +999 -519
  116. agno/models/cerebras/cerebras.py +19 -13
  117. agno/models/cerebras/cerebras_openai.py +8 -5
  118. agno/models/cohere/chat.py +27 -1
  119. agno/models/cometapi/__init__.py +5 -0
  120. agno/models/cometapi/cometapi.py +57 -0
  121. agno/models/dashscope/dashscope.py +1 -0
  122. agno/models/deepinfra/deepinfra.py +2 -2
  123. agno/models/deepseek/deepseek.py +2 -2
  124. agno/models/fireworks/fireworks.py +2 -2
  125. agno/models/google/gemini.py +103 -31
  126. agno/models/groq/groq.py +28 -11
  127. agno/models/huggingface/huggingface.py +2 -1
  128. agno/models/internlm/internlm.py +2 -2
  129. agno/models/langdb/langdb.py +4 -4
  130. agno/models/litellm/chat.py +18 -1
  131. agno/models/litellm/litellm_openai.py +2 -2
  132. agno/models/llama_cpp/__init__.py +5 -0
  133. agno/models/llama_cpp/llama_cpp.py +22 -0
  134. agno/models/message.py +139 -0
  135. agno/models/meta/llama.py +27 -10
  136. agno/models/meta/llama_openai.py +5 -17
  137. agno/models/nebius/nebius.py +6 -6
  138. agno/models/nexus/__init__.py +3 -0
  139. agno/models/nexus/nexus.py +22 -0
  140. agno/models/nvidia/nvidia.py +2 -2
  141. agno/models/ollama/chat.py +59 -5
  142. agno/models/openai/chat.py +69 -29
  143. agno/models/openai/responses.py +103 -106
  144. agno/models/openrouter/openrouter.py +41 -3
  145. agno/models/perplexity/perplexity.py +4 -5
  146. agno/models/portkey/portkey.py +3 -3
  147. agno/models/requesty/__init__.py +5 -0
  148. agno/models/requesty/requesty.py +52 -0
  149. agno/models/response.py +77 -1
  150. agno/models/sambanova/sambanova.py +2 -2
  151. agno/models/siliconflow/__init__.py +5 -0
  152. agno/models/siliconflow/siliconflow.py +25 -0
  153. agno/models/together/together.py +2 -2
  154. agno/models/utils.py +254 -8
  155. agno/models/vercel/v0.py +2 -2
  156. agno/models/vertexai/__init__.py +0 -0
  157. agno/models/vertexai/claude.py +96 -0
  158. agno/models/vllm/vllm.py +1 -0
  159. agno/models/xai/xai.py +3 -2
  160. agno/os/app.py +543 -178
  161. agno/os/auth.py +24 -14
  162. agno/os/config.py +1 -0
  163. agno/os/interfaces/__init__.py +1 -0
  164. agno/os/interfaces/a2a/__init__.py +3 -0
  165. agno/os/interfaces/a2a/a2a.py +42 -0
  166. agno/os/interfaces/a2a/router.py +250 -0
  167. agno/os/interfaces/a2a/utils.py +924 -0
  168. agno/os/interfaces/agui/agui.py +23 -7
  169. agno/os/interfaces/agui/router.py +27 -3
  170. agno/os/interfaces/agui/utils.py +242 -142
  171. agno/os/interfaces/base.py +6 -2
  172. agno/os/interfaces/slack/router.py +81 -23
  173. agno/os/interfaces/slack/slack.py +29 -14
  174. agno/os/interfaces/whatsapp/router.py +11 -4
  175. agno/os/interfaces/whatsapp/whatsapp.py +14 -7
  176. agno/os/mcp.py +111 -54
  177. agno/os/middleware/__init__.py +7 -0
  178. agno/os/middleware/jwt.py +233 -0
  179. agno/os/router.py +556 -139
  180. agno/os/routers/evals/evals.py +71 -34
  181. agno/os/routers/evals/schemas.py +31 -31
  182. agno/os/routers/evals/utils.py +6 -5
  183. agno/os/routers/health.py +31 -0
  184. agno/os/routers/home.py +52 -0
  185. agno/os/routers/knowledge/knowledge.py +185 -38
  186. agno/os/routers/knowledge/schemas.py +82 -22
  187. agno/os/routers/memory/memory.py +158 -53
  188. agno/os/routers/memory/schemas.py +20 -16
  189. agno/os/routers/metrics/metrics.py +20 -8
  190. agno/os/routers/metrics/schemas.py +16 -16
  191. agno/os/routers/session/session.py +499 -38
  192. agno/os/schema.py +308 -198
  193. agno/os/utils.py +401 -41
  194. agno/reasoning/anthropic.py +80 -0
  195. agno/reasoning/azure_ai_foundry.py +2 -2
  196. agno/reasoning/deepseek.py +2 -2
  197. agno/reasoning/default.py +3 -1
  198. agno/reasoning/gemini.py +73 -0
  199. agno/reasoning/groq.py +2 -2
  200. agno/reasoning/ollama.py +2 -2
  201. agno/reasoning/openai.py +7 -2
  202. agno/reasoning/vertexai.py +76 -0
  203. agno/run/__init__.py +6 -0
  204. agno/run/agent.py +248 -94
  205. agno/run/base.py +44 -5
  206. agno/run/team.py +238 -97
  207. agno/run/workflow.py +144 -33
  208. agno/session/agent.py +105 -89
  209. agno/session/summary.py +65 -25
  210. agno/session/team.py +176 -96
  211. agno/session/workflow.py +406 -40
  212. agno/team/team.py +3854 -1610
  213. agno/tools/dalle.py +2 -4
  214. agno/tools/decorator.py +4 -2
  215. agno/tools/duckduckgo.py +15 -11
  216. agno/tools/e2b.py +14 -7
  217. agno/tools/eleven_labs.py +23 -25
  218. agno/tools/exa.py +21 -16
  219. agno/tools/file.py +153 -23
  220. agno/tools/file_generation.py +350 -0
  221. agno/tools/firecrawl.py +4 -4
  222. agno/tools/function.py +250 -30
  223. agno/tools/gmail.py +238 -14
  224. agno/tools/google_drive.py +270 -0
  225. agno/tools/googlecalendar.py +36 -8
  226. agno/tools/googlesheets.py +20 -5
  227. agno/tools/jira.py +20 -0
  228. agno/tools/knowledge.py +3 -3
  229. agno/tools/mcp/__init__.py +10 -0
  230. agno/tools/mcp/mcp.py +331 -0
  231. agno/tools/mcp/multi_mcp.py +347 -0
  232. agno/tools/mcp/params.py +24 -0
  233. agno/tools/mcp_toolbox.py +284 -0
  234. agno/tools/mem0.py +11 -17
  235. agno/tools/memori.py +1 -53
  236. agno/tools/memory.py +419 -0
  237. agno/tools/models/nebius.py +5 -5
  238. agno/tools/models_labs.py +20 -10
  239. agno/tools/notion.py +204 -0
  240. agno/tools/parallel.py +314 -0
  241. agno/tools/scrapegraph.py +58 -31
  242. agno/tools/searxng.py +2 -2
  243. agno/tools/serper.py +2 -2
  244. agno/tools/slack.py +18 -3
  245. agno/tools/spider.py +2 -2
  246. agno/tools/tavily.py +146 -0
  247. agno/tools/whatsapp.py +1 -1
  248. agno/tools/workflow.py +278 -0
  249. agno/tools/yfinance.py +12 -11
  250. agno/utils/agent.py +820 -0
  251. agno/utils/audio.py +27 -0
  252. agno/utils/common.py +90 -1
  253. agno/utils/events.py +217 -2
  254. agno/utils/gemini.py +180 -22
  255. agno/utils/hooks.py +57 -0
  256. agno/utils/http.py +111 -0
  257. agno/utils/knowledge.py +12 -5
  258. agno/utils/log.py +1 -0
  259. agno/utils/mcp.py +92 -2
  260. agno/utils/media.py +188 -10
  261. agno/utils/merge_dict.py +22 -1
  262. agno/utils/message.py +60 -0
  263. agno/utils/models/claude.py +40 -11
  264. agno/utils/print_response/agent.py +105 -21
  265. agno/utils/print_response/team.py +103 -38
  266. agno/utils/print_response/workflow.py +251 -34
  267. agno/utils/reasoning.py +22 -1
  268. agno/utils/serialize.py +32 -0
  269. agno/utils/streamlit.py +16 -10
  270. agno/utils/string.py +41 -0
  271. agno/utils/team.py +98 -9
  272. agno/utils/tools.py +1 -1
  273. agno/vectordb/base.py +23 -4
  274. agno/vectordb/cassandra/cassandra.py +65 -9
  275. agno/vectordb/chroma/chromadb.py +182 -38
  276. agno/vectordb/clickhouse/clickhousedb.py +64 -11
  277. agno/vectordb/couchbase/couchbase.py +105 -10
  278. agno/vectordb/lancedb/lance_db.py +124 -133
  279. agno/vectordb/langchaindb/langchaindb.py +25 -7
  280. agno/vectordb/lightrag/lightrag.py +17 -3
  281. agno/vectordb/llamaindex/__init__.py +3 -0
  282. agno/vectordb/llamaindex/llamaindexdb.py +46 -7
  283. agno/vectordb/milvus/milvus.py +126 -9
  284. agno/vectordb/mongodb/__init__.py +7 -1
  285. agno/vectordb/mongodb/mongodb.py +112 -7
  286. agno/vectordb/pgvector/pgvector.py +142 -21
  287. agno/vectordb/pineconedb/pineconedb.py +80 -8
  288. agno/vectordb/qdrant/qdrant.py +125 -39
  289. agno/vectordb/redis/__init__.py +9 -0
  290. agno/vectordb/redis/redisdb.py +694 -0
  291. agno/vectordb/singlestore/singlestore.py +111 -25
  292. agno/vectordb/surrealdb/surrealdb.py +31 -5
  293. agno/vectordb/upstashdb/upstashdb.py +76 -8
  294. agno/vectordb/weaviate/weaviate.py +86 -15
  295. agno/workflow/__init__.py +2 -0
  296. agno/workflow/agent.py +299 -0
  297. agno/workflow/condition.py +112 -18
  298. agno/workflow/loop.py +69 -10
  299. agno/workflow/parallel.py +266 -118
  300. agno/workflow/router.py +110 -17
  301. agno/workflow/step.py +638 -129
  302. agno/workflow/steps.py +65 -6
  303. agno/workflow/types.py +61 -23
  304. agno/workflow/workflow.py +2085 -272
  305. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/METADATA +182 -58
  306. agno-2.3.0.dist-info/RECORD +577 -0
  307. agno/knowledge/reader/url_reader.py +0 -128
  308. agno/tools/googlesearch.py +0 -98
  309. agno/tools/mcp.py +0 -610
  310. agno/utils/models/aws_claude.py +0 -170
  311. agno-2.0.1.dist-info/RECORD +0 -515
  312. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/WHEEL +0 -0
  313. {agno-2.0.1.dist-info → agno-2.3.0.dist-info}/licenses/LICENSE +0 -0
  314. {agno-2.0.1.dist-info → agno-2.3.0.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:
@@ -164,14 +180,55 @@ class Step:
164
180
  return response.metrics
165
181
  return None
166
182
 
183
+ def _call_custom_function(
184
+ self,
185
+ func: Callable,
186
+ step_input: StepInput,
187
+ session_state: Optional[Dict[str, Any]] = None,
188
+ run_context: Optional[RunContext] = None,
189
+ ) -> Any:
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
195
+ if session_state is not None and self._function_has_session_state_param():
196
+ kwargs["session_state"] = session_state
197
+
198
+ return func(step_input, **kwargs)
199
+
200
+ async def _acall_custom_function(
201
+ self,
202
+ func: Callable,
203
+ step_input: StepInput,
204
+ session_state: Optional[Dict[str, Any]] = None,
205
+ run_context: Optional[RunContext] = None,
206
+ ) -> Any:
207
+ """Call custom async function with session_state support if the function accepts it"""
208
+
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)
217
+ else:
218
+ return await func(step_input, **kwargs)
219
+
167
220
  def execute(
168
221
  self,
169
222
  step_input: StepInput,
170
223
  session_id: Optional[str] = None,
171
224
  user_id: Optional[str] = None,
172
225
  workflow_run_response: Optional["WorkflowRunOutput"] = None,
226
+ run_context: Optional[RunContext] = None,
173
227
  session_state: Optional[Dict[str, Any]] = None,
174
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,
175
232
  ) -> StepOutput:
176
233
  """Execute the step with StepInput, returning final StepOutput (non-streaming)"""
177
234
  log_debug(f"Executing step: {self.name}")
@@ -179,46 +236,89 @@ class Step:
179
236
  if step_input.previous_step_outputs:
180
237
  step_input.previous_step_content = step_input.get_last_step_content()
181
238
 
239
+ if workflow_session:
240
+ step_input.workflow_session = workflow_session
241
+
242
+ # Create session_state copy once to avoid duplication.
243
+ # Consider both run_context.session_state and session_state.
244
+ if run_context is not None and run_context.session_state is not None:
245
+ session_state_copy = run_context.session_state
246
+ else:
247
+ session_state_copy = copy(session_state) if session_state is not None else {}
248
+
182
249
  # Execute with retries
183
250
  for attempt in range(self.max_retries + 1):
184
251
  try:
185
252
  response: Union[RunOutput, TeamRunOutput, StepOutput]
186
253
  if self._executor_type == "function":
187
- if inspect.iscoroutinefunction(self.active_executor) or inspect.isasyncgenfunction(
188
- self.active_executor
189
- ):
254
+ if _is_async_callable(self.active_executor) or _is_async_generator_function(self.active_executor):
190
255
  raise ValueError("Cannot use async function with synchronous execution")
191
- if inspect.isgeneratorfunction(self.active_executor):
256
+ if _is_generator_function(self.active_executor):
192
257
  content = ""
193
258
  final_response = None
194
259
  try:
195
- for chunk in self.active_executor(step_input): # type: ignore
196
- if (
197
- hasattr(chunk, "content")
198
- and chunk.content is not None
199
- and isinstance(chunk.content, str)
200
- ):
201
- content += chunk.content
260
+ for chunk in self._call_custom_function(
261
+ self.active_executor,
262
+ step_input,
263
+ session_state_copy, # type: ignore[arg-type]
264
+ run_context,
265
+ ): # type: ignore
266
+ if isinstance(chunk, (BaseRunOutputEvent)):
267
+ if (
268
+ isinstance(chunk, (RunContentEvent, TeamRunContentEvent))
269
+ and chunk.content is not None
270
+ ):
271
+ # Its a regular chunk of content
272
+ if isinstance(chunk.content, str):
273
+ content += chunk.content
274
+ # Its the BaseModel object, set it as the content. Replace any previous content.
275
+ # There should be no previous str content at this point
276
+ elif isinstance(chunk.content, BaseModel):
277
+ content = chunk.content # type: ignore[assignment]
278
+ else:
279
+ # Case when parse_response is False and the content is a dict
280
+ content += str(chunk.content)
281
+ elif isinstance(chunk, (RunOutput, TeamRunOutput)):
282
+ # This is the final response from the agent/team
283
+ content = chunk.content # type: ignore[assignment]
284
+ # If the chunk is a StepOutput, use it as the final response
285
+ elif isinstance(chunk, StepOutput):
286
+ final_response = chunk
287
+ break
288
+ # Non Agent/Team data structure that was yielded
202
289
  else:
203
290
  content += str(chunk)
204
- if isinstance(chunk, StepOutput):
205
- final_response = chunk
206
291
 
207
292
  except StopIteration as e:
208
293
  if hasattr(e, "value") and isinstance(e.value, StepOutput):
209
294
  final_response = e.value
210
295
 
296
+ # Merge session_state changes back
297
+ if run_context is None and session_state is not None:
298
+ merge_dictionaries(session_state, session_state_copy)
299
+
211
300
  if final_response is not None:
212
301
  response = final_response
213
302
  else:
214
303
  response = StepOutput(content=content)
215
304
  else:
216
- # Execute function directly with StepInput
217
- result = self.active_executor(step_input) # type: ignore
305
+ # Execute function with signature inspection for session_state support
306
+ result = self._call_custom_function(
307
+ self.active_executor, # type: ignore[arg-type]
308
+ step_input,
309
+ session_state_copy,
310
+ run_context,
311
+ )
312
+
313
+ # Merge session_state changes back
314
+ if run_context is None and session_state is not None:
315
+ merge_dictionaries(session_state, session_state_copy)
218
316
 
219
317
  # If function returns StepOutput, use it directly
220
318
  if isinstance(result, StepOutput):
221
319
  response = result
320
+ elif isinstance(result, (RunOutput, TeamRunOutput)):
321
+ response = StepOutput(content=result.content)
222
322
  else:
223
323
  response = StepOutput(content=str(result))
224
324
  else:
@@ -248,9 +348,22 @@ class Step:
248
348
  if isinstance(self.active_executor, Team):
249
349
  kwargs["store_member_responses"] = True
250
350
 
251
- session_state_copy = copy(session_state)
351
+ num_history_runs = self.num_history_runs if self.num_history_runs else num_history_runs
352
+
353
+ use_history = (
354
+ self.add_workflow_history
355
+ if self.add_workflow_history is not None
356
+ else add_workflow_history_to_steps
357
+ )
358
+
359
+ final_message = message
360
+ if use_history and workflow_session:
361
+ history_messages = workflow_session.get_workflow_history_context(num_runs=num_history_runs)
362
+ if history_messages:
363
+ final_message = f"{history_messages}{message}"
364
+
252
365
  response = self.active_executor.run( # type: ignore
253
- input=message, # type: ignore
366
+ input=final_message, # type: ignore
254
367
  images=images,
255
368
  videos=videos,
256
369
  audio=audios,
@@ -258,11 +371,13 @@ class Step:
258
371
  session_id=session_id,
259
372
  user_id=user_id,
260
373
  session_state=session_state_copy, # Send a copy to the executor
374
+ run_context=run_context,
261
375
  **kwargs,
262
376
  )
263
377
 
264
378
  # Update workflow session state
265
- merge_dictionaries(session_state, session_state_copy) # type: ignore
379
+ if run_context is None and session_state is not None:
380
+ merge_dictionaries(session_state, session_state_copy)
266
381
 
267
382
  if store_executor_outputs and workflow_run_response is not None:
268
383
  self._store_executor_response(workflow_run_response, response) # type: ignore
@@ -291,25 +406,97 @@ class Step:
291
406
 
292
407
  return StepOutput(content=f"Step {self.name} failed but skipped", success=False)
293
408
 
409
+ def _function_has_run_context_param(self) -> bool:
410
+ """Check if the custom function has a run_context parameter"""
411
+ if self._executor_type != "function":
412
+ return False
413
+
414
+ try:
415
+ sig = inspect.signature(self.active_executor) # type: ignore
416
+ return "run_context" in sig.parameters
417
+ except Exception:
418
+ return False
419
+
420
+ def _function_has_session_state_param(self) -> bool:
421
+ """Check if the custom function has a session_state parameter"""
422
+ if self._executor_type != "function":
423
+ return False
424
+
425
+ try:
426
+ sig = inspect.signature(self.active_executor) # type: ignore
427
+ return "session_state" in sig.parameters
428
+ except Exception:
429
+ return False
430
+
431
+ def _enrich_event_with_context(
432
+ self,
433
+ event: Any,
434
+ workflow_run_response: Optional["WorkflowRunOutput"] = None,
435
+ step_index: Optional[Union[int, tuple]] = None,
436
+ ) -> Any:
437
+ """Enrich event with step and workflow context information"""
438
+ if workflow_run_response is None:
439
+ return event
440
+ if hasattr(event, "workflow_id"):
441
+ event.workflow_id = workflow_run_response.workflow_id
442
+ if hasattr(event, "workflow_run_id"):
443
+ event.workflow_run_id = workflow_run_response.run_id
444
+ if hasattr(event, "step_id"):
445
+ event.step_id = self.step_id
446
+ if hasattr(event, "step_name") and self.name is not None:
447
+ if getattr(event, "step_name", None) is None:
448
+ event.step_name = self.name
449
+ # Only set step_index if it's not already set (preserve parallel.py's tuples)
450
+ if hasattr(event, "step_index") and step_index is not None:
451
+ if event.step_index is None:
452
+ event.step_index = step_index
453
+
454
+ return event
455
+
294
456
  def execute_stream(
295
457
  self,
296
458
  step_input: StepInput,
297
459
  session_id: Optional[str] = None,
298
460
  user_id: Optional[str] = None,
461
+ stream_events: bool = False,
299
462
  stream_intermediate_steps: bool = False,
463
+ stream_executor_events: bool = True,
300
464
  workflow_run_response: Optional["WorkflowRunOutput"] = None,
465
+ run_context: Optional[RunContext] = None,
301
466
  session_state: Optional[Dict[str, Any]] = None,
302
467
  step_index: Optional[Union[int, tuple]] = None,
303
468
  store_executor_outputs: bool = True,
304
469
  parent_step_id: Optional[str] = None,
470
+ workflow_session: Optional["WorkflowSession"] = None,
471
+ add_workflow_history_to_steps: Optional[bool] = False,
472
+ num_history_runs: int = 3,
305
473
  ) -> Iterator[Union[WorkflowRunOutputEvent, StepOutput]]:
306
474
  """Execute the step with event-driven streaming support"""
307
475
 
308
476
  if step_input.previous_step_outputs:
309
477
  step_input.previous_step_content = step_input.get_last_step_content()
310
478
 
479
+ if workflow_session:
480
+ step_input.workflow_session = workflow_session
481
+
482
+ # Create session_state copy once to avoid duplication.
483
+ # Consider both run_context.session_state and session_state.
484
+ if run_context is not None and run_context.session_state is not None:
485
+ session_state_copy = run_context.session_state
486
+ else:
487
+ session_state_copy = copy(session_state) if session_state is not None else {}
488
+
489
+ # Considering both stream_events and stream_intermediate_steps (deprecated)
490
+ if stream_intermediate_steps is not None:
491
+ warnings.warn(
492
+ "The 'stream_intermediate_steps' parameter is deprecated and will be removed in future versions. Use 'stream_events' instead.",
493
+ DeprecationWarning,
494
+ stacklevel=2,
495
+ )
496
+ stream_events = stream_events or stream_intermediate_steps
497
+
311
498
  # Emit StepStartedEvent
312
- if stream_intermediate_steps and workflow_run_response:
499
+ if stream_events and workflow_run_response:
313
500
  yield StepStartedEvent(
314
501
  run_id=workflow_run_response.run_id or "",
315
502
  workflow_name=workflow_run_response.workflow_name or "",
@@ -329,30 +516,48 @@ class Step:
329
516
 
330
517
  if self._executor_type == "function":
331
518
  log_debug(f"Executing function executor for step: {self.name}")
332
-
333
- if inspect.iscoroutinefunction(self.active_executor) or inspect.isasyncgenfunction(
334
- self.active_executor
335
- ):
519
+ if _is_async_callable(self.active_executor) or _is_async_generator_function(self.active_executor):
336
520
  raise ValueError("Cannot use async function with synchronous execution")
337
-
338
- if inspect.isgeneratorfunction(self.active_executor):
521
+ if _is_generator_function(self.active_executor):
339
522
  log_debug("Function returned iterable, streaming events")
340
523
  content = ""
341
524
  try:
342
- for event in self.active_executor(step_input): # type: ignore
343
- if (
344
- hasattr(event, "content")
345
- and event.content is not None
346
- and isinstance(event.content, str)
347
- ):
348
- content += event.content
349
- else:
350
- content += str(event)
351
- if isinstance(event, StepOutput):
525
+ iterator = self._call_custom_function(
526
+ self.active_executor,
527
+ step_input,
528
+ session_state_copy,
529
+ run_context,
530
+ )
531
+ for event in iterator: # type: ignore
532
+ if isinstance(event, (BaseRunOutputEvent)):
533
+ if (
534
+ isinstance(event, (RunContentEvent, TeamRunContentEvent))
535
+ and event.content is not None
536
+ ):
537
+ if isinstance(event.content, str):
538
+ content += event.content
539
+ elif isinstance(event.content, BaseModel):
540
+ content = event.content # type: ignore[assignment]
541
+ else:
542
+ content = str(event.content)
543
+ # Only yield executor events if stream_executor_events is True
544
+ if stream_executor_events:
545
+ enriched_event = self._enrich_event_with_context(
546
+ event, workflow_run_response, step_index
547
+ )
548
+ yield enriched_event # type: ignore[misc]
549
+ elif isinstance(event, (RunOutput, TeamRunOutput)):
550
+ content = event.content # type: ignore[assignment]
551
+ elif isinstance(event, StepOutput):
352
552
  final_response = event
353
553
  break
354
554
  else:
355
- yield event # type: ignore[misc]
555
+ content += str(event)
556
+
557
+ # Merge session_state changes back
558
+ if run_context is None and session_state is not None:
559
+ merge_dictionaries(session_state, session_state_copy)
560
+
356
561
  if not final_response:
357
562
  final_response = StepOutput(content=content)
358
563
  except StopIteration as e:
@@ -360,9 +565,21 @@ class Step:
360
565
  final_response = e.value
361
566
 
362
567
  else:
363
- result = self.active_executor(step_input) # type: ignore
568
+ result = self._call_custom_function(
569
+ self.active_executor, # type: ignore[arg-type]
570
+ step_input,
571
+ session_state_copy,
572
+ run_context,
573
+ )
574
+
575
+ # Merge session_state changes back
576
+ if run_context is None and session_state is not None:
577
+ merge_dictionaries(session_state, session_state_copy)
578
+
364
579
  if isinstance(result, StepOutput):
365
580
  final_response = result
581
+ elif isinstance(result, (RunOutput, TeamRunOutput)):
582
+ final_response = StepOutput(content=result.content)
366
583
  else:
367
584
  final_response = StepOutput(content=str(result))
368
585
  log_debug("Function returned non-iterable, created StepOutput")
@@ -392,9 +609,22 @@ class Step:
392
609
  if isinstance(self.active_executor, Team):
393
610
  kwargs["store_member_responses"] = True
394
611
 
395
- session_state_copy = copy(session_state)
612
+ num_history_runs = self.num_history_runs if self.num_history_runs else num_history_runs
613
+
614
+ use_history = (
615
+ self.add_workflow_history
616
+ if self.add_workflow_history is not None
617
+ else add_workflow_history_to_steps
618
+ )
619
+
620
+ final_message = message
621
+ if use_history and workflow_session:
622
+ history_messages = workflow_session.get_workflow_history_context(num_runs=num_history_runs)
623
+ if history_messages:
624
+ final_message = f"{history_messages}{message}"
625
+
396
626
  response_stream = self.active_executor.run( # type: ignore[call-overload, misc]
397
- input=message,
627
+ input=final_message,
398
628
  images=images,
399
629
  videos=videos,
400
630
  audio=audios,
@@ -403,16 +633,9 @@ class Step:
403
633
  user_id=user_id,
404
634
  session_state=session_state_copy, # Send a copy to the executor
405
635
  stream=True,
406
- stream_intermediate_steps=stream_intermediate_steps,
407
- # Pass workflow context directly via kwargs
408
- workflow_context={
409
- "workflow_id": workflow_run_response.workflow_id if workflow_run_response else None,
410
- "workflow_run_id": workflow_run_response.run_id if workflow_run_response else None,
411
- "step_id": self.step_id,
412
- "step_name": self.name,
413
- "step_index": step_index,
414
- },
636
+ stream_events=stream_events,
415
637
  yield_run_response=True,
638
+ run_context=run_context,
416
639
  **kwargs,
417
640
  )
418
641
 
@@ -421,15 +644,21 @@ class Step:
421
644
  if isinstance(event, RunOutput) or isinstance(event, TeamRunOutput):
422
645
  active_executor_run_response = event
423
646
  break
424
- yield event # type: ignore[misc]
647
+ # Only yield executor events if stream_executor_events is True
648
+ if stream_executor_events:
649
+ enriched_event = self._enrich_event_with_context(
650
+ event, workflow_run_response, step_index
651
+ )
652
+ yield enriched_event # type: ignore[misc]
425
653
 
426
654
  # Update workflow session state
427
- merge_dictionaries(session_state, session_state_copy) # type: ignore
655
+ if run_context is None and session_state is not None:
656
+ merge_dictionaries(session_state, session_state_copy)
428
657
 
429
658
  if store_executor_outputs and workflow_run_response is not None:
430
659
  self._store_executor_response(workflow_run_response, active_executor_run_response) # type: ignore
431
660
 
432
- final_response = self._process_step_output(active_executor_run_response) # type: ignore
661
+ final_response = active_executor_run_response # type: ignore
433
662
 
434
663
  else:
435
664
  raise ValueError(f"Unsupported executor type: {self._executor_type}")
@@ -443,10 +672,11 @@ class Step:
443
672
  use_workflow_logger()
444
673
 
445
674
  # Yield the step output
675
+ final_response = self._process_step_output(final_response)
446
676
  yield final_response
447
677
 
448
678
  # Emit StepCompletedEvent
449
- if stream_intermediate_steps and workflow_run_response:
679
+ if stream_events and workflow_run_response:
450
680
  yield StepCompletedEvent(
451
681
  run_id=workflow_run_response.run_id or "",
452
682
  workflow_name=workflow_run_response.workflow_name or "",
@@ -484,8 +714,12 @@ class Step:
484
714
  session_id: Optional[str] = None,
485
715
  user_id: Optional[str] = None,
486
716
  workflow_run_response: Optional["WorkflowRunOutput"] = None,
717
+ run_context: Optional[RunContext] = None,
487
718
  session_state: Optional[Dict[str, Any]] = None,
488
719
  store_executor_outputs: bool = True,
720
+ workflow_session: Optional["WorkflowSession"] = None,
721
+ add_workflow_history_to_steps: Optional[bool] = False,
722
+ num_history_runs: int = 3,
489
723
  ) -> StepOutput:
490
724
  """Execute the step with StepInput, returning final StepOutput (non-streaming)"""
491
725
  logger.info(f"Executing async step (non-streaming): {self.name}")
@@ -494,61 +728,118 @@ class Step:
494
728
  if step_input.previous_step_outputs:
495
729
  step_input.previous_step_content = step_input.get_last_step_content()
496
730
 
731
+ if workflow_session:
732
+ step_input.workflow_session = workflow_session
733
+
734
+ # Create session_state copy once to avoid duplication.
735
+ # Consider both run_context.session_state and session_state.
736
+ if run_context is not None and run_context.session_state is not None:
737
+ session_state_copy = run_context.session_state
738
+ else:
739
+ session_state_copy = copy(session_state) if session_state is not None else {}
740
+
497
741
  # Execute with retries
498
742
  for attempt in range(self.max_retries + 1):
499
743
  try:
500
744
  if self._executor_type == "function":
501
- import inspect
502
-
503
- if inspect.isgeneratorfunction(self.active_executor) or inspect.isasyncgenfunction(
745
+ if _is_generator_function(self.active_executor) or _is_async_generator_function(
504
746
  self.active_executor
505
747
  ):
506
748
  content = ""
507
749
  final_response = None
508
750
  try:
509
- if inspect.isgeneratorfunction(self.active_executor):
510
- for chunk in self.active_executor(step_input): # type: ignore
511
- if (
512
- hasattr(chunk, "content")
513
- and chunk.content is not None
514
- and isinstance(chunk.content, str)
515
- ):
516
- content += chunk.content
517
- else:
518
- content += str(chunk)
519
- if isinstance(chunk, StepOutput):
520
- final_response = chunk
521
- else:
522
- if inspect.isasyncgenfunction(self.active_executor):
523
- async for chunk in self.active_executor(step_input): # type: ignore
751
+ if _is_generator_function(self.active_executor):
752
+ iterator = self._call_custom_function(
753
+ self.active_executor,
754
+ step_input,
755
+ session_state_copy,
756
+ run_context,
757
+ )
758
+ for chunk in iterator: # type: ignore
759
+ if isinstance(chunk, (BaseRunOutputEvent)):
524
760
  if (
525
- hasattr(chunk, "content")
761
+ isinstance(chunk, (RunContentEvent, TeamRunContentEvent))
526
762
  and chunk.content is not None
527
- and isinstance(chunk.content, str)
528
763
  ):
529
- content += chunk.content
764
+ if isinstance(chunk.content, str):
765
+ content += chunk.content
766
+ elif isinstance(chunk.content, BaseModel):
767
+ content = chunk.content # type: ignore[assignment]
768
+ else:
769
+ content = str(chunk.content)
770
+ elif isinstance(chunk, (RunOutput, TeamRunOutput)):
771
+ content = chunk.content # type: ignore[assignment]
772
+ elif isinstance(chunk, StepOutput):
773
+ final_response = chunk
774
+ break
775
+ else:
776
+ content += str(chunk)
777
+
778
+ else:
779
+ if _is_async_generator_function(self.active_executor):
780
+ iterator = await self._acall_custom_function(
781
+ self.active_executor,
782
+ step_input,
783
+ session_state_copy,
784
+ run_context,
785
+ )
786
+ async for chunk in iterator: # type: ignore
787
+ if isinstance(chunk, (BaseRunOutputEvent)):
788
+ if (
789
+ isinstance(chunk, (RunContentEvent, TeamRunContentEvent))
790
+ and chunk.content is not None
791
+ ):
792
+ if isinstance(chunk.content, str):
793
+ content += chunk.content
794
+ elif isinstance(chunk.content, BaseModel):
795
+ content = chunk.content # type: ignore[assignment]
796
+ else:
797
+ content = str(chunk.content)
798
+ elif isinstance(chunk, (RunOutput, TeamRunOutput)):
799
+ content = chunk.content # type: ignore[assignment]
800
+ elif isinstance(chunk, StepOutput):
801
+ final_response = chunk
802
+ break
530
803
  else:
531
804
  content += str(chunk)
532
- if isinstance(chunk, StepOutput):
533
- final_response = chunk
534
805
 
535
806
  except StopIteration as e:
536
807
  if hasattr(e, "value") and isinstance(e.value, StepOutput):
537
808
  final_response = e.value
538
809
 
810
+ # Merge session_state changes back
811
+ if run_context is None and session_state is not None:
812
+ merge_dictionaries(session_state, session_state_copy)
813
+
539
814
  if final_response is not None:
540
815
  response = final_response
541
816
  else:
542
817
  response = StepOutput(content=content)
543
818
  else:
544
- if inspect.iscoroutinefunction(self.active_executor):
545
- result = await self.active_executor(step_input) # type: ignore
819
+ if _is_async_callable(self.active_executor):
820
+ result = await self._acall_custom_function(
821
+ self.active_executor,
822
+ step_input,
823
+ session_state_copy,
824
+ run_context,
825
+ )
546
826
  else:
547
- result = self.active_executor(step_input) # type: ignore
827
+ result = self._call_custom_function(
828
+ self.active_executor, # type: ignore[arg-type]
829
+ step_input,
830
+ session_state_copy,
831
+ run_context,
832
+ )
833
+
834
+ # Merge session_state changes back
835
+ if run_context is None and session_state is not None:
836
+ merge_dictionaries(session_state, session_state_copy)
548
837
 
549
838
  # If function returns StepOutput, use it directly
550
839
  if isinstance(result, StepOutput):
551
840
  response = result
841
+ elif isinstance(result, (RunOutput, TeamRunOutput)):
842
+ response = StepOutput(content=result.content)
552
843
  else:
553
844
  response = StepOutput(content=str(result))
554
845
 
@@ -579,9 +870,22 @@ class Step:
579
870
  if isinstance(self.active_executor, Team):
580
871
  kwargs["store_member_responses"] = True
581
872
 
582
- session_state_copy = copy(session_state)
873
+ num_history_runs = self.num_history_runs if self.num_history_runs else num_history_runs
874
+
875
+ use_history = (
876
+ self.add_workflow_history
877
+ if self.add_workflow_history is not None
878
+ else add_workflow_history_to_steps
879
+ )
880
+
881
+ final_message = message
882
+ if use_history and workflow_session:
883
+ history_messages = workflow_session.get_workflow_history_context(num_runs=num_history_runs)
884
+ if history_messages:
885
+ final_message = f"{history_messages}{message}"
886
+
583
887
  response = await self.active_executor.arun( # type: ignore
584
- input=message, # type: ignore
888
+ input=final_message, # type: ignore
585
889
  images=images,
586
890
  videos=videos,
587
891
  audio=audios,
@@ -589,11 +893,13 @@ class Step:
589
893
  session_id=session_id,
590
894
  user_id=user_id,
591
895
  session_state=session_state_copy,
896
+ run_context=run_context,
592
897
  **kwargs,
593
898
  )
594
899
 
595
900
  # Update workflow session state
596
- merge_dictionaries(session_state, session_state_copy) # type: ignore
901
+ if run_context is None and session_state is not None:
902
+ merge_dictionaries(session_state, session_state_copy)
597
903
 
598
904
  if store_executor_outputs and workflow_run_response is not None:
599
905
  self._store_executor_response(workflow_run_response, response) # type: ignore
@@ -627,19 +933,44 @@ class Step:
627
933
  step_input: StepInput,
628
934
  session_id: Optional[str] = None,
629
935
  user_id: Optional[str] = None,
936
+ stream_events: bool = False,
630
937
  stream_intermediate_steps: bool = False,
938
+ stream_executor_events: bool = True,
631
939
  workflow_run_response: Optional["WorkflowRunOutput"] = None,
940
+ run_context: Optional[RunContext] = None,
632
941
  session_state: Optional[Dict[str, Any]] = None,
633
942
  step_index: Optional[Union[int, tuple]] = None,
634
943
  store_executor_outputs: bool = True,
635
944
  parent_step_id: Optional[str] = None,
945
+ workflow_session: Optional["WorkflowSession"] = None,
946
+ add_workflow_history_to_steps: Optional[bool] = False,
947
+ num_history_runs: int = 3,
636
948
  ) -> AsyncIterator[Union[WorkflowRunOutputEvent, StepOutput]]:
637
949
  """Execute the step with event-driven streaming support"""
638
950
 
639
951
  if step_input.previous_step_outputs:
640
952
  step_input.previous_step_content = step_input.get_last_step_content()
641
953
 
642
- if stream_intermediate_steps and workflow_run_response:
954
+ if workflow_session:
955
+ step_input.workflow_session = workflow_session
956
+
957
+ # Create session_state copy once to avoid duplication.
958
+ # Consider both run_context.session_state and session_state.
959
+ if run_context is not None and run_context.session_state is not None:
960
+ session_state_copy = run_context.session_state
961
+ else:
962
+ session_state_copy = copy(session_state) if session_state is not None else {}
963
+
964
+ # Considering both stream_events and stream_intermediate_steps (deprecated)
965
+ if stream_intermediate_steps is not None:
966
+ warnings.warn(
967
+ "The 'stream_intermediate_steps' parameter is deprecated and will be removed in future versions. Use 'stream_events' instead.",
968
+ DeprecationWarning,
969
+ stacklevel=2,
970
+ )
971
+ stream_events = stream_events or stream_intermediate_steps
972
+
973
+ if stream_events and workflow_run_response:
643
974
  # Emit StepStartedEvent
644
975
  yield StepStartedEvent(
645
976
  run_id=workflow_run_response.run_id or "",
@@ -660,61 +991,117 @@ class Step:
660
991
 
661
992
  if self._executor_type == "function":
662
993
  log_debug(f"Executing async function executor for step: {self.name}")
663
- import inspect
664
994
 
665
995
  # Check if the function is an async generator
666
- if inspect.isasyncgenfunction(self.active_executor):
996
+ if _is_async_generator_function(self.active_executor):
667
997
  content = ""
668
998
  # It's an async generator - iterate over it
669
- async for event in self.active_executor(step_input): # type: ignore
670
- if (
671
- hasattr(event, "content")
672
- and event.content is not None
673
- and isinstance(event.content, str)
674
- ):
675
- content += event.content
676
- else:
677
- content += str(event)
678
- if isinstance(event, StepOutput):
999
+ iterator = await self._acall_custom_function(
1000
+ self.active_executor,
1001
+ step_input,
1002
+ session_state_copy,
1003
+ run_context,
1004
+ )
1005
+ async for event in iterator: # type: ignore
1006
+ if isinstance(event, (BaseRunOutputEvent)):
1007
+ if (
1008
+ isinstance(event, (RunContentEvent, TeamRunContentEvent))
1009
+ and event.content is not None
1010
+ ):
1011
+ if isinstance(event.content, str):
1012
+ content += event.content
1013
+ elif isinstance(event.content, BaseModel):
1014
+ content = event.content # type: ignore[assignment]
1015
+ else:
1016
+ content = str(event.content)
1017
+
1018
+ # Only yield executor events if stream_executor_events is True
1019
+ if stream_executor_events:
1020
+ enriched_event = self._enrich_event_with_context(
1021
+ event, workflow_run_response, step_index
1022
+ )
1023
+ yield enriched_event # type: ignore[misc]
1024
+ elif isinstance(event, (RunOutput, TeamRunOutput)):
1025
+ content = event.content # type: ignore[assignment]
1026
+ elif isinstance(event, StepOutput):
679
1027
  final_response = event
680
1028
  break
681
1029
  else:
682
- yield event # type: ignore[misc]
1030
+ content += str(event)
683
1031
  if not final_response:
684
1032
  final_response = StepOutput(content=content)
685
- elif inspect.iscoroutinefunction(self.active_executor):
1033
+ elif _is_async_callable(self.active_executor):
686
1034
  # It's a regular async function - await it
687
- result = await self.active_executor(step_input) # type: ignore
1035
+ result = await self._acall_custom_function(
1036
+ self.active_executor,
1037
+ step_input,
1038
+ session_state_copy,
1039
+ run_context,
1040
+ )
688
1041
  if isinstance(result, StepOutput):
689
1042
  final_response = result
1043
+ elif isinstance(result, (RunOutput, TeamRunOutput)):
1044
+ final_response = StepOutput(content=result.content)
690
1045
  else:
691
1046
  final_response = StepOutput(content=str(result))
692
- elif inspect.isgeneratorfunction(self.active_executor):
1047
+ elif _is_generator_function(self.active_executor):
693
1048
  content = ""
694
1049
  # It's a regular generator function - iterate over it
695
- for event in self.active_executor(step_input): # type: ignore
696
- if (
697
- hasattr(event, "content")
698
- and event.content is not None
699
- and isinstance(event.content, str)
700
- ):
701
- content += event.content
702
- else:
703
- content += str(event)
704
- if isinstance(event, StepOutput):
1050
+ iterator = self._call_custom_function(
1051
+ self.active_executor,
1052
+ step_input,
1053
+ session_state_copy,
1054
+ run_context,
1055
+ )
1056
+ for event in iterator: # type: ignore
1057
+ if isinstance(event, (BaseRunOutputEvent)):
1058
+ if (
1059
+ isinstance(event, (RunContentEvent, TeamRunContentEvent))
1060
+ and event.content is not None
1061
+ ):
1062
+ if isinstance(event.content, str):
1063
+ content += event.content
1064
+ elif isinstance(event.content, BaseModel):
1065
+ content = event.content # type: ignore[assignment]
1066
+ else:
1067
+ content = str(event.content)
1068
+
1069
+ # Only yield executor events if stream_executor_events is True
1070
+ if stream_executor_events:
1071
+ enriched_event = self._enrich_event_with_context(
1072
+ event, workflow_run_response, step_index
1073
+ )
1074
+ yield enriched_event # type: ignore[misc]
1075
+ elif isinstance(event, (RunOutput, TeamRunOutput)):
1076
+ content = event.content # type: ignore[assignment]
1077
+ elif isinstance(event, StepOutput):
705
1078
  final_response = event
706
1079
  break
707
1080
  else:
708
- yield event # type: ignore[misc]
1081
+ if isinstance(content, str):
1082
+ content += str(event)
1083
+ else:
1084
+ content = str(event)
709
1085
  if not final_response:
710
1086
  final_response = StepOutput(content=content)
711
1087
  else:
712
1088
  # It's a regular function - call it directly
713
- result = self.active_executor(step_input) # type: ignore
1089
+ result = self._call_custom_function(
1090
+ self.active_executor, # type: ignore[arg-type]
1091
+ step_input,
1092
+ session_state_copy,
1093
+ run_context,
1094
+ )
714
1095
  if isinstance(result, StepOutput):
715
1096
  final_response = result
1097
+ elif isinstance(result, (RunOutput, TeamRunOutput)):
1098
+ final_response = StepOutput(content=result.content)
716
1099
  else:
717
1100
  final_response = StepOutput(content=str(result))
1101
+
1102
+ # Merge session_state changes back
1103
+ if run_context is None and session_state is not None:
1104
+ merge_dictionaries(session_state, session_state_copy)
718
1105
  else:
719
1106
  # For agents and teams, prepare message with context
720
1107
  message = self._prepare_message(
@@ -741,9 +1128,22 @@ class Step:
741
1128
  if isinstance(self.active_executor, Team):
742
1129
  kwargs["store_member_responses"] = True
743
1130
 
744
- session_state_copy = copy(session_state)
1131
+ num_history_runs = self.num_history_runs if self.num_history_runs else num_history_runs
1132
+
1133
+ use_history = (
1134
+ self.add_workflow_history
1135
+ if self.add_workflow_history is not None
1136
+ else add_workflow_history_to_steps
1137
+ )
1138
+
1139
+ final_message = message
1140
+ if use_history and workflow_session:
1141
+ history_messages = workflow_session.get_workflow_history_context(num_runs=num_history_runs)
1142
+ if history_messages:
1143
+ final_message = f"{history_messages}{message}"
1144
+
745
1145
  response_stream = self.active_executor.arun( # type: ignore
746
- input=message,
1146
+ input=final_message,
747
1147
  images=images,
748
1148
  videos=videos,
749
1149
  audio=audios,
@@ -752,34 +1152,32 @@ class Step:
752
1152
  user_id=user_id,
753
1153
  session_state=session_state_copy,
754
1154
  stream=True,
755
- stream_intermediate_steps=stream_intermediate_steps,
756
- # Pass workflow context directly via kwargs
757
- workflow_context={
758
- "workflow_id": workflow_run_response.workflow_id if workflow_run_response else None,
759
- "workflow_run_id": workflow_run_response.run_id if workflow_run_response else None,
760
- "step_id": self.step_id,
761
- "step_name": self.name,
762
- "step_index": step_index,
763
- },
1155
+ stream_events=stream_events,
1156
+ run_context=run_context,
764
1157
  yield_run_response=True,
765
1158
  **kwargs,
766
1159
  )
767
1160
 
768
1161
  active_executor_run_response = None
769
1162
  async for event in response_stream:
770
- log_debug(f"Received async event from agent: {type(event).__name__}")
771
1163
  if isinstance(event, RunOutput) or isinstance(event, TeamRunOutput):
772
1164
  active_executor_run_response = event
773
1165
  break
774
- yield event # type: ignore[misc]
1166
+ # Only yield executor events if stream_executor_events is True
1167
+ if stream_executor_events:
1168
+ enriched_event = self._enrich_event_with_context(
1169
+ event, workflow_run_response, step_index
1170
+ )
1171
+ yield enriched_event # type: ignore[misc]
775
1172
 
776
1173
  # Update workflow session state
777
- merge_dictionaries(session_state, session_state_copy) # type: ignore
1174
+ if run_context is None and session_state is not None:
1175
+ merge_dictionaries(session_state, session_state_copy)
778
1176
 
779
1177
  if store_executor_outputs and workflow_run_response is not None:
780
1178
  self._store_executor_response(workflow_run_response, active_executor_run_response) # type: ignore
781
1179
 
782
- final_response = self._process_step_output(active_executor_run_response) # type: ignore
1180
+ final_response = active_executor_run_response # type: ignore
783
1181
  else:
784
1182
  raise ValueError(f"Unsupported executor type: {self._executor_type}")
785
1183
 
@@ -791,9 +1189,10 @@ class Step:
791
1189
  use_workflow_logger()
792
1190
 
793
1191
  # Yield the final response
1192
+ final_response = self._process_step_output(final_response)
794
1193
  yield final_response
795
1194
 
796
- if stream_intermediate_steps and workflow_run_response:
1195
+ if stream_events and workflow_run_response:
797
1196
  # Emit StepCompletedEvent
798
1197
  yield StepCompletedEvent(
799
1198
  run_id=workflow_run_response.run_id or "",
@@ -826,6 +1225,83 @@ class Step:
826
1225
 
827
1226
  return
828
1227
 
1228
+ def get_chat_history(self, session_id: str, last_n_runs: Optional[int] = None) -> List[Message]:
1229
+ """Return the step's Agent or Team chat history for the given session.
1230
+
1231
+ Args:
1232
+ session_id: The session ID to get the chat history for. If not provided, the current cached session ID is used.
1233
+ last_n_runs: Number of recent runs to include. If None, all runs will be considered.
1234
+
1235
+ Returns:
1236
+ List[Message]: The step's Agent or Team chat history for the given session.
1237
+ """
1238
+ session: Union[AgentSession, TeamSession, WorkflowSession, None] = None
1239
+
1240
+ if self.agent:
1241
+ session = self.agent.get_session(session_id=session_id)
1242
+ if not session:
1243
+ log_warning("Session not found")
1244
+ return []
1245
+
1246
+ if not isinstance(session, WorkflowSession):
1247
+ raise ValueError("The provided session is not a WorkflowSession")
1248
+
1249
+ session = cast(WorkflowSession, session)
1250
+ return session.get_messages(last_n_runs=last_n_runs, agent_id=self.agent.id)
1251
+
1252
+ elif self.team:
1253
+ session = self.team.get_session(session_id=session_id)
1254
+ if not session:
1255
+ log_warning("Session not found")
1256
+ return []
1257
+
1258
+ if not isinstance(session, WorkflowSession):
1259
+ raise ValueError("The provided session is not a WorkflowSession")
1260
+
1261
+ session = cast(WorkflowSession, session)
1262
+ return session.get_messages(last_n_runs=last_n_runs, team_id=self.team.id)
1263
+
1264
+ return []
1265
+
1266
+ async def aget_chat_history(
1267
+ self, session_id: Optional[str] = None, last_n_runs: Optional[int] = None
1268
+ ) -> List[Message]:
1269
+ """Return the step's Agent or Team chat history for the given session.
1270
+
1271
+ Args:
1272
+ session_id: The session ID to get the chat history for. If not provided, the current cached session ID is used.
1273
+ last_n_runs: Number of recent runs to include. If None, all runs will be considered.
1274
+
1275
+ Returns:
1276
+ List[Message]: The step's Agent or Team chat history for the given session.
1277
+ """
1278
+ session: Union[AgentSession, TeamSession, WorkflowSession, None] = None
1279
+
1280
+ if self.agent:
1281
+ session = await self.agent.aget_session(session_id=session_id)
1282
+ if not session:
1283
+ log_warning("Session not found")
1284
+ return []
1285
+
1286
+ if not isinstance(session, WorkflowSession):
1287
+ raise ValueError("The provided session is not a WorkflowSession")
1288
+
1289
+ session = cast(WorkflowSession, session)
1290
+ return session.get_messages(last_n_runs=last_n_runs, agent_id=self.agent.id)
1291
+
1292
+ elif self.team:
1293
+ session = await self.team.aget_session(session_id=session_id)
1294
+ if not session:
1295
+ log_warning("Session not found")
1296
+ return []
1297
+
1298
+ if not isinstance(session, WorkflowSession):
1299
+ raise ValueError("The provided session is not a WorkflowSession")
1300
+
1301
+ return session.get_messages(last_n_runs=last_n_runs, team_id=self.team.id)
1302
+
1303
+ return []
1304
+
829
1305
  def _store_executor_response(
830
1306
  self, workflow_run_response: "WorkflowRunOutput", executor_run_response: Union[RunOutput, TeamRunOutput]
831
1307
  ) -> None:
@@ -835,6 +1311,14 @@ class Step:
835
1311
  executor_run_response.parent_run_id = workflow_run_response.run_id
836
1312
  executor_run_response.workflow_step_id = self.step_id
837
1313
 
1314
+ # Scrub the executor response based on the executor's storage flags before storing
1315
+ if (
1316
+ not self.active_executor.store_media
1317
+ or not self.active_executor.store_tool_messages
1318
+ or not self.active_executor.store_history_messages
1319
+ ): # type: ignore
1320
+ self.active_executor._scrub_run_output_for_storage(executor_run_response) # type: ignore
1321
+
838
1322
  # Get the raw response from the step's active executor
839
1323
  raw_response = executor_run_response
840
1324
  if raw_response and isinstance(raw_response, (RunOutput, TeamRunOutput)):
@@ -1021,3 +1505,28 @@ class Step:
1021
1505
  continue
1022
1506
 
1023
1507
  return videos
1508
+
1509
+
1510
+ def _is_async_callable(obj: Any) -> TypeGuard[Callable[..., Any]]:
1511
+ """Checks if obj is an async callable (coroutine function or callable with async __call__)"""
1512
+ return inspect.iscoroutinefunction(obj) or (callable(obj) and inspect.iscoroutinefunction(obj.__call__))
1513
+
1514
+
1515
+ def _is_generator_function(obj: Any) -> TypeGuard[Callable[..., Any]]:
1516
+ """Checks if obj is a generator function, including callable class instances with generator __call__ methods"""
1517
+ if inspect.isgeneratorfunction(obj):
1518
+ return True
1519
+ # Check if it's a callable class instance with a generator __call__ method
1520
+ if callable(obj) and hasattr(obj, "__call__"):
1521
+ return inspect.isgeneratorfunction(obj.__call__)
1522
+ return False
1523
+
1524
+
1525
+ def _is_async_generator_function(obj: Any) -> TypeGuard[Callable[..., Any]]:
1526
+ """Checks if obj is an async generator function, including callable class instances"""
1527
+ if inspect.isasyncgenfunction(obj):
1528
+ return True
1529
+ # Check if it's a callable class instance with an async generator __call__ method
1530
+ if callable(obj) and hasattr(obj, "__call__"):
1531
+ return inspect.isasyncgenfunction(obj.__call__)
1532
+ return False