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/workflow.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import warnings
2
3
  from dataclasses import dataclass
3
4
  from datetime import datetime
4
5
  from os import getenv
@@ -24,13 +25,13 @@ from fastapi import WebSocket
24
25
  from pydantic import BaseModel
25
26
 
26
27
  from agno.agent.agent import Agent
27
- from agno.db.base import BaseDb, SessionType
28
+ from agno.db.base import AsyncBaseDb, BaseDb, SessionType
28
29
  from agno.exceptions import InputCheckError, OutputCheckError, RunCancelledException
29
30
  from agno.media import Audio, File, Image, Video
30
31
  from agno.models.message import Message
31
32
  from agno.models.metrics import Metrics
32
- from agno.run.agent import RunEvent
33
- from agno.run.base import RunStatus
33
+ from agno.run import RunContext, RunStatus
34
+ from agno.run.agent import RunContentEvent, RunEvent, RunOutput
34
35
  from agno.run.cancel import (
35
36
  cancel_run as cancel_run_global,
36
37
  )
@@ -39,6 +40,7 @@ from agno.run.cancel import (
39
40
  raise_if_cancelled,
40
41
  register_run,
41
42
  )
43
+ from agno.run.team import RunContentEvent as TeamRunContentEvent
42
44
  from agno.run.team import TeamRunEvent
43
45
  from agno.run.workflow import (
44
46
  StepOutputEvent,
@@ -49,7 +51,7 @@ from agno.run.workflow import (
49
51
  WorkflowRunOutputEvent,
50
52
  WorkflowStartedEvent,
51
53
  )
52
- from agno.session.workflow import WorkflowSession
54
+ from agno.session.workflow import WorkflowChatInteraction, WorkflowSession
53
55
  from agno.team.team import Team
54
56
  from agno.utils.common import is_typed_dict, validate_typed_dict
55
57
  from agno.utils.log import (
@@ -67,6 +69,7 @@ from agno.utils.print_response.workflow import (
67
69
  print_response,
68
70
  print_response_stream,
69
71
  )
72
+ from agno.workflow import WorkflowAgent
70
73
  from agno.workflow.condition import Condition
71
74
  from agno.workflow.loop import Loop
72
75
  from agno.workflow.parallel import Parallel
@@ -129,7 +132,10 @@ class Workflow:
129
132
  steps: Optional[WorkflowSteps] = None
130
133
 
131
134
  # Database to use for this workflow
132
- db: Optional[BaseDb] = None
135
+ db: Optional[Union[BaseDb, AsyncBaseDb]] = None
136
+
137
+ # Agentic Workflow - WorkflowAgent that decides when to run the workflow
138
+ agent: Optional[WorkflowAgent] = None # type: ignore
133
139
 
134
140
  # Default session_id to use for this workflow (autogenerated if not set)
135
141
  session_id: Optional[str] = None
@@ -147,7 +153,9 @@ class Workflow:
147
153
  # Stream the response from the Workflow
148
154
  stream: Optional[bool] = None
149
155
  # Stream the intermediate steps from the Workflow
150
- stream_intermediate_steps: bool = False
156
+ stream_events: bool = False
157
+ # Stream events from executors (agents/teams/functions) within steps
158
+ stream_executor_events: bool = True
151
159
 
152
160
  # Persist the events on the run response
153
161
  store_events: bool = False
@@ -170,20 +178,35 @@ class Workflow:
170
178
  # This helps us improve the Agent and provide better support
171
179
  telemetry: bool = True
172
180
 
181
+ # Add this flag to control if the workflow should add history to the steps
182
+ add_workflow_history_to_steps: bool = False
183
+ # Number of historical runs to include in the messages
184
+ num_history_runs: int = 3
185
+
186
+ # Deprecated. Use stream_events instead.
187
+ stream_intermediate_steps: bool = False
188
+
189
+ # If True, run hooks as FastAPI background tasks (non-blocking). Set by AgentOS.
190
+ _run_hooks_in_background: bool = False
191
+
173
192
  def __init__(
174
193
  self,
175
194
  id: Optional[str] = None,
176
195
  name: Optional[str] = None,
177
196
  description: Optional[str] = None,
178
- db: Optional[BaseDb] = None,
197
+ db: Optional[Union[BaseDb, AsyncBaseDb]] = None,
179
198
  steps: Optional[WorkflowSteps] = None,
199
+ agent: Optional[WorkflowAgent] = None,
180
200
  session_id: Optional[str] = None,
181
201
  session_state: Optional[Dict[str, Any]] = None,
182
202
  overwrite_db_session_state: bool = False,
183
203
  user_id: Optional[str] = None,
204
+ debug_level: Literal[1, 2] = 1,
184
205
  debug_mode: Optional[bool] = False,
185
206
  stream: Optional[bool] = None,
207
+ stream_events: bool = False,
186
208
  stream_intermediate_steps: bool = False,
209
+ stream_executor_events: bool = True,
187
210
  store_events: bool = False,
188
211
  events_to_skip: Optional[List[Union[WorkflowRunEvent, RunEvent, TeamRunEvent]]] = None,
189
212
  store_executor_outputs: bool = True,
@@ -191,29 +214,49 @@ class Workflow:
191
214
  metadata: Optional[Dict[str, Any]] = None,
192
215
  cache_session: bool = False,
193
216
  telemetry: bool = True,
217
+ add_workflow_history_to_steps: bool = False,
218
+ num_history_runs: int = 3,
194
219
  ):
195
220
  self.id = id
196
221
  self.name = name
197
222
  self.description = description
198
223
  self.steps = steps
224
+ self.agent = agent
199
225
  self.session_id = session_id
200
226
  self.session_state = session_state
201
227
  self.overwrite_db_session_state = overwrite_db_session_state
202
228
  self.user_id = user_id
203
229
  self.debug_mode = debug_mode
230
+ self.debug_level = debug_level
204
231
  self.store_events = store_events
205
232
  self.events_to_skip = events_to_skip or []
206
233
  self.stream = stream
207
- self.stream_intermediate_steps = stream_intermediate_steps
234
+ self.stream_executor_events = stream_executor_events
208
235
  self.store_executor_outputs = store_executor_outputs
209
236
  self.input_schema = input_schema
210
237
  self.metadata = metadata
211
238
  self.cache_session = cache_session
212
239
  self.db = db
213
240
  self.telemetry = telemetry
214
-
241
+ self.add_workflow_history_to_steps = add_workflow_history_to_steps
242
+ self.num_history_runs = num_history_runs
215
243
  self._workflow_session: Optional[WorkflowSession] = None
216
244
 
245
+ if stream_intermediate_steps:
246
+ warnings.warn(
247
+ "The 'stream_intermediate_steps' parameter is deprecated and will be removed in future versions. Use 'stream_events' instead.",
248
+ DeprecationWarning,
249
+ stacklevel=2,
250
+ )
251
+ self.stream_events = stream_events or stream_intermediate_steps
252
+
253
+ # Warn if workflow history is enabled without a database
254
+ if self.add_workflow_history_to_steps and self.db is None:
255
+ log_warning(
256
+ "Workflow history is enabled (add_workflow_history_to_steps=True) but no database is configured. "
257
+ "History won't be persisted. Add a database to persist runs across executions. "
258
+ )
259
+
217
260
  def set_id(self) -> None:
218
261
  if self.id is None:
219
262
  if self.name is not None:
@@ -221,6 +264,9 @@ class Workflow:
221
264
  else:
222
265
  self.id = str(uuid4())
223
266
 
267
+ def _has_async_db(self) -> bool:
268
+ return self.db is not None and isinstance(self.db, AsyncBaseDb)
269
+
224
270
  def _validate_input(
225
271
  self, input: Optional[Union[str, Dict[str, Any], List[Any], BaseModel, List[Message]]]
226
272
  ) -> Optional[Union[str, List, Dict, Message, BaseModel]]:
@@ -329,10 +375,8 @@ class Workflow:
329
375
  self,
330
376
  session_id: Optional[str] = None,
331
377
  user_id: Optional[str] = None,
332
- session_state: Optional[Dict[str, Any]] = None,
333
- run_id: Optional[str] = None,
334
- ) -> Tuple[str, Optional[str], Dict[str, Any]]:
335
- """Initialize the session for the agent."""
378
+ ) -> Tuple[str, Optional[str]]:
379
+ """Initialize the session for the workflow."""
336
380
 
337
381
  if session_id is None:
338
382
  if self.session_id:
@@ -345,27 +389,25 @@ class Workflow:
345
389
  log_debug(f"Session ID: {session_id}", center=True)
346
390
 
347
391
  # Use the default user_id when necessary
348
- if user_id is None:
392
+ if user_id is None or user_id == "":
349
393
  user_id = self.user_id
350
394
 
351
- # Determine the session_state with proper precedence
352
- if session_state is None:
353
- session_state = self.session_state or {}
354
- else:
355
- # If run session_state is provided, merge agent defaults under it
356
- # This ensures run state takes precedence over agent defaults
357
- if self.session_state:
358
- from agno.utils.merge_dict import merge_dictionaries
359
-
360
- base_state = self.session_state.copy()
361
- merge_dictionaries(base_state, session_state)
362
- session_state.clear()
363
- session_state.update(base_state)
395
+ return session_id, user_id
364
396
 
365
- if user_id is not None:
397
+ def _initialize_session_state(
398
+ self,
399
+ session_state: Dict[str, Any],
400
+ user_id: Optional[str] = None,
401
+ session_id: Optional[str] = None,
402
+ run_id: Optional[str] = None,
403
+ ) -> Dict[str, Any]:
404
+ """Initialize the session state for the workflow."""
405
+ if user_id:
366
406
  session_state["current_user_id"] = user_id
367
407
  if session_id is not None:
368
408
  session_state["current_session_id"] = session_id
409
+ if run_id is not None:
410
+ session_state["current_run_id"] = run_id
369
411
 
370
412
  session_state.update(
371
413
  {
@@ -377,7 +419,7 @@ class Workflow:
377
419
  if self.name:
378
420
  session_state["workflow_name"] = self.name
379
421
 
380
- return session_id, user_id, session_state # type: ignore
422
+ return session_state
381
423
 
382
424
  def _generate_workflow_session_name(self) -> str:
383
425
  """Generate a name for the workflow session"""
@@ -393,6 +435,33 @@ class Workflow:
393
435
  new_session_name = f"{truncated_desc} - {datetime_str}"
394
436
  return new_session_name
395
437
 
438
+ async def aset_session_name(
439
+ self, session_id: Optional[str] = None, autogenerate: bool = False, session_name: Optional[str] = None
440
+ ) -> WorkflowSession:
441
+ """Set the session name and save to storage, using an async database"""
442
+ session_id = session_id or self.session_id
443
+
444
+ if session_id is None:
445
+ raise Exception("Session ID is not set")
446
+
447
+ # -*- Read from storage
448
+ session = await self.aget_session(session_id=session_id) # type: ignore
449
+
450
+ if autogenerate:
451
+ # -*- Generate name for session
452
+ session_name = self._generate_workflow_session_name()
453
+ log_debug(f"Generated Workflow Session Name: {session_name}")
454
+ elif session_name is None:
455
+ raise Exception("Session name is not set")
456
+
457
+ # -*- Rename session
458
+ session.session_data["session_name"] = session_name # type: ignore
459
+
460
+ # -*- Save to storage
461
+ await self.asave_session(session=session) # type: ignore
462
+
463
+ return session # type: ignore
464
+
396
465
  def set_session_name(
397
466
  self, session_id: Optional[str] = None, autogenerate: bool = False, session_name: Optional[str] = None
398
467
  ) -> WorkflowSession:
@@ -420,6 +489,16 @@ class Workflow:
420
489
 
421
490
  return session # type: ignore
422
491
 
492
+ async def aget_session_name(self, session_id: Optional[str] = None) -> str:
493
+ """Get the session name for the given session ID and user ID."""
494
+ session_id = session_id or self.session_id
495
+ if session_id is None:
496
+ raise Exception("Session ID is not set")
497
+ session = await self.aget_session(session_id=session_id) # type: ignore
498
+ if session is None:
499
+ raise Exception("Session not found")
500
+ return session.session_data.get("session_name", "") if session.session_data else ""
501
+
423
502
  def get_session_name(self, session_id: Optional[str] = None) -> str:
424
503
  """Get the session name for the given session ID and user ID."""
425
504
  session_id = session_id or self.session_id
@@ -430,6 +509,16 @@ class Workflow:
430
509
  raise Exception("Session not found")
431
510
  return session.session_data.get("session_name", "") if session.session_data else ""
432
511
 
512
+ async def aget_session_state(self, session_id: Optional[str] = None) -> Dict[str, Any]:
513
+ """Get the session state for the given session ID and user ID."""
514
+ session_id = session_id or self.session_id
515
+ if session_id is None:
516
+ raise Exception("Session ID is not set")
517
+ session = await self.aget_session(session_id=session_id) # type: ignore
518
+ if session is None:
519
+ raise Exception("Session not found")
520
+ return session.session_data.get("session_state", {}) if session.session_data else {}
521
+
433
522
  def get_session_state(self, session_id: Optional[str] = None) -> Dict[str, Any]:
434
523
  """Get the session state for the given session ID and user ID."""
435
524
  session_id = session_id or self.session_id
@@ -440,6 +529,69 @@ class Workflow:
440
529
  raise Exception("Session not found")
441
530
  return session.session_data.get("session_state", {}) if session.session_data else {}
442
531
 
532
+ def update_session_state(
533
+ self, session_state_updates: Dict[str, Any], session_id: Optional[str] = None
534
+ ) -> Dict[str, Any]:
535
+ """
536
+ Update the session state for the given session ID.
537
+ Args:
538
+ session_state_updates: The updates to apply to the session state. Should be a dictionary of key-value pairs.
539
+ session_id: The session ID to update. If not provided, the current cached session ID is used.
540
+ Returns:
541
+ dict: The updated session state.
542
+ """
543
+ session_id = session_id or self.session_id
544
+ if session_id is None:
545
+ raise Exception("Session ID is not set")
546
+ session = self.get_session(session_id=session_id) # type: ignore
547
+ if session is None:
548
+ raise Exception("Session not found")
549
+
550
+ if session.session_data is not None and "session_state" not in session.session_data:
551
+ session.session_data["session_state"] = {}
552
+
553
+ for key, value in session_state_updates.items():
554
+ session.session_data["session_state"][key] = value # type: ignore
555
+
556
+ self.save_session(session=session)
557
+
558
+ return session.session_data["session_state"] # type: ignore
559
+
560
+ async def aupdate_session_state(
561
+ self, session_state_updates: Dict[str, Any], session_id: Optional[str] = None
562
+ ) -> Dict[str, Any]:
563
+ """
564
+ Update the session state for the given session ID (async).
565
+ Args:
566
+ session_state_updates: The updates to apply to the session state. Should be a dictionary of key-value pairs.
567
+ session_id: The session ID to update. If not provided, the current cached session ID is used.
568
+ Returns:
569
+ dict: The updated session state.
570
+ """
571
+ session_id = session_id or self.session_id
572
+ if session_id is None:
573
+ raise Exception("Session ID is not set")
574
+ session = await self.aget_session(session_id=session_id) # type: ignore
575
+ if session is None:
576
+ raise Exception("Session not found")
577
+
578
+ if session.session_data is not None and "session_state" not in session.session_data:
579
+ session.session_data["session_state"] = {} # type: ignore
580
+
581
+ for key, value in session_state_updates.items():
582
+ session.session_data["session_state"][key] = value # type: ignore
583
+
584
+ await self.asave_session(session=session)
585
+
586
+ return session.session_data["session_state"] # type: ignore
587
+
588
+ async def adelete_session(self, session_id: str):
589
+ """Delete the current session and save to storage"""
590
+ if self.db is None:
591
+ return
592
+ # -*- Delete session
593
+ await self.db.delete_session(session_id=session_id) # type: ignore
594
+
443
595
  def delete_session(self, session_id: str):
444
596
  """Delete the current session and save to storage"""
445
597
  if self.db is None:
@@ -447,6 +599,25 @@ class Workflow:
447
599
  # -*- Delete session
448
600
  self.db.delete_session(session_id=session_id)
449
601
 
602
+ async def aget_run_output(self, run_id: str, session_id: Optional[str] = None) -> Optional[WorkflowRunOutput]:
603
+ """Get a RunOutput from the database."""
604
+ if self._workflow_session is not None:
605
+ run_response = self._workflow_session.get_run(run_id=run_id)
606
+ if run_response is not None:
607
+ return run_response
608
+ else:
609
+ log_warning(f"RunOutput {run_id} not found in AgentSession {self._workflow_session.session_id}")
610
+ return None
611
+ else:
612
+ workflow_session = await self.aget_session(session_id=session_id) # type: ignore
613
+ if workflow_session is not None:
614
+ run_response = workflow_session.get_run(run_id=run_id)
615
+ if run_response is not None:
616
+ return run_response
617
+ else:
618
+ log_warning(f"RunOutput {run_id} not found in AgentSession {session_id}")
619
+ return None
620
+
450
621
  def get_run_output(self, run_id: str, session_id: Optional[str] = None) -> Optional[WorkflowRunOutput]:
451
622
  """Get a RunOutput from the database."""
452
623
  if self._workflow_session is not None:
@@ -466,6 +637,26 @@ class Workflow:
466
637
  log_warning(f"RunOutput {run_id} not found in AgentSession {session_id}")
467
638
  return None
468
639
 
640
+ async def aget_last_run_output(self, session_id: Optional[str] = None) -> Optional[WorkflowRunOutput]:
641
+ """Get the last run response from the database."""
642
+ if (
643
+ self._workflow_session is not None
644
+ and self._workflow_session.runs is not None
645
+ and len(self._workflow_session.runs) > 0
646
+ ):
647
+ run_response = self._workflow_session.runs[-1]
648
+ if run_response is not None:
649
+ return run_response
650
+ else:
651
+ workflow_session = await self.aget_session(session_id=session_id) # type: ignore
652
+ if workflow_session is not None and workflow_session.runs is not None and len(workflow_session.runs) > 0:
653
+ run_response = workflow_session.runs[-1]
654
+ if run_response is not None:
655
+ return run_response
656
+ else:
657
+ log_warning(f"No run responses found in WorkflowSession {session_id}")
658
+ return None
659
+
469
660
  def get_last_run_output(self, session_id: Optional[str] = None) -> Optional[WorkflowRunOutput]:
470
661
  """Get the last run response from the database."""
471
662
  if (
@@ -504,6 +695,48 @@ class Workflow:
504
695
 
505
696
  workflow_session = cast(WorkflowSession, self._read_session(session_id=session_id))
506
697
 
698
+ if workflow_session is None:
699
+ # Creating new session if none found
700
+ log_debug(f"Creating new WorkflowSession: {session_id}")
701
+ session_data = {}
702
+ if self.session_state is not None:
703
+ from copy import deepcopy
704
+
705
+ session_data["session_state"] = deepcopy(self.session_state)
706
+ workflow_session = WorkflowSession(
707
+ session_id=session_id,
708
+ workflow_id=self.id,
709
+ user_id=user_id,
710
+ workflow_data=self._get_workflow_data(),
711
+ session_data=session_data,
712
+ metadata=self.metadata,
713
+ created_at=int(time()),
714
+ )
715
+
716
+ # Cache the session if relevant
717
+ if workflow_session is not None and self.cache_session:
718
+ self._workflow_session = workflow_session
719
+
720
+ return workflow_session
721
+
722
+ async def aread_or_create_session(
723
+ self,
724
+ session_id: str,
725
+ user_id: Optional[str] = None,
726
+ ) -> WorkflowSession:
727
+ from time import time
728
+
729
+ # Returning cached session if we have one
730
+ if self._workflow_session is not None and self._workflow_session.session_id == session_id:
731
+ return self._workflow_session
732
+
733
+ # Try to load from database
734
+ workflow_session = None
735
+ if self.db is not None:
736
+ log_debug(f"Reading WorkflowSession: {session_id}")
737
+
738
+ workflow_session = cast(WorkflowSession, await self._aread_session(session_id=session_id))
739
+
507
740
  if workflow_session is None:
508
741
  # Creating new session if none found
509
742
  log_debug(f"Creating new WorkflowSession: {session_id}")
@@ -523,6 +756,30 @@ class Workflow:
523
756
 
524
757
  return workflow_session
525
758
 
759
+ async def aget_session(
760
+ self,
761
+ session_id: Optional[str] = None,
762
+ ) -> Optional[WorkflowSession]:
763
+ """Load an WorkflowSession from database.
764
+
765
+ Args:
766
+ session_id: The session_id to load from storage.
767
+
768
+ Returns:
769
+ WorkflowSession: The WorkflowSession loaded from the database or created if it does not exist.
770
+ """
771
+ session_id_to_load = session_id or self.session_id
772
+ if session_id_to_load is None:
773
+ raise Exception("No session_id provided")
774
+
775
+ # Try to load from database
776
+ if self.db is not None:
777
+ workflow_session = cast(WorkflowSession, await self._aread_session(session_id=session_id_to_load))
778
+ return workflow_session
779
+
780
+ log_warning(f"WorkflowSession {session_id_to_load} not found in db")
781
+ return None
782
+
526
783
  def get_session(
527
784
  self,
528
785
  session_id: Optional[str] = None,
@@ -548,6 +805,25 @@ class Workflow:
548
805
  log_warning(f"WorkflowSession {session_id_to_load} not found in db")
549
806
  return None
550
807
 
808
+ async def asave_session(self, session: WorkflowSession) -> None:
809
+ """Save the WorkflowSession to storage, using an async database.
810
+
811
+ Returns:
812
+ Optional[WorkflowSession]: The saved WorkflowSession or None if not saved.
813
+ """
814
+ if self.db is not None and session.session_data is not None:
815
+ if session.session_data.get("session_state") is not None:
816
+ session.session_data["session_state"].pop("current_session_id", None)
817
+ session.session_data["session_state"].pop("current_user_id", None)
818
+ session.session_data["session_state"].pop("current_run_id", None)
819
+ session.session_data["session_state"].pop("workflow_id", None)
820
+ session.session_data["session_state"].pop("run_id", None)
821
+ session.session_data["session_state"].pop("session_id", None)
822
+ session.session_data["session_state"].pop("workflow_name", None)
823
+
824
+ await self._aupsert_session(session=session) # type: ignore
825
+ log_debug(f"Created or updated WorkflowSession record: {session.session_id}")
826
+
551
827
  def save_session(self, session: WorkflowSession) -> None:
552
828
  """Save the WorkflowSession to storage
553
829
 
@@ -567,7 +843,66 @@ class Workflow:
567
843
  self._upsert_session(session=session)
568
844
  log_debug(f"Created or updated WorkflowSession record: {session.session_id}")
569
845
 
846
+ def get_chat_history(
847
+ self, session_id: Optional[str] = None, last_n_runs: Optional[int] = None
848
+ ) -> List[WorkflowChatInteraction]:
849
+ """Return a list of dictionaries containing the input and output for each run in the session.
850
+
851
+ Args:
852
+ session_id: The session ID to get the chat history for. If not provided, the current cached session ID is used.
853
+ last_n_runs: Number of recent runs to include. If None, all runs will be considered.
854
+
855
+ Returns:
856
+ A list of WorkflowChatInteraction objects.
857
+ """
858
+ session_id = session_id or self.session_id
859
+ if session_id is None:
860
+ log_warning("Session ID is not set, cannot get messages for session")
861
+ return []
862
+
863
+ session = self.get_session(
864
+ session_id=session_id,
865
+ )
866
+ if session is None:
867
+ raise Exception("Session not found")
868
+
869
+ return session.get_chat_history(last_n_runs=last_n_runs)
870
+
871
+ async def aget_chat_history(
872
+ self, session_id: Optional[str] = None, last_n_runs: Optional[int] = None
873
+ ) -> List[WorkflowChatInteraction]:
874
+ """Return a list of dictionaries containing the input and output for each run in the session.
875
+
876
+ Args:
877
+ session_id: The session ID to get the chat history for. If not provided, the current cached session ID is used.
878
+ last_n_runs: Number of recent runs to include. If None, all runs will be considered.
879
+
880
+ Returns:
881
+ A list of dictionaries containing the input and output for each run.
882
+ """
883
+ session_id = session_id or self.session_id
884
+ if session_id is None:
885
+ log_warning("Session ID is not set, cannot get messages for session")
886
+ return []
887
+
888
+ session = await self.aget_session(session_id=session_id)
889
+ if session is None:
890
+ raise Exception("Session not found")
891
+
892
+ return session.get_chat_history(last_n_runs=last_n_runs)
893
+
570
894
  # -*- Session Database Functions
895
+ async def _aread_session(self, session_id: str) -> Optional[WorkflowSession]:
896
+ """Get a Session from the database."""
897
+ try:
898
+ if not self.db:
899
+ raise ValueError("Db not initialized")
900
+ session = await self.db.get_session(session_id=session_id, session_type=SessionType.WORKFLOW) # type: ignore
901
+ return session if isinstance(session, (WorkflowSession, type(None))) else None
902
+ except Exception as e:
903
+ log_warning(f"Error getting session from db: {e}")
904
+ return None
905
+
571
906
  def _read_session(self, session_id: str) -> Optional[WorkflowSession]:
572
907
  """Get a Session from the database."""
573
908
  try:
@@ -579,9 +914,19 @@ class Workflow:
579
914
  log_warning(f"Error getting session from db: {e}")
580
915
  return None
581
916
 
582
- def _upsert_session(self, session: WorkflowSession) -> Optional[WorkflowSession]:
917
+ async def _aupsert_session(self, session: WorkflowSession) -> Optional[WorkflowSession]:
583
918
  """Upsert a Session into the database."""
919
+ try:
920
+ if not self.db:
921
+ raise ValueError("Db not initialized")
922
+ result = await self.db.upsert_session(session=session) # type: ignore
923
+ return result if isinstance(result, (WorkflowSession, type(None))) else None
924
+ except Exception as e:
925
+ log_warning(f"Error upserting session into db: {e}")
926
+ return None
584
927
 
928
+ def _upsert_session(self, session: WorkflowSession) -> Optional[WorkflowSession]:
929
+ """Upsert a Session into the database."""
585
930
  try:
586
931
  if not self.db:
587
932
  raise ValueError("Db not initialized")
@@ -649,7 +994,7 @@ class Workflow:
649
994
  else:
650
995
  step_type = STEP_TYPE_MAPPING[type(step)]
651
996
  step_dict = {
652
- "name": step.name if hasattr(step, "name") else step.__name__,
997
+ "name": step.name if hasattr(step, "name") else step.__name__, # type: ignore
653
998
  "description": step.description if hasattr(step, "description") else "User-defined callable step",
654
999
  "type": step_type.value,
655
1000
  }
@@ -668,16 +1013,36 @@ class Workflow:
668
1013
 
669
1014
  return workflow_data
670
1015
 
671
- def _handle_event(
1016
+ def _broadcast_to_websocket(
672
1017
  self,
673
- event: "WorkflowRunOutputEvent",
674
- workflow_run_response: WorkflowRunOutput,
1018
+ event: Any,
675
1019
  websocket_handler: Optional[WebSocketHandler] = None,
676
- ) -> "WorkflowRunOutputEvent":
677
- """Handle workflow events for storage - similar to Team._handle_event"""
678
- if self.store_events:
679
- # Check if this event type should be skipped
680
- if self.events_to_skip:
1020
+ ) -> None:
1021
+ """Broadcast events to WebSocket if available (async context only)"""
1022
+ if websocket_handler:
1023
+ try:
1024
+ loop = asyncio.get_running_loop()
1025
+ if loop:
1026
+ asyncio.create_task(websocket_handler.handle_event(event))
1027
+ except RuntimeError:
1028
+ pass
1029
+
1030
+ def _handle_event(
1031
+ self,
1032
+ event: "WorkflowRunOutputEvent",
1033
+ workflow_run_response: WorkflowRunOutput,
1034
+ websocket_handler: Optional[WebSocketHandler] = None,
1035
+ ) -> "WorkflowRunOutputEvent":
1036
+ """Handle workflow events for storage - similar to Team._handle_event"""
1037
+ from agno.run.agent import RunOutput
1038
+ from agno.run.base import BaseRunOutputEvent
1039
+ from agno.run.team import TeamRunOutput
1040
+
1041
+ if isinstance(event, (RunOutput, TeamRunOutput)):
1042
+ return event
1043
+ if self.store_events:
1044
+ # Check if this event type should be skipped
1045
+ if self.events_to_skip:
681
1046
  event_type = event.event
682
1047
  for skip_event in self.events_to_skip:
683
1048
  if isinstance(skip_event, str):
@@ -689,21 +1054,41 @@ class Workflow:
689
1054
  return event
690
1055
 
691
1056
  # Store the event
692
- if workflow_run_response.events is None:
693
- workflow_run_response.events = []
694
-
695
- workflow_run_response.events.append(event)
1057
+ if isinstance(event, BaseRunOutputEvent):
1058
+ if workflow_run_response.events is None:
1059
+ workflow_run_response.events = []
1060
+ workflow_run_response.events.append(event)
696
1061
 
697
1062
  # Broadcast to WebSocket if available (async context only)
698
- if websocket_handler:
699
- import asyncio
1063
+ self._broadcast_to_websocket(event, websocket_handler)
700
1064
 
701
- try:
702
- loop = asyncio.get_running_loop()
703
- if loop:
704
- asyncio.create_task(websocket_handler.handle_event(event))
705
- except RuntimeError:
706
- pass
1065
+ return event
1066
+
1067
+ def _enrich_event_with_workflow_context(
1068
+ self,
1069
+ event: Any,
1070
+ workflow_run_response: WorkflowRunOutput,
1071
+ step_index: Optional[Union[int, tuple]] = None,
1072
+ step: Optional[Any] = None,
1073
+ ) -> Any:
1074
+ """Enrich any event with workflow context information for frontend tracking"""
1075
+
1076
+ step_id = getattr(step, "step_id", None) if step else None
1077
+ step_name = getattr(step, "name", None) if step else None
1078
+
1079
+ if hasattr(event, "workflow_id"):
1080
+ event.workflow_id = workflow_run_response.workflow_id
1081
+ if hasattr(event, "workflow_run_id"):
1082
+ event.workflow_run_id = workflow_run_response.run_id
1083
+ if hasattr(event, "step_id") and step_id:
1084
+ event.step_id = step_id
1085
+ if hasattr(event, "step_name") and step_name is not None:
1086
+ if event.step_name is None:
1087
+ event.step_name = step_name
1088
+ # Only set step_index if it's not already set (preserve parallel.py's tuples)
1089
+ if hasattr(event, "step_index") and step_index is not None:
1090
+ if event.step_index is None:
1091
+ event.step_index = step_index
707
1092
 
708
1093
  return event
709
1094
 
@@ -725,9 +1110,12 @@ class Workflow:
725
1110
  """Set debug mode and configure logging"""
726
1111
  if self.debug_mode or getenv("AGNO_DEBUG", "false").lower() == "true":
727
1112
  use_workflow_logger()
1113
+ debug_level: Literal[1, 2] = (
1114
+ cast(Literal[1, 2], int(env)) if (env := getenv("AGNO_DEBUG_LEVEL")) in ("1", "2") else self.debug_level
1115
+ )
728
1116
 
729
1117
  self.debug_mode = True
730
- set_log_level_to_debug(source_type="workflow")
1118
+ set_log_level_to_debug(source_type="workflow", level=debug_level)
731
1119
 
732
1120
  # Propagate to steps - only if steps is iterable (not callable)
733
1121
  if self.steps and not callable(self.steps):
@@ -811,7 +1199,11 @@ class Workflow:
811
1199
  else:
812
1200
  return len(self.steps)
813
1201
 
814
- def _aggregate_workflow_metrics(self, step_results: List[Union[StepOutput, List[StepOutput]]]) -> WorkflowMetrics:
1202
+ def _aggregate_workflow_metrics(
1203
+ self,
1204
+ step_results: List[Union[StepOutput, List[StepOutput]]],
1205
+ current_workflow_metrics: Optional[WorkflowMetrics] = None,
1206
+ ) -> WorkflowMetrics:
815
1207
  """Aggregate metrics from all step responses into structured workflow metrics"""
816
1208
  steps_dict = {}
817
1209
 
@@ -839,8 +1231,13 @@ class Workflow:
839
1231
  for step_result in step_results:
840
1232
  process_step_output(cast(StepOutput, step_result))
841
1233
 
1234
+ duration = None
1235
+ if current_workflow_metrics and current_workflow_metrics.duration is not None:
1236
+ duration = current_workflow_metrics.duration
1237
+
842
1238
  return WorkflowMetrics(
843
1239
  steps=steps_dict,
1240
+ duration=duration,
844
1241
  )
845
1242
 
846
1243
  def _call_custom_function(self, func: Callable, execution_input: WorkflowExecutionInput, **kwargs: Any) -> Any:
@@ -875,24 +1272,31 @@ class Workflow:
875
1272
  return func(**call_kwargs)
876
1273
  except TypeError as e:
877
1274
  # If signature inspection fails, fall back to original method
878
- logger.error(
879
- f"Function signature inspection failed: {e}. Falling back to original calling convention."
880
- )
1275
+ logger.error(f"Function signature inspection failed: {e}. Falling back to original calling convention.")
881
1276
  return func(**kwargs)
882
1277
 
1278
+ def _accumulate_partial_step_data(
1279
+ self, event: Union[RunContentEvent, TeamRunContentEvent], partial_step_content: str
1280
+ ) -> str:
1281
+ """Accumulate partial step data from streaming events"""
1282
+ if isinstance(event, (RunContentEvent, TeamRunContentEvent)) and event.content:
1283
+ if isinstance(event.content, str):
1284
+ partial_step_content += event.content
1285
+ return partial_step_content
1286
+
883
1287
  def _execute(
884
1288
  self,
885
1289
  session: WorkflowSession,
886
1290
  execution_input: WorkflowExecutionInput,
887
1291
  workflow_run_response: WorkflowRunOutput,
888
- session_state: Optional[Dict[str, Any]] = None,
1292
+ run_context: RunContext,
1293
+ background_tasks: Optional[Any] = None,
889
1294
  **kwargs: Any,
890
1295
  ) -> WorkflowRunOutput:
891
1296
  """Execute a specific pipeline by name synchronously"""
892
1297
  from inspect import isasyncgenfunction, iscoroutinefunction, isgeneratorfunction
893
1298
 
894
1299
  workflow_run_response.status = RunStatus.running
895
- register_run(workflow_run_response.run_id) # type: ignore
896
1300
 
897
1301
  if callable(self.steps):
898
1302
  if iscoroutinefunction(self.steps) or isasyncgenfunction(self.steps):
@@ -951,8 +1355,14 @@ class Workflow:
951
1355
  session_id=session.session_id,
952
1356
  user_id=self.user_id,
953
1357
  workflow_run_response=workflow_run_response,
954
- session_state=session_state,
1358
+ run_context=run_context,
955
1359
  store_executor_outputs=self.store_executor_outputs,
1360
+ workflow_session=session,
1361
+ add_workflow_history_to_steps=self.add_workflow_history_to_steps
1362
+ if self.add_workflow_history_to_steps
1363
+ else None,
1364
+ num_history_runs=self.num_history_runs,
1365
+ background_tasks=background_tasks,
956
1366
  )
957
1367
 
958
1368
  # Check for cancellation after step execution
@@ -978,7 +1388,14 @@ class Workflow:
978
1388
 
979
1389
  # Update the workflow_run_response with completion data
980
1390
  if collected_step_outputs:
981
- workflow_run_response.metrics = self._aggregate_workflow_metrics(collected_step_outputs)
1391
+ # Stop the timer for the Run duration
1392
+ if workflow_run_response.metrics:
1393
+ workflow_run_response.metrics.stop_timer()
1394
+
1395
+ workflow_run_response.metrics = self._aggregate_workflow_metrics(
1396
+ collected_step_outputs,
1397
+ workflow_run_response.metrics, # type: ignore[arg-type]
1398
+ )
982
1399
  last_output = cast(StepOutput, collected_step_outputs[-1])
983
1400
 
984
1401
  # Use deepest nested content if this is a container (Steps/Router/Loop/etc.)
@@ -1023,6 +1440,10 @@ class Workflow:
1023
1440
  raise e
1024
1441
 
1025
1442
  finally:
1443
+ # Stop timer on error
1444
+ if workflow_run_response.metrics:
1445
+ workflow_run_response.metrics.stop_timer()
1446
+
1026
1447
  self._update_session_metrics(session=session, workflow_run_response=workflow_run_response)
1027
1448
  session.upsert_run(run=workflow_run_response)
1028
1449
  self.save_session(session=session)
@@ -1040,8 +1461,9 @@ class Workflow:
1040
1461
  session: WorkflowSession,
1041
1462
  execution_input: WorkflowExecutionInput,
1042
1463
  workflow_run_response: WorkflowRunOutput,
1043
- session_state: Optional[Dict[str, Any]] = None,
1044
- stream_intermediate_steps: bool = False,
1464
+ run_context: RunContext,
1465
+ stream_events: bool = False,
1466
+ background_tasks: Optional[Any] = None,
1045
1467
  **kwargs: Any,
1046
1468
  ) -> Iterator[WorkflowRunOutputEvent]:
1047
1469
  """Execute a specific pipeline by name with event streaming"""
@@ -1049,10 +1471,6 @@ class Workflow:
1049
1471
 
1050
1472
  workflow_run_response.status = RunStatus.running
1051
1473
 
1052
- # Register run for cancellation tracking
1053
- if workflow_run_response.run_id:
1054
- register_run(workflow_run_response.run_id)
1055
-
1056
1474
  workflow_started_event = WorkflowStartedEvent(
1057
1475
  run_id=workflow_run_response.run_id or "",
1058
1476
  workflow_name=workflow_run_response.workflow_name,
@@ -1097,11 +1515,22 @@ class Workflow:
1097
1515
 
1098
1516
  early_termination = False
1099
1517
 
1518
+ # Track partial step data in case of cancellation
1519
+ current_step_name = ""
1520
+ current_step = None
1521
+ partial_step_content = ""
1522
+
1100
1523
  for i, step in enumerate(self.steps): # type: ignore[arg-type]
1101
1524
  raise_if_cancelled(workflow_run_response.run_id) # type: ignore
1102
1525
  step_name = getattr(step, "name", f"step_{i + 1}")
1103
1526
  log_debug(f"Streaming step {i + 1}/{self._get_step_count()}: {step_name}")
1104
1527
 
1528
+ # Track current step for cancellation handler
1529
+ current_step_name = step_name
1530
+ current_step = step
1531
+ # Reset partial data for this step
1532
+ partial_step_content = ""
1533
+
1105
1534
  # Create enhanced StepInput
1106
1535
  step_input = self._create_step_input(
1107
1536
  execution_input=execution_input,
@@ -1117,13 +1546,24 @@ class Workflow:
1117
1546
  step_input,
1118
1547
  session_id=session.session_id,
1119
1548
  user_id=self.user_id,
1120
- stream_intermediate_steps=stream_intermediate_steps,
1549
+ stream_events=stream_events,
1550
+ stream_executor_events=self.stream_executor_events,
1121
1551
  workflow_run_response=workflow_run_response,
1122
- session_state=session_state,
1552
+ run_context=run_context,
1123
1553
  step_index=i,
1124
1554
  store_executor_outputs=self.store_executor_outputs,
1555
+ workflow_session=session,
1556
+ add_workflow_history_to_steps=self.add_workflow_history_to_steps
1557
+ if self.add_workflow_history_to_steps
1558
+ else None,
1559
+ num_history_runs=self.num_history_runs,
1560
+ background_tasks=background_tasks,
1125
1561
  ):
1126
1562
  raise_if_cancelled(workflow_run_response.run_id) # type: ignore
1563
+
1564
+ # Accumulate partial data from streaming events
1565
+ partial_step_content = self._accumulate_partial_step_data(event, partial_step_content) # type: ignore
1566
+
1127
1567
  # Handle events
1128
1568
  if isinstance(event, StepOutput):
1129
1569
  step_output = event
@@ -1172,11 +1612,19 @@ class Workflow:
1172
1612
  yield step_output_event
1173
1613
 
1174
1614
  elif isinstance(event, WorkflowRunOutputEvent): # type: ignore
1175
- yield self._handle_event(event, workflow_run_response) # type: ignore
1615
+ # Enrich event with workflow context before yielding
1616
+ enriched_event = self._enrich_event_with_workflow_context(
1617
+ event, workflow_run_response, step_index=i, step=step
1618
+ )
1619
+ yield self._handle_event(enriched_event, workflow_run_response) # type: ignore
1176
1620
 
1177
1621
  else:
1178
- # Yield other internal events
1179
- yield self._handle_event(event, workflow_run_response) # type: ignore
1622
+ # Enrich other events with workflow context before yielding
1623
+ enriched_event = self._enrich_event_with_workflow_context(
1624
+ event, workflow_run_response, step_index=i, step=step
1625
+ )
1626
+ if self.stream_executor_events:
1627
+ yield self._handle_event(enriched_event, workflow_run_response) # type: ignore
1180
1628
 
1181
1629
  # Break out of main step loop if early termination was requested
1182
1630
  if "early_termination" in locals() and early_termination:
@@ -1184,7 +1632,14 @@ class Workflow:
1184
1632
 
1185
1633
  # Update the workflow_run_response with completion data
1186
1634
  if collected_step_outputs:
1187
- workflow_run_response.metrics = self._aggregate_workflow_metrics(collected_step_outputs)
1635
+ # Stop the timer for the Run duration
1636
+ if workflow_run_response.metrics:
1637
+ workflow_run_response.metrics.stop_timer()
1638
+
1639
+ workflow_run_response.metrics = self._aggregate_workflow_metrics(
1640
+ collected_step_outputs,
1641
+ workflow_run_response.metrics, # type: ignore[arg-type]
1642
+ )
1188
1643
  last_output = cast(StepOutput, collected_step_outputs[-1])
1189
1644
 
1190
1645
  # Use deepest nested content if this is a container (Steps/Router/Loop/etc.)
@@ -1230,6 +1685,36 @@ class Workflow:
1230
1685
  logger.info(f"Workflow run {workflow_run_response.run_id} was cancelled during streaming")
1231
1686
  workflow_run_response.status = RunStatus.cancelled
1232
1687
  workflow_run_response.content = str(e)
1688
+
1689
+ # Capture partial progress from the step that was cancelled mid-stream
1690
+ if partial_step_content:
1691
+ logger.info(
1692
+ f"Step with name '{current_step_name}' was cancelled. Setting its partial progress as step output."
1693
+ )
1694
+ partial_step_output = StepOutput(
1695
+ step_name=current_step_name,
1696
+ step_id=getattr(current_step, "step_id", None) if current_step else None,
1697
+ step_type=StepType.STEP,
1698
+ executor_type=getattr(current_step, "executor_type", None) if current_step else None,
1699
+ executor_name=getattr(current_step, "executor_name", None) if current_step else None,
1700
+ content=partial_step_content,
1701
+ success=False,
1702
+ error="Cancelled during execution",
1703
+ )
1704
+ collected_step_outputs.append(partial_step_output)
1705
+
1706
+ # Preserve all progress (completed steps + partial step) before cancellation
1707
+ if collected_step_outputs:
1708
+ workflow_run_response.step_results = collected_step_outputs
1709
+ # Stop the timer for the Run duration
1710
+ if workflow_run_response.metrics:
1711
+ workflow_run_response.metrics.stop_timer()
1712
+
1713
+ workflow_run_response.metrics = self._aggregate_workflow_metrics(
1714
+ collected_step_outputs,
1715
+ workflow_run_response.metrics, # type: ignore[arg-type]
1716
+ )
1717
+
1233
1718
  cancelled_event = WorkflowCancelledEvent(
1234
1719
  run_id=workflow_run_response.run_id or "",
1235
1720
  workflow_id=self.id,
@@ -1270,6 +1755,10 @@ class Workflow:
1270
1755
  )
1271
1756
  yield self._handle_event(workflow_completed_event, workflow_run_response)
1272
1757
 
1758
+ # Stop timer on error
1759
+ if workflow_run_response.metrics:
1760
+ workflow_run_response.metrics.stop_timer()
1761
+
1273
1762
  # Store the completed workflow response
1274
1763
  self._update_session_metrics(session=session, workflow_run_response=workflow_run_response)
1275
1764
  session.upsert_run(run=workflow_run_response)
@@ -1332,21 +1821,46 @@ class Workflow:
1332
1821
  # For regular async functions, use the same signature inspection logic in fallback
1333
1822
  return await func(**call_kwargs) # type: ignore
1334
1823
 
1824
+ async def _aload_or_create_session(
1825
+ self, session_id: str, user_id: Optional[str], session_state: Optional[Dict[str, Any]]
1826
+ ) -> Tuple[WorkflowSession, Dict[str, Any]]:
1827
+ """Load or create session from database, update metadata, and prepare session state.
1828
+
1829
+ Returns:
1830
+ Tuple of (workflow_session, prepared_session_state)
1831
+ """
1832
+ # Read existing session from database
1833
+ if self._has_async_db():
1834
+ workflow_session = await self.aread_or_create_session(session_id=session_id, user_id=user_id)
1835
+ else:
1836
+ workflow_session = self.read_or_create_session(session_id=session_id, user_id=user_id)
1837
+ self._update_metadata(session=workflow_session)
1838
+
1839
+ # Update session state from DB
1840
+ _session_state = session_state if session_state is not None else {}
1841
+ _session_state = self._load_session_state(session=workflow_session, session_state=_session_state)
1842
+
1843
+ return workflow_session, _session_state
1844
+
1335
1845
  async def _aexecute(
1336
1846
  self,
1337
- session: WorkflowSession,
1847
+ session_id: str,
1848
+ user_id: Optional[str],
1338
1849
  execution_input: WorkflowExecutionInput,
1339
1850
  workflow_run_response: WorkflowRunOutput,
1340
- session_state: Optional[Dict[str, Any]] = None,
1851
+ run_context: RunContext,
1852
+ background_tasks: Optional[Any] = None,
1341
1853
  **kwargs: Any,
1342
1854
  ) -> WorkflowRunOutput:
1343
1855
  """Execute a specific pipeline by name asynchronously"""
1344
1856
  from inspect import isasyncgenfunction, iscoroutinefunction, isgeneratorfunction
1345
1857
 
1346
- workflow_run_response.status = RunStatus.running
1858
+ # Read existing session from database
1859
+ workflow_session, run_context.session_state = await self._aload_or_create_session(
1860
+ session_id=session_id, user_id=user_id, session_state=run_context.session_state
1861
+ )
1347
1862
 
1348
- # Register run for cancellation tracking
1349
- register_run(workflow_run_response.run_id) # type: ignore
1863
+ workflow_run_response.status = RunStatus.running
1350
1864
 
1351
1865
  if callable(self.steps):
1352
1866
  # Execute the workflow with the custom executor
@@ -1410,11 +1924,17 @@ class Workflow:
1410
1924
 
1411
1925
  step_output = await step.aexecute( # type: ignore[union-attr]
1412
1926
  step_input,
1413
- session_id=session.session_id,
1927
+ session_id=session_id,
1414
1928
  user_id=self.user_id,
1415
1929
  workflow_run_response=workflow_run_response,
1416
- session_state=session_state,
1930
+ run_context=run_context,
1417
1931
  store_executor_outputs=self.store_executor_outputs,
1932
+ workflow_session=workflow_session,
1933
+ add_workflow_history_to_steps=self.add_workflow_history_to_steps
1934
+ if self.add_workflow_history_to_steps
1935
+ else None,
1936
+ num_history_runs=self.num_history_runs,
1937
+ background_tasks=background_tasks,
1418
1938
  )
1419
1939
 
1420
1940
  # Check for cancellation after step execution
@@ -1440,7 +1960,14 @@ class Workflow:
1440
1960
 
1441
1961
  # Update the workflow_run_response with completion data
1442
1962
  if collected_step_outputs:
1443
- workflow_run_response.metrics = self._aggregate_workflow_metrics(collected_step_outputs)
1963
+ # Stop the timer for the Run duration
1964
+ if workflow_run_response.metrics:
1965
+ workflow_run_response.metrics.stop_timer()
1966
+
1967
+ workflow_run_response.metrics = self._aggregate_workflow_metrics(
1968
+ collected_step_outputs,
1969
+ workflow_run_response.metrics, # type: ignore[arg-type]
1970
+ )
1444
1971
  last_output = cast(StepOutput, collected_step_outputs[-1])
1445
1972
 
1446
1973
  # Use deepest nested content if this is a container (Steps/Router/Loop/etc.)
@@ -1480,31 +2007,45 @@ class Workflow:
1480
2007
  workflow_run_response.content = f"Workflow execution failed: {e}"
1481
2008
  raise e
1482
2009
 
1483
- self._update_session_metrics(session=session, workflow_run_response=workflow_run_response)
1484
- session.upsert_run(run=workflow_run_response)
1485
- self.save_session(session=session)
2010
+ # Stop timer on error
2011
+ if workflow_run_response.metrics:
2012
+ workflow_run_response.metrics.stop_timer()
2013
+
2014
+ self._update_session_metrics(session=workflow_session, workflow_run_response=workflow_run_response)
2015
+ workflow_session.upsert_run(run=workflow_run_response)
2016
+ if self._has_async_db():
2017
+ await self.asave_session(session=workflow_session)
2018
+ else:
2019
+ self.save_session(session=workflow_session)
1486
2020
  # Always clean up the run tracking
1487
2021
  cleanup_run(workflow_run_response.run_id) # type: ignore
1488
2022
 
1489
2023
  # Log Workflow Telemetry
1490
2024
  if self.telemetry:
1491
- await self._alog_workflow_telemetry(session_id=session.session_id, run_id=workflow_run_response.run_id)
2025
+ await self._alog_workflow_telemetry(session_id=session_id, run_id=workflow_run_response.run_id)
1492
2026
 
1493
2027
  return workflow_run_response
1494
2028
 
1495
2029
  async def _aexecute_stream(
1496
2030
  self,
1497
- session: WorkflowSession,
2031
+ session_id: str,
2032
+ user_id: Optional[str],
1498
2033
  execution_input: WorkflowExecutionInput,
1499
2034
  workflow_run_response: WorkflowRunOutput,
1500
- session_state: Optional[Dict[str, Any]] = None,
1501
- stream_intermediate_steps: bool = False,
2035
+ run_context: RunContext,
2036
+ stream_events: bool = False,
1502
2037
  websocket_handler: Optional[WebSocketHandler] = None,
2038
+ background_tasks: Optional[Any] = None,
1503
2039
  **kwargs: Any,
1504
2040
  ) -> AsyncIterator[WorkflowRunOutputEvent]:
1505
2041
  """Execute a specific pipeline by name with event streaming"""
1506
2042
  from inspect import isasyncgenfunction, iscoroutinefunction, isgeneratorfunction
1507
2043
 
2044
+ # Read existing session from database
2045
+ workflow_session, run_context.session_state = await self._aload_or_create_session(
2046
+ session_id=session_id, user_id=user_id, session_state=run_context.session_state
2047
+ )
2048
+
1508
2049
  workflow_run_response.status = RunStatus.running
1509
2050
 
1510
2051
  workflow_started_event = WorkflowStartedEvent(
@@ -1559,12 +2100,22 @@ class Workflow:
1559
2100
 
1560
2101
  early_termination = False
1561
2102
 
2103
+ # Track partial step data in case of cancellation
2104
+ current_step_name = ""
2105
+ current_step = None
2106
+ partial_step_content = ""
2107
+
1562
2108
  for i, step in enumerate(self.steps): # type: ignore[arg-type]
1563
2109
  if workflow_run_response.run_id:
1564
2110
  raise_if_cancelled(workflow_run_response.run_id)
1565
2111
  step_name = getattr(step, "name", f"step_{i + 1}")
1566
2112
  log_debug(f"Async streaming step {i + 1}/{self._get_step_count()}: {step_name}")
1567
2113
 
2114
+ current_step_name = step_name
2115
+ current_step = step
2116
+ # Reset partial data for this step
2117
+ partial_step_content = ""
2118
+
1568
2119
  # Create enhanced StepInput
1569
2120
  step_input = self._create_step_input(
1570
2121
  execution_input=execution_input,
@@ -1578,16 +2129,27 @@ class Workflow:
1578
2129
  # Execute step with streaming and yield all events
1579
2130
  async for event in step.aexecute_stream( # type: ignore[union-attr]
1580
2131
  step_input,
1581
- session_id=session.session_id,
2132
+ session_id=session_id,
1582
2133
  user_id=self.user_id,
1583
- stream_intermediate_steps=stream_intermediate_steps,
2134
+ stream_events=stream_events,
2135
+ stream_executor_events=self.stream_executor_events,
1584
2136
  workflow_run_response=workflow_run_response,
1585
- session_state=session_state,
2137
+ run_context=run_context,
1586
2138
  step_index=i,
1587
2139
  store_executor_outputs=self.store_executor_outputs,
2140
+ workflow_session=workflow_session,
2141
+ add_workflow_history_to_steps=self.add_workflow_history_to_steps
2142
+ if self.add_workflow_history_to_steps
2143
+ else None,
2144
+ num_history_runs=self.num_history_runs,
2145
+ background_tasks=background_tasks,
1588
2146
  ):
1589
2147
  if workflow_run_response.run_id:
1590
2148
  raise_if_cancelled(workflow_run_response.run_id)
2149
+
2150
+ # Accumulate partial data from streaming events
2151
+ partial_step_content = self._accumulate_partial_step_data(event, partial_step_content) # type: ignore
2152
+
1591
2153
  if isinstance(event, StepOutput):
1592
2154
  step_output = event
1593
2155
  collected_step_outputs.append(step_output)
@@ -1634,11 +2196,23 @@ class Workflow:
1634
2196
  yield step_output_event
1635
2197
 
1636
2198
  elif isinstance(event, WorkflowRunOutputEvent): # type: ignore
1637
- yield self._handle_event(event, workflow_run_response, websocket_handler=websocket_handler) # type: ignore
2199
+ # Enrich event with workflow context before yielding
2200
+ enriched_event = self._enrich_event_with_workflow_context(
2201
+ event, workflow_run_response, step_index=i, step=step
2202
+ )
2203
+ yield self._handle_event(
2204
+ enriched_event, workflow_run_response, websocket_handler=websocket_handler
2205
+ ) # type: ignore
1638
2206
 
1639
2207
  else:
1640
- # Yield other internal events
1641
- yield self._handle_event(event, workflow_run_response, websocket_handler=websocket_handler) # type: ignore
2208
+ # Enrich other events with workflow context before yielding
2209
+ enriched_event = self._enrich_event_with_workflow_context(
2210
+ event, workflow_run_response, step_index=i, step=step
2211
+ )
2212
+ if self.stream_executor_events:
2213
+ yield self._handle_event(
2214
+ enriched_event, workflow_run_response, websocket_handler=websocket_handler
2215
+ ) # type: ignore
1642
2216
 
1643
2217
  # Break out of main step loop if early termination was requested
1644
2218
  if "early_termination" in locals() and early_termination:
@@ -1646,7 +2220,14 @@ class Workflow:
1646
2220
 
1647
2221
  # Update the workflow_run_response with completion data
1648
2222
  if collected_step_outputs:
1649
- workflow_run_response.metrics = self._aggregate_workflow_metrics(collected_step_outputs)
2223
+ # Stop the timer for the Run duration
2224
+ if workflow_run_response.metrics:
2225
+ workflow_run_response.metrics.stop_timer()
2226
+
2227
+ workflow_run_response.metrics = self._aggregate_workflow_metrics(
2228
+ collected_step_outputs,
2229
+ workflow_run_response.metrics, # type: ignore[arg-type]
2230
+ )
1650
2231
  last_output = cast(StepOutput, collected_step_outputs[-1])
1651
2232
 
1652
2233
  # Use deepest nested content if this is a container (Steps/Router/Loop/etc.)
@@ -1678,7 +2259,7 @@ class Workflow:
1678
2259
  run_id=workflow_run_response.run_id or "",
1679
2260
  workflow_id=self.id,
1680
2261
  workflow_name=self.name,
1681
- session_id=session.session_id,
2262
+ session_id=session_id,
1682
2263
  error=str(e),
1683
2264
  )
1684
2265
 
@@ -1692,11 +2273,41 @@ class Workflow:
1692
2273
  logger.info(f"Workflow run {workflow_run_response.run_id} was cancelled during streaming")
1693
2274
  workflow_run_response.status = RunStatus.cancelled
1694
2275
  workflow_run_response.content = str(e)
2276
+
2277
+ # Capture partial progress from the step that was cancelled mid-stream
2278
+ if partial_step_content:
2279
+ logger.info(
2280
+ f"Step with name '{current_step_name}' was cancelled. Setting its partial progress as step output."
2281
+ )
2282
+ partial_step_output = StepOutput(
2283
+ step_name=current_step_name,
2284
+ step_id=getattr(current_step, "step_id", None) if current_step else None,
2285
+ step_type=StepType.STEP,
2286
+ executor_type=getattr(current_step, "executor_type", None) if current_step else None,
2287
+ executor_name=getattr(current_step, "executor_name", None) if current_step else None,
2288
+ content=partial_step_content,
2289
+ success=False,
2290
+ error="Cancelled during execution",
2291
+ )
2292
+ collected_step_outputs.append(partial_step_output)
2293
+
2294
+ # Preserve all progress (completed steps + partial step) before cancellation
2295
+ if collected_step_outputs:
2296
+ workflow_run_response.step_results = collected_step_outputs
2297
+ # Stop the timer for the Run duration
2298
+ if workflow_run_response.metrics:
2299
+ workflow_run_response.metrics.stop_timer()
2300
+
2301
+ workflow_run_response.metrics = self._aggregate_workflow_metrics(
2302
+ collected_step_outputs,
2303
+ workflow_run_response.metrics, # type: ignore[arg-type]
2304
+ )
2305
+
1695
2306
  cancelled_event = WorkflowCancelledEvent(
1696
2307
  run_id=workflow_run_response.run_id or "",
1697
2308
  workflow_id=self.id,
1698
2309
  workflow_name=self.name,
1699
- session_id=session.session_id,
2310
+ session_id=session_id,
1700
2311
  reason=str(e),
1701
2312
  )
1702
2313
  yield self._handle_event(
@@ -1713,7 +2324,7 @@ class Workflow:
1713
2324
  run_id=workflow_run_response.run_id or "",
1714
2325
  workflow_id=self.id,
1715
2326
  workflow_name=self.name,
1716
- session_id=session.session_id,
2327
+ session_id=session_id,
1717
2328
  error=str(e),
1718
2329
  )
1719
2330
 
@@ -1736,14 +2347,21 @@ class Workflow:
1736
2347
  )
1737
2348
  yield self._handle_event(workflow_completed_event, workflow_run_response, websocket_handler=websocket_handler)
1738
2349
 
2350
+ # Stop timer on error
2351
+ if workflow_run_response.metrics:
2352
+ workflow_run_response.metrics.stop_timer()
2353
+
1739
2354
  # Store the completed workflow response
1740
- self._update_session_metrics(session=session, workflow_run_response=workflow_run_response)
1741
- session.upsert_run(run=workflow_run_response)
1742
- self.save_session(session=session)
2355
+ self._update_session_metrics(session=workflow_session, workflow_run_response=workflow_run_response)
2356
+ workflow_session.upsert_run(run=workflow_run_response)
2357
+ if self._has_async_db():
2358
+ await self.asave_session(session=workflow_session)
2359
+ else:
2360
+ self.save_session(session=workflow_session)
1743
2361
 
1744
2362
  # Log Workflow Telemetry
1745
2363
  if self.telemetry:
1746
- await self._alog_workflow_telemetry(session_id=session.session_id, run_id=workflow_run_response.run_id)
2364
+ await self._alog_workflow_telemetry(session_id=session_id, run_id=workflow_run_response.run_id)
1747
2365
 
1748
2366
  # Always clean up the run tracking
1749
2367
  cleanup_run(workflow_run_response.run_id) # type: ignore
@@ -1767,16 +2385,19 @@ class Workflow:
1767
2385
 
1768
2386
  self.initialize_workflow()
1769
2387
 
1770
- session_id, user_id, session_state = self._initialize_session(
1771
- session_id=session_id, user_id=user_id, session_state=session_state, run_id=run_id
1772
- )
2388
+ session_id, user_id = self._initialize_session(session_id=session_id, user_id=user_id)
1773
2389
 
1774
2390
  # Read existing session from database
1775
- workflow_session = self.read_or_create_session(session_id=session_id, user_id=user_id)
1776
- self._update_metadata(session=workflow_session)
2391
+ workflow_session, session_state = await self._aload_or_create_session(
2392
+ session_id=session_id, user_id=user_id, session_state=session_state
2393
+ )
1777
2394
 
1778
- # Update session state from DB
1779
- session_state = self._load_session_state(session=workflow_session, session_state=session_state)
2395
+ run_context = RunContext(
2396
+ run_id=run_id,
2397
+ session_id=session_id,
2398
+ user_id=user_id,
2399
+ session_state=session_state,
2400
+ )
1780
2401
 
1781
2402
  self._prepare_steps()
1782
2403
 
@@ -1791,9 +2412,16 @@ class Workflow:
1791
2412
  status=RunStatus.pending,
1792
2413
  )
1793
2414
 
2415
+ # Start the run metrics timer
2416
+ workflow_run_response.metrics = WorkflowMetrics(steps={})
2417
+ workflow_run_response.metrics.start_timer()
2418
+
1794
2419
  # Store PENDING response immediately
1795
2420
  workflow_session.upsert_run(run=workflow_run_response)
1796
- self.save_session(session=workflow_session)
2421
+ if self._has_async_db():
2422
+ await self.asave_session(session=workflow_session)
2423
+ else:
2424
+ self.save_session(session=workflow_session)
1797
2425
 
1798
2426
  # Prepare execution input
1799
2427
  inputs = WorkflowExecutionInput(
@@ -1812,15 +2440,29 @@ class Workflow:
1812
2440
  try:
1813
2441
  # Update status to RUNNING and save
1814
2442
  workflow_run_response.status = RunStatus.running
1815
- self.save_session(session=workflow_session)
1816
-
1817
- await self._aexecute(
1818
- session=workflow_session,
1819
- execution_input=inputs,
1820
- workflow_run_response=workflow_run_response,
1821
- session_state=session_state,
1822
- **kwargs,
1823
- )
2443
+ if self._has_async_db():
2444
+ await self.asave_session(session=workflow_session)
2445
+ else:
2446
+ self.save_session(session=workflow_session)
2447
+
2448
+ if self.agent is not None:
2449
+ self._aexecute_workflow_agent(
2450
+ user_input=input, # type: ignore
2451
+ execution_input=inputs,
2452
+ run_context=run_context,
2453
+ stream=False,
2454
+ **kwargs,
2455
+ )
2456
+ else:
2457
+ await self._aexecute(
2458
+ session_id=session_id,
2459
+ user_id=user_id,
2460
+ execution_input=inputs,
2461
+ workflow_run_response=workflow_run_response,
2462
+ run_context=run_context,
2463
+ session_state=session_state,
2464
+ **kwargs,
2465
+ )
1824
2466
 
1825
2467
  log_debug(f"Background execution completed with status: {workflow_run_response.status}")
1826
2468
 
@@ -1828,7 +2470,10 @@ class Workflow:
1828
2470
  logger.error(f"Background workflow execution failed: {e}")
1829
2471
  workflow_run_response.status = RunStatus.error
1830
2472
  workflow_run_response.content = f"Background execution failed: {str(e)}"
1831
- self.save_session(session=workflow_session)
2473
+ if self._has_async_db():
2474
+ await self.asave_session(session=workflow_session)
2475
+ else:
2476
+ self.save_session(session=workflow_session)
1832
2477
 
1833
2478
  # Create and start asyncio task
1834
2479
  loop = asyncio.get_running_loop()
@@ -1848,7 +2493,7 @@ class Workflow:
1848
2493
  images: Optional[List[Image]] = None,
1849
2494
  videos: Optional[List[Video]] = None,
1850
2495
  files: Optional[List[File]] = None,
1851
- stream_intermediate_steps: bool = False,
2496
+ stream_events: bool = False,
1852
2497
  websocket_handler: Optional[WebSocketHandler] = None,
1853
2498
  **kwargs: Any,
1854
2499
  ) -> WorkflowRunOutput:
@@ -1858,93 +2503,936 @@ class Workflow:
1858
2503
 
1859
2504
  self.initialize_workflow()
1860
2505
 
1861
- session_id, user_id, session_state = self._initialize_session(
1862
- session_id=session_id, user_id=user_id, session_state=session_state, run_id=run_id
1863
- )
2506
+ session_id, user_id = self._initialize_session(session_id=session_id, user_id=user_id)
1864
2507
 
1865
2508
  # Read existing session from database
1866
- workflow_session = self.read_or_create_session(session_id=session_id, user_id=user_id)
1867
- self._update_metadata(session=workflow_session)
2509
+ workflow_session, session_state = await self._aload_or_create_session(
2510
+ session_id=session_id, user_id=user_id, session_state=session_state
2511
+ )
1868
2512
 
1869
- # Update session state from DB
1870
- session_state = self._load_session_state(session=workflow_session, session_state=session_state)
2513
+ run_context = RunContext(
2514
+ run_id=run_id,
2515
+ session_id=session_id,
2516
+ user_id=user_id,
2517
+ session_state=session_state,
2518
+ )
2519
+
2520
+ self._prepare_steps()
2521
+
2522
+ # Create workflow run response with PENDING status
2523
+ workflow_run_response = WorkflowRunOutput(
2524
+ run_id=run_id,
2525
+ input=input,
2526
+ session_id=session_id,
2527
+ workflow_id=self.id,
2528
+ workflow_name=self.name,
2529
+ created_at=int(datetime.now().timestamp()),
2530
+ status=RunStatus.pending,
2531
+ )
2532
+
2533
+ # Start the run metrics timer
2534
+ workflow_run_response.metrics = WorkflowMetrics(steps={})
2535
+ workflow_run_response.metrics.start_timer()
2536
+
2537
+ # Prepare execution input
2538
+ inputs = WorkflowExecutionInput(
2539
+ input=input,
2540
+ additional_data=additional_data,
2541
+ audio=audio, # type: ignore
2542
+ images=images, # type: ignore
2543
+ videos=videos, # type: ignore
2544
+ files=files, # type: ignore
2545
+ )
2546
+
2547
+ self.update_agents_and_teams_session_info()
2548
+
2549
+ async def execute_workflow_background_stream():
2550
+ """Background execution with streaming and WebSocket broadcasting"""
2551
+ try:
2552
+ if self.agent is not None:
2553
+ result = self._aexecute_workflow_agent(
2554
+ user_input=input, # type: ignore
2555
+ run_context=run_context,
2556
+ execution_input=inputs,
2557
+ stream=True,
2558
+ websocket_handler=websocket_handler,
2559
+ **kwargs,
2560
+ )
2561
+ # For streaming, result is an async iterator
2562
+ async for event in result: # type: ignore
2563
+ # Events are automatically broadcast by _handle_event in the agent execution
2564
+ # We just consume them here to drive the execution
2565
+ pass
2566
+ log_debug(
2567
+ f"Background streaming execution (workflow agent) completed with status: {workflow_run_response.status}"
2568
+ )
2569
+ else:
2570
+ # Update status to RUNNING and save
2571
+ workflow_run_response.status = RunStatus.running
2572
+
2573
+ workflow_session.upsert_run(run=workflow_run_response)
2574
+ if self._has_async_db():
2575
+ await self.asave_session(session=workflow_session)
2576
+ else:
2577
+ self.save_session(session=workflow_session)
2578
+
2579
+ # Execute with streaming - consume all events (they're auto-broadcast via _handle_event)
2580
+ async for event in self._aexecute_stream(
2581
+ session_id=session_id,
2582
+ user_id=user_id,
2583
+ execution_input=inputs,
2584
+ workflow_run_response=workflow_run_response,
2585
+ stream_events=stream_events,
2586
+ run_context=run_context,
2587
+ websocket_handler=websocket_handler,
2588
+ **kwargs,
2589
+ ):
2590
+ # Events are automatically broadcast by _handle_event
2591
+ # We just consume them here to drive the execution
2592
+ pass
2593
+
2594
+ log_debug(f"Background streaming execution completed with status: {workflow_run_response.status}")
2595
+
2596
+ except Exception as e:
2597
+ logger.error(f"Background streaming workflow execution failed: {e}")
2598
+ workflow_run_response.status = RunStatus.error
2599
+ workflow_run_response.content = f"Background streaming execution failed: {str(e)}"
2600
+ if self._has_async_db():
2601
+ await self.asave_session(session=workflow_session)
2602
+ else:
2603
+ self.save_session(session=workflow_session)
2604
+
2605
+ # Create and start asyncio task for background streaming execution
2606
+ loop = asyncio.get_running_loop()
2607
+ loop.create_task(execute_workflow_background_stream())
2608
+
2609
+ # Return SAME object that will be updated by background execution
2610
+ return workflow_run_response
2611
+
2612
+ async def aget_run(self, run_id: str, session_id: Optional[str] = None) -> Optional[WorkflowRunOutput]:
2613
+ """Get the status and details of a background workflow run - SIMPLIFIED"""
2614
+ # Use provided session_id or fall back to self.session_id
2615
+ _session_id = session_id if session_id is not None else self.session_id
2616
+
2617
+ if self.db is not None and _session_id is not None:
2618
+ session = await self.db.aget_session(session_id=_session_id, session_type=SessionType.WORKFLOW) # type: ignore
2619
+ if session and isinstance(session, WorkflowSession) and session.runs:
2620
+ # Find the run by ID
2621
+ for run in session.runs:
2622
+ if run.run_id == run_id:
2623
+ return run
2624
+
2625
+ return None
2626
+
2627
+ def get_run(self, run_id: str, session_id: Optional[str] = None) -> Optional[WorkflowRunOutput]:
2628
+ """Get the status and details of a background workflow run - SIMPLIFIED"""
2629
+ # Use provided session_id or fall back to self.session_id
2630
+ _session_id = session_id if session_id is not None else self.session_id
2631
+
2632
+ if self.db is not None and _session_id is not None:
2633
+ session = self.db.get_session(session_id=_session_id, session_type=SessionType.WORKFLOW)
2634
+ if session and isinstance(session, WorkflowSession) and session.runs:
2635
+ # Find the run by ID
2636
+ for run in session.runs:
2637
+ if run.run_id == run_id:
2638
+ return run
2639
+
2640
+ return None
2641
+
2642
+ def _initialize_workflow_agent(
2643
+ self,
2644
+ session: WorkflowSession,
2645
+ execution_input: WorkflowExecutionInput,
2646
+ run_context: RunContext,
2647
+ stream: bool = False,
2648
+ ) -> None:
2649
+ """Initialize the workflow agent with tools (but NOT context - that's passed per-run)"""
2650
+ from agno.tools.function import Function
2651
+
2652
+ workflow_tool_func = self.agent.create_workflow_tool( # type: ignore
2653
+ workflow=self,
2654
+ session=session,
2655
+ execution_input=execution_input,
2656
+ run_context=run_context,
2657
+ stream=stream,
2658
+ )
2659
+ workflow_tool = Function.from_callable(workflow_tool_func)
2660
+
2661
+ self.agent.tools = [workflow_tool] # type: ignore
2662
+ self.agent._rebuild_tools = True # type: ignore
2663
+
2664
+ log_debug("Workflow agent initialized with run_workflow tool")
2665
+
2666
+ def _get_workflow_agent_dependencies(self, session: WorkflowSession) -> Dict[str, Any]:
2667
+ """Build dependencies dict with workflow context to pass to agent.run()"""
2668
+ # Get configuration from the WorkflowAgent instance
2669
+ add_history = True
2670
+ num_runs = 5
2671
+
2672
+ if self.agent and isinstance(self.agent, WorkflowAgent):
2673
+ add_history = self.agent.add_workflow_history
2674
+ num_runs = self.agent.num_history_runs or 5
2675
+
2676
+ if add_history:
2677
+ history_context = (
2678
+ session.get_workflow_history_context(num_runs=num_runs) or "No previous workflow runs in this session."
2679
+ )
2680
+ else:
2681
+ history_context = "No workflow history available."
2682
+
2683
+ # Build workflow context with description and history
2684
+ workflow_context = ""
2685
+ if self.description:
2686
+ workflow_context += f"Workflow Description: {self.description}\n\n"
2687
+
2688
+ workflow_context += history_context
2689
+
2690
+ return {
2691
+ "workflow_context": workflow_context,
2692
+ }
2693
+
2694
+ def _execute_workflow_agent(
2695
+ self,
2696
+ user_input: Union[str, Dict[str, Any], List[Any], BaseModel],
2697
+ session: WorkflowSession,
2698
+ execution_input: WorkflowExecutionInput,
2699
+ run_context: RunContext,
2700
+ stream: bool = False,
2701
+ **kwargs: Any,
2702
+ ) -> Union[WorkflowRunOutput, Iterator[WorkflowRunOutputEvent]]:
2703
+ """
2704
+ Execute the workflow agent in streaming or non-streaming mode.
2705
+
2706
+ The agent decides whether to run the workflow or answer directly from history.
2707
+
2708
+ Args:
2709
+ user_input: The user's input
2710
+ session: The workflow session
2711
+ execution_input: The execution input
2712
+ run_context: The run context
2713
+ stream: Whether to stream the response
2714
+ stream_intermediate_steps: Whether to stream intermediate steps
2715
+
2716
+ Returns:
2717
+ WorkflowRunOutput if stream=False, Iterator[WorkflowRunOutputEvent] if stream=True
2718
+ """
2719
+ if stream:
2720
+ return self._run_workflow_agent_stream(
2721
+ agent_input=user_input,
2722
+ session=session,
2723
+ execution_input=execution_input,
2724
+ run_context=run_context,
2725
+ stream=stream,
2726
+ **kwargs,
2727
+ )
2728
+ else:
2729
+ return self._run_workflow_agent(
2730
+ agent_input=user_input,
2731
+ session=session,
2732
+ execution_input=execution_input,
2733
+ run_context=run_context,
2734
+ stream=stream,
2735
+ )
2736
+
2737
+ def _run_workflow_agent_stream(
2738
+ self,
2739
+ agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
2740
+ session: WorkflowSession,
2741
+ execution_input: WorkflowExecutionInput,
2742
+ run_context: RunContext,
2743
+ stream: bool = False,
2744
+ **kwargs: Any,
2745
+ ) -> Iterator[WorkflowRunOutputEvent]:
2746
+ """
2747
+ Execute the workflow agent in streaming mode.
2748
+
2749
+ The agent's tool (run_workflow) is a generator that yields workflow events directly.
2750
+ These events bubble up through the agent's streaming and are yielded here.
2751
+ We filter to only yield WorkflowRunOutputEvent to the CLI.
2752
+
2753
+ Yields:
2754
+ WorkflowRunOutputEvent: Events from workflow execution (agent events are filtered)
2755
+ """
2756
+ from typing import get_args
2757
+
2758
+ from agno.run.workflow import WorkflowCompletedEvent, WorkflowRunOutputEvent
2759
+
2760
+ # Initialize agent with stream_intermediate_steps=True so tool yields events
2761
+ self._initialize_workflow_agent(session, execution_input, run_context=run_context, stream=stream)
2762
+
2763
+ # Build dependencies with workflow context
2764
+ run_context.dependencies = self._get_workflow_agent_dependencies(session)
2765
+
2766
+ # Run agent with streaming - workflow events will bubble up from the tool
2767
+ agent_response: Optional[RunOutput] = None
2768
+ workflow_executed = False
2769
+
2770
+ from agno.run.agent import RunContentEvent
2771
+ from agno.run.team import RunContentEvent as TeamRunContentEvent
2772
+ from agno.run.workflow import WorkflowAgentCompletedEvent, WorkflowAgentStartedEvent
2773
+
2774
+ log_debug(f"Executing workflow agent with streaming - input: {agent_input}...")
2775
+
2776
+ # Create a workflow run response upfront for potential direct answer (will be used only if workflow is not executed)
2777
+ run_id = str(uuid4())
2778
+ direct_reply_run_response = WorkflowRunOutput(
2779
+ run_id=run_id,
2780
+ input=execution_input.input,
2781
+ session_id=session.session_id,
2782
+ workflow_id=self.id,
2783
+ workflow_name=self.name,
2784
+ created_at=int(datetime.now().timestamp()),
2785
+ )
2786
+
2787
+ # Yield WorkflowAgentStartedEvent at the beginning (stored in direct_reply_run_response)
2788
+ agent_started_event = WorkflowAgentStartedEvent(
2789
+ workflow_name=self.name,
2790
+ workflow_id=self.id,
2791
+ session_id=session.session_id,
2792
+ )
2793
+ yield agent_started_event
2794
+
2795
+ # Run the agent in streaming mode and yield all events
2796
+ for event in self.agent.run( # type: ignore[union-attr]
2797
+ input=agent_input,
2798
+ stream=True,
2799
+ stream_intermediate_steps=True,
2800
+ yield_run_response=True,
2801
+ session_id=session.session_id,
2802
+ dependencies=run_context.dependencies, # Pass context dynamically per-run
2803
+ session_state=run_context.session_state, # Pass session state dynamically per-run
2804
+ ): # type: ignore
2805
+ if isinstance(event, tuple(get_args(WorkflowRunOutputEvent))):
2806
+ yield event # type: ignore[misc]
2807
+
2808
+ # Track if workflow was executed by checking for WorkflowCompletedEvent
2809
+ if isinstance(event, WorkflowCompletedEvent):
2810
+ workflow_executed = True
2811
+ elif isinstance(event, (RunContentEvent, TeamRunContentEvent)):
2812
+ if event.step_name is None:
2813
+ # This is from the workflow agent itself
2814
+ # Enrich with metadata to mark it as a workflow agent event
2815
+
2816
+ if workflow_executed:
2817
+ continue # Skip if workflow was already executed
2818
+
2819
+ # workflow_agent field is used by consumers of the events to distinguish between workflow agent and regular agent
2820
+ event.workflow_agent = True # type: ignore
2821
+ yield event # type: ignore[misc]
2822
+
2823
+ # Capture the final RunOutput (but don't yield it)
2824
+ if isinstance(event, RunOutput):
2825
+ agent_response = event
2826
+
2827
+ # Handle direct answer case (no workflow execution)
2828
+ if not workflow_executed:
2829
+ # Update the pre-created workflow run response with the direct answer
2830
+ direct_reply_run_response.content = agent_response.content if agent_response else ""
2831
+ direct_reply_run_response.status = RunStatus.completed
2832
+ direct_reply_run_response.workflow_agent_run = agent_response
2833
+
2834
+ workflow_run_response = direct_reply_run_response
2835
+
2836
+ # Store the full agent RunOutput and establish parent-child relationship
2837
+ if agent_response:
2838
+ agent_response.parent_run_id = workflow_run_response.run_id
2839
+ agent_response.workflow_id = workflow_run_response.workflow_id
2840
+
2841
+ log_debug(f"Agent decision: workflow_executed={workflow_executed}")
2842
+
2843
+ # Yield WorkflowAgentCompletedEvent (user internally by print_response_stream)
2844
+ agent_completed_event = WorkflowAgentCompletedEvent(
2845
+ run_id=agent_response.run_id if agent_response else None,
2846
+ workflow_name=self.name,
2847
+ workflow_id=self.id,
2848
+ session_id=session.session_id,
2849
+ content=workflow_run_response.content,
2850
+ )
2851
+ yield agent_completed_event
2852
+
2853
+ # Yield a workflow completed event with the agent's direct response
2854
+ completed_event = WorkflowCompletedEvent(
2855
+ run_id=workflow_run_response.run_id or "",
2856
+ content=workflow_run_response.content,
2857
+ workflow_name=workflow_run_response.workflow_name,
2858
+ workflow_id=workflow_run_response.workflow_id,
2859
+ session_id=workflow_run_response.session_id,
2860
+ step_results=[],
2861
+ metadata={"agent_direct_response": True},
2862
+ )
2863
+ yield completed_event
2864
+
2865
+ # Update the run in session
2866
+ session.upsert_run(run=workflow_run_response)
2867
+ # Save session
2868
+ self.save_session(session=session)
2869
+
2870
+ else:
2871
+ # Workflow was executed by the tool
2872
+ reloaded_session = self.get_session(session_id=session.session_id)
2873
+
2874
+ if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
2875
+ # Get the last run (which is the one just created by the tool)
2876
+ last_run = reloaded_session.runs[-1]
2877
+
2878
+ # Yield WorkflowAgentCompletedEvent
2879
+ agent_completed_event = WorkflowAgentCompletedEvent(
2880
+ run_id=agent_response.run_id if agent_response else None,
2881
+ workflow_name=self.name,
2882
+ workflow_id=self.id,
2883
+ session_id=session.session_id,
2884
+ content=agent_response.content if agent_response else None,
2885
+ )
2886
+ yield agent_completed_event
2887
+
2888
+ # Update the last run with workflow_agent_run
2889
+ last_run.workflow_agent_run = agent_response
2890
+
2891
+ # Store the full agent RunOutput and establish parent-child relationship
2892
+ if agent_response:
2893
+ agent_response.parent_run_id = last_run.run_id
2894
+ agent_response.workflow_id = last_run.workflow_id
2895
+
2896
+ # Save the reloaded session (which has the updated run)
2897
+ self.save_session(session=reloaded_session)
2898
+
2899
+ else:
2900
+ log_warning("Could not reload session or no runs found after workflow execution")
2901
+
2902
+ def _run_workflow_agent(
2903
+ self,
2904
+ agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
2905
+ session: WorkflowSession,
2906
+ execution_input: WorkflowExecutionInput,
2907
+ run_context: RunContext,
2908
+ stream: bool = False,
2909
+ ) -> WorkflowRunOutput:
2910
+ """
2911
+ Execute the workflow agent in non-streaming mode.
2912
+
2913
+ The agent decides whether to run the workflow or answer directly from history.
2914
+
2915
+ Returns:
2916
+ WorkflowRunOutput: The workflow run output with agent response
2917
+ """
2918
+
2919
+ # Initialize the agent
2920
+ self._initialize_workflow_agent(session, execution_input, run_context=run_context, stream=stream)
2921
+
2922
+ # Build dependencies with workflow context
2923
+ run_context.dependencies = self._get_workflow_agent_dependencies(session)
2924
+
2925
+ # Run the agent
2926
+ agent_response: RunOutput = self.agent.run( # type: ignore[union-attr]
2927
+ input=agent_input,
2928
+ session_id=session.session_id,
2929
+ dependencies=run_context.dependencies,
2930
+ session_state=run_context.session_state,
2931
+ stream=stream,
2932
+ ) # type: ignore
2933
+
2934
+ # Check if the agent called the workflow tool
2935
+ workflow_executed = False
2936
+ if agent_response.messages:
2937
+ for message in agent_response.messages:
2938
+ if message.role == "assistant" and message.tool_calls:
2939
+ # Check if the tool call is specifically for run_workflow
2940
+ for tool_call in message.tool_calls:
2941
+ # Handle both dict and object formats
2942
+ if isinstance(tool_call, dict):
2943
+ tool_name = tool_call.get("function", {}).get("name", "")
2944
+ else:
2945
+ tool_name = tool_call.function.name if hasattr(tool_call, "function") else ""
2946
+
2947
+ if tool_name == "run_workflow":
2948
+ workflow_executed = True
2949
+ break
2950
+ if workflow_executed:
2951
+ break
2952
+
2953
+ log_debug(f"Workflow agent execution complete. Workflow executed: {workflow_executed}")
2954
+
2955
+ # Handle direct answer case (no workflow execution)
2956
+ if not workflow_executed:
2957
+ # Create a new workflow run output for the direct answer
2958
+ run_id = str(uuid4())
2959
+ workflow_run_response = WorkflowRunOutput(
2960
+ run_id=run_id,
2961
+ input=execution_input.input,
2962
+ session_id=session.session_id,
2963
+ workflow_id=self.id,
2964
+ workflow_name=self.name,
2965
+ created_at=int(datetime.now().timestamp()),
2966
+ content=agent_response.content,
2967
+ status=RunStatus.completed,
2968
+ workflow_agent_run=agent_response,
2969
+ )
2970
+
2971
+ # Store the full agent RunOutput and establish parent-child relationship
2972
+ if agent_response:
2973
+ agent_response.parent_run_id = workflow_run_response.run_id
2974
+ agent_response.workflow_id = workflow_run_response.workflow_id
2975
+
2976
+ # Update the run in session
2977
+ session.upsert_run(run=workflow_run_response)
2978
+ self.save_session(session=session)
2979
+
2980
+ log_debug(f"Agent decision: workflow_executed={workflow_executed}")
2981
+
2982
+ return workflow_run_response
2983
+ else:
2984
+ # Workflow was executed by the tool
2985
+ reloaded_session = self.get_session(session_id=session.session_id)
2986
+
2987
+ if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
2988
+ # Get the last run (which is the one just created by the tool)
2989
+ last_run = reloaded_session.runs[-1]
2990
+
2991
+ # Update the last run directly with workflow_agent_run
2992
+ last_run.workflow_agent_run = agent_response
2993
+
2994
+ # Store the full agent RunOutput and establish parent-child relationship
2995
+ if agent_response:
2996
+ agent_response.parent_run_id = last_run.run_id
2997
+ agent_response.workflow_id = last_run.workflow_id
2998
+
2999
+ # Save the reloaded session (which has the updated run)
3000
+ self.save_session(session=reloaded_session)
3001
+
3002
+ # Return the last run directly (WRO2 from inner workflow)
3003
+ return last_run
3004
+ else:
3005
+ log_warning("Could not reload session or no runs found after workflow execution")
3006
+ # Return a placeholder error response
3007
+ return WorkflowRunOutput(
3008
+ run_id=str(uuid4()),
3009
+ input=execution_input.input,
3010
+ session_id=session.session_id,
3011
+ workflow_id=self.id,
3012
+ workflow_name=self.name,
3013
+ created_at=int(datetime.now().timestamp()),
3014
+ content="Error: Workflow execution failed",
3015
+ status=RunStatus.error,
3016
+ )
3017
+
3018
+ def _async_initialize_workflow_agent(
3019
+ self,
3020
+ session: WorkflowSession,
3021
+ execution_input: WorkflowExecutionInput,
3022
+ run_context: RunContext,
3023
+ websocket_handler: Optional[WebSocketHandler] = None,
3024
+ stream: bool = False,
3025
+ ) -> None:
3026
+ """Initialize the workflow agent with async tools (but NOT context - that's passed per-run)"""
3027
+ from agno.tools.function import Function
3028
+
3029
+ workflow_tool_func = self.agent.async_create_workflow_tool( # type: ignore
3030
+ workflow=self,
3031
+ session=session,
3032
+ execution_input=execution_input,
3033
+ run_context=run_context,
3034
+ stream=stream,
3035
+ websocket_handler=websocket_handler,
3036
+ )
3037
+ workflow_tool = Function.from_callable(workflow_tool_func)
3038
+
3039
+ self.agent.tools = [workflow_tool] # type: ignore
3040
+ self.agent._rebuild_tools = True # type: ignore
3041
+
3042
+ log_debug("Workflow agent initialized with async run_workflow tool")
3043
+
3044
+ async def _aload_session_for_workflow_agent(
3045
+ self,
3046
+ session_id: str,
3047
+ user_id: Optional[str],
3048
+ session_state: Optional[Dict[str, Any]],
3049
+ ) -> Tuple[WorkflowSession, Dict[str, Any]]:
3050
+ """Helper to load or create session for workflow agent execution"""
3051
+ return await self._aload_or_create_session(session_id=session_id, user_id=user_id, session_state=session_state)
3052
+
3053
+ def _aexecute_workflow_agent(
3054
+ self,
3055
+ user_input: Union[str, Dict[str, Any], List[Any], BaseModel],
3056
+ run_context: RunContext,
3057
+ execution_input: WorkflowExecutionInput,
3058
+ stream: bool = False,
3059
+ websocket_handler: Optional[WebSocketHandler] = None,
3060
+ **kwargs: Any,
3061
+ ):
3062
+ """
3063
+ Execute the workflow agent asynchronously in streaming or non-streaming mode.
3064
+
3065
+ The agent decides whether to run the workflow or answer directly from history.
3066
+
3067
+ Args:
3068
+ user_input: The user's input
3069
+ session: The workflow session
3070
+ run_context: The run context
3071
+ execution_input: The execution input
3072
+ stream: Whether to stream the response
3073
+ websocket_handler: The WebSocket handler
3074
+
3075
+ Returns:
3076
+ Coroutine[WorkflowRunOutput] if stream=False, AsyncIterator[WorkflowRunOutputEvent] if stream=True
3077
+ """
3078
+
3079
+ if stream:
3080
+
3081
+ async def _stream():
3082
+ session, session_state_loaded = await self._aload_session_for_workflow_agent(
3083
+ run_context.session_id, run_context.user_id, run_context.session_state
3084
+ )
3085
+ async for event in self._arun_workflow_agent_stream(
3086
+ agent_input=user_input,
3087
+ session=session,
3088
+ execution_input=execution_input,
3089
+ run_context=run_context,
3090
+ stream=stream,
3091
+ websocket_handler=websocket_handler,
3092
+ **kwargs,
3093
+ ):
3094
+ yield event
3095
+
3096
+ return _stream()
3097
+ else:
3098
+
3099
+ async def _execute():
3100
+ session, session_state_loaded = await self._aload_session_for_workflow_agent(
3101
+ run_context.session_id, run_context.user_id, run_context.session_state
3102
+ )
3103
+ return await self._arun_workflow_agent(
3104
+ agent_input=user_input,
3105
+ session=session,
3106
+ execution_input=execution_input,
3107
+ run_context=run_context,
3108
+ stream=stream,
3109
+ )
3110
+
3111
+ return _execute()
3112
+
3113
+ async def _arun_workflow_agent_stream(
3114
+ self,
3115
+ agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
3116
+ session: WorkflowSession,
3117
+ execution_input: WorkflowExecutionInput,
3118
+ run_context: RunContext,
3119
+ stream: bool = False,
3120
+ websocket_handler: Optional[WebSocketHandler] = None,
3121
+ **kwargs: Any,
3122
+ ) -> AsyncIterator[WorkflowRunOutputEvent]:
3123
+ """
3124
+ Execute the workflow agent asynchronously in streaming mode.
3125
+
3126
+ The agent's tool (run_workflow) is an async generator that yields workflow events directly.
3127
+ These events bubble up through the agent's streaming and are yielded here.
3128
+ We filter to only yield WorkflowRunOutputEvent to the CLI.
3129
+
3130
+ Yields:
3131
+ WorkflowRunOutputEvent: Events from workflow execution (agent events are filtered)
3132
+ """
3133
+ from typing import get_args
3134
+
3135
+ from agno.run.workflow import WorkflowCompletedEvent, WorkflowRunOutputEvent
3136
+
3137
+ logger.info("Workflow agent enabled - async streaming mode")
3138
+ log_debug(f"User input: {agent_input}")
3139
+
3140
+ self._async_initialize_workflow_agent(
3141
+ session,
3142
+ execution_input,
3143
+ run_context=run_context,
3144
+ stream=stream,
3145
+ websocket_handler=websocket_handler,
3146
+ )
3147
+
3148
+ run_context.dependencies = self._get_workflow_agent_dependencies(session)
3149
+
3150
+ agent_response: Optional[RunOutput] = None
3151
+ workflow_executed = False
3152
+
3153
+ from agno.run.agent import RunContentEvent
3154
+ from agno.run.team import RunContentEvent as TeamRunContentEvent
3155
+ from agno.run.workflow import WorkflowAgentCompletedEvent, WorkflowAgentStartedEvent
3156
+
3157
+ log_debug(f"Executing async workflow agent with streaming - input: {agent_input}...")
3158
+
3159
+ # Create a workflow run response upfront for potential direct answer (will be used only if workflow is not executed)
3160
+ run_id = str(uuid4())
3161
+ direct_reply_run_response = WorkflowRunOutput(
3162
+ run_id=run_id,
3163
+ input=execution_input.input,
3164
+ session_id=session.session_id,
3165
+ workflow_id=self.id,
3166
+ workflow_name=self.name,
3167
+ created_at=int(datetime.now().timestamp()),
3168
+ )
3169
+
3170
+ # Yield WorkflowAgentStartedEvent at the beginning (stored in direct_reply_run_response)
3171
+ agent_started_event = WorkflowAgentStartedEvent(
3172
+ workflow_name=self.name,
3173
+ workflow_id=self.id,
3174
+ session_id=session.session_id,
3175
+ )
3176
+ self._broadcast_to_websocket(agent_started_event, websocket_handler)
3177
+ yield agent_started_event
3178
+
3179
+ # Run the agent in streaming mode and yield all events
3180
+ async for event in self.agent.arun( # type: ignore[union-attr]
3181
+ input=agent_input,
3182
+ stream=True,
3183
+ stream_intermediate_steps=True,
3184
+ yield_run_response=True,
3185
+ session_id=session.session_id,
3186
+ dependencies=run_context.dependencies, # Pass context dynamically per-run
3187
+ session_state=run_context.session_state, # Pass session state dynamically per-run
3188
+ ): # type: ignore
3189
+ if isinstance(event, tuple(get_args(WorkflowRunOutputEvent))):
3190
+ yield event # type: ignore[misc]
3191
+
3192
+ if isinstance(event, WorkflowCompletedEvent):
3193
+ workflow_executed = True
3194
+ log_debug("Workflow execution detected via WorkflowCompletedEvent")
3195
+
3196
+ elif isinstance(event, (RunContentEvent, TeamRunContentEvent)):
3197
+ if event.step_name is None:
3198
+ # This is from the workflow agent itself
3199
+ # Enrich with metadata to mark it as a workflow agent event
3200
+
3201
+ if workflow_executed:
3202
+ continue # Skip if workflow was already executed
3203
+
3204
+ # workflow_agent field is used by consumers of the events to distinguish between workflow agent and regular agent
3205
+ event.workflow_agent = True # type: ignore
3206
+
3207
+ # Broadcast to WebSocket if available (async context only)
3208
+ self._broadcast_to_websocket(event, websocket_handler)
3209
+
3210
+ yield event # type: ignore[misc]
3211
+
3212
+ # Capture the final RunOutput (but don't yield it)
3213
+ if isinstance(event, RunOutput):
3214
+ agent_response = event
3215
+ log_debug(
3216
+ f"Agent response: {str(agent_response.content)[:100] if agent_response.content else 'None'}..."
3217
+ )
3218
+
3219
+ # Handle direct answer case (no workflow execution)
3220
+ if not workflow_executed:
3221
+ # Update the pre-created workflow run response with the direct answer
3222
+ direct_reply_run_response.content = agent_response.content if agent_response else ""
3223
+ direct_reply_run_response.status = RunStatus.completed
3224
+ direct_reply_run_response.workflow_agent_run = agent_response
3225
+
3226
+ workflow_run_response = direct_reply_run_response
3227
+
3228
+ # Store the full agent RunOutput and establish parent-child relationship
3229
+ if agent_response:
3230
+ agent_response.parent_run_id = workflow_run_response.run_id
3231
+ agent_response.workflow_id = workflow_run_response.workflow_id
3232
+
3233
+ # Yield WorkflowAgentCompletedEvent
3234
+ agent_completed_event = WorkflowAgentCompletedEvent(
3235
+ workflow_name=self.name,
3236
+ workflow_id=self.id,
3237
+ run_id=agent_response.run_id if agent_response else None,
3238
+ session_id=session.session_id,
3239
+ content=workflow_run_response.content,
3240
+ )
3241
+ self._broadcast_to_websocket(agent_completed_event, websocket_handler)
3242
+ yield agent_completed_event
3243
+
3244
+ # Yield a workflow completed event with the agent's direct response (user internally by aprint_response_stream)
3245
+ completed_event = WorkflowCompletedEvent(
3246
+ run_id=workflow_run_response.run_id or "",
3247
+ content=workflow_run_response.content,
3248
+ workflow_name=workflow_run_response.workflow_name,
3249
+ workflow_id=workflow_run_response.workflow_id,
3250
+ session_id=workflow_run_response.session_id,
3251
+ step_results=[],
3252
+ metadata={"agent_direct_response": True},
3253
+ )
3254
+ yield completed_event
3255
+
3256
+ # Update the run in session
3257
+ session.upsert_run(run=workflow_run_response)
3258
+ # Save session
3259
+ if self._has_async_db():
3260
+ await self.asave_session(session=session)
3261
+ else:
3262
+ self.save_session(session=session)
3263
+
3264
+ else:
3265
+ # Workflow was executed by the tool
3266
+ if self._has_async_db():
3267
+ reloaded_session = await self.aget_session(session_id=session.session_id)
3268
+ else:
3269
+ reloaded_session = self.get_session(session_id=session.session_id)
3270
+
3271
+ if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
3272
+ # Get the last run (which is the one just created by the tool)
3273
+ last_run = reloaded_session.runs[-1]
3274
+
3275
+ # Yield WorkflowAgentCompletedEvent
3276
+ agent_completed_event = WorkflowAgentCompletedEvent(
3277
+ run_id=agent_response.run_id if agent_response else None,
3278
+ workflow_name=self.name,
3279
+ workflow_id=self.id,
3280
+ session_id=session.session_id,
3281
+ content=agent_response.content if agent_response else None,
3282
+ )
3283
+
3284
+ self._broadcast_to_websocket(agent_completed_event, websocket_handler)
3285
+
3286
+ yield agent_completed_event
3287
+
3288
+ # Update the last run with workflow_agent_run
3289
+ last_run.workflow_agent_run = agent_response
3290
+
3291
+ # Store the full agent RunOutput and establish parent-child relationship
3292
+ if agent_response:
3293
+ agent_response.parent_run_id = last_run.run_id
3294
+ agent_response.workflow_id = last_run.workflow_id
3295
+
3296
+ # Save the reloaded session (which has the updated run)
3297
+ if self._has_async_db():
3298
+ await self.asave_session(session=reloaded_session)
3299
+ else:
3300
+ self.save_session(session=reloaded_session)
3301
+
3302
+ else:
3303
+ log_warning("Could not reload session or no runs found after workflow execution")
3304
+
3305
+ async def _arun_workflow_agent(
3306
+ self,
3307
+ agent_input: Union[str, Dict[str, Any], List[Any], BaseModel],
3308
+ session: WorkflowSession,
3309
+ execution_input: WorkflowExecutionInput,
3310
+ run_context: RunContext,
3311
+ stream: bool = False,
3312
+ ) -> WorkflowRunOutput:
3313
+ """
3314
+ Execute the workflow agent asynchronously in non-streaming mode.
3315
+
3316
+ The agent decides whether to run the workflow or answer directly from history.
1871
3317
 
1872
- self._prepare_steps()
3318
+ Returns:
3319
+ WorkflowRunOutput: The workflow run output with agent response
3320
+ """
3321
+ # Initialize the agent
3322
+ self._async_initialize_workflow_agent(session, execution_input, run_context=run_context, stream=stream)
3323
+
3324
+ # Build dependencies with workflow context
3325
+ run_context.dependencies = self._get_workflow_agent_dependencies(session)
3326
+
3327
+ # Run the agent
3328
+ agent_response: RunOutput = await self.agent.arun( # type: ignore[union-attr]
3329
+ input=agent_input,
3330
+ session_id=session.session_id,
3331
+ dependencies=run_context.dependencies,
3332
+ session_state=run_context.session_state,
3333
+ stream=stream,
3334
+ ) # type: ignore
3335
+
3336
+ # Check if the agent called the workflow tool
3337
+ workflow_executed = False
3338
+ if agent_response.messages:
3339
+ for message in agent_response.messages:
3340
+ if message.role == "assistant" and message.tool_calls:
3341
+ # Check if the tool call is specifically for run_workflow
3342
+ for tool_call in message.tool_calls:
3343
+ # Handle both dict and object formats
3344
+ if isinstance(tool_call, dict):
3345
+ tool_name = tool_call.get("function", {}).get("name", "")
3346
+ else:
3347
+ tool_name = tool_call.function.name if hasattr(tool_call, "function") else ""
1873
3348
 
1874
- # Create workflow run response with PENDING status
1875
- workflow_run_response = WorkflowRunOutput(
1876
- run_id=run_id,
1877
- input=input,
1878
- session_id=session_id,
1879
- workflow_id=self.id,
1880
- workflow_name=self.name,
1881
- created_at=int(datetime.now().timestamp()),
1882
- status=RunStatus.pending,
1883
- )
3349
+ if tool_name == "run_workflow":
3350
+ workflow_executed = True
3351
+ break
3352
+ if workflow_executed:
3353
+ break
1884
3354
 
1885
- # Store PENDING response immediately
1886
- workflow_session.upsert_run(run=workflow_run_response)
1887
- self.save_session(session=workflow_session)
3355
+ # Handle direct answer case (no workflow execution)
3356
+ if not workflow_executed:
3357
+ # Create a new workflow run output for the direct answer
3358
+ run_id = str(uuid4())
3359
+ workflow_run_response = WorkflowRunOutput(
3360
+ run_id=run_id,
3361
+ input=execution_input.input,
3362
+ session_id=session.session_id,
3363
+ workflow_id=self.id,
3364
+ workflow_name=self.name,
3365
+ created_at=int(datetime.now().timestamp()),
3366
+ content=agent_response.content,
3367
+ status=RunStatus.completed,
3368
+ workflow_agent_run=agent_response,
3369
+ )
1888
3370
 
1889
- # Prepare execution input
1890
- inputs = WorkflowExecutionInput(
1891
- input=input,
1892
- additional_data=additional_data,
1893
- audio=audio, # type: ignore
1894
- images=images, # type: ignore
1895
- videos=videos, # type: ignore
1896
- files=files, # type: ignore
1897
- )
3371
+ # Store the full agent RunOutput and establish parent-child relationship
3372
+ if agent_response:
3373
+ agent_response.parent_run_id = workflow_run_response.run_id
3374
+ agent_response.workflow_id = workflow_run_response.workflow_id
1898
3375
 
1899
- self.update_agents_and_teams_session_info()
3376
+ # Update the run in session
3377
+ session.upsert_run(run=workflow_run_response)
3378
+ if self._has_async_db():
3379
+ await self.asave_session(session=session)
3380
+ else:
3381
+ self.save_session(session=session)
1900
3382
 
1901
- async def execute_workflow_background_stream():
1902
- """Background execution with streaming and WebSocket broadcasting"""
1903
- try:
1904
- # Update status to RUNNING and save
1905
- workflow_run_response.status = RunStatus.running
1906
- self.save_session(session=workflow_session)
1907
-
1908
- # Execute with streaming - consume all events (they're auto-broadcast via _handle_event)
1909
- async for event in self._aexecute_stream(
1910
- execution_input=inputs,
1911
- session=workflow_session,
1912
- workflow_run_response=workflow_run_response,
1913
- stream_intermediate_steps=stream_intermediate_steps,
1914
- session_state=session_state,
1915
- websocket_handler=websocket_handler,
1916
- **kwargs,
1917
- ):
1918
- # Events are automatically broadcast by _handle_event
1919
- # We just consume them here to drive the execution
1920
- pass
3383
+ log_debug(f"Agent decision: workflow_executed={workflow_executed}")
1921
3384
 
1922
- log_debug(f"Background streaming execution completed with status: {workflow_run_response.status}")
3385
+ return workflow_run_response
3386
+ else:
3387
+ # Workflow was executed by the tool
3388
+ logger.info("=" * 80)
3389
+ logger.info("WORKFLOW AGENT: Called run_workflow tool (async)")
3390
+ logger.info(" ➜ Workflow was executed, retrieving results...")
3391
+ logger.info("=" * 80)
3392
+
3393
+ log_debug("Reloading session from database to get the latest workflow run...")
3394
+ if self._has_async_db():
3395
+ reloaded_session = await self.aget_session(session_id=session.session_id)
3396
+ else:
3397
+ reloaded_session = self.get_session(session_id=session.session_id)
1923
3398
 
1924
- except Exception as e:
1925
- logger.error(f"Background streaming workflow execution failed: {e}")
1926
- workflow_run_response.status = RunStatus.error
1927
- workflow_run_response.content = f"Background streaming execution failed: {str(e)}"
1928
- self.save_session(session=workflow_session)
3399
+ if reloaded_session and reloaded_session.runs and len(reloaded_session.runs) > 0:
3400
+ # Get the last run (which is the one just created by the tool)
3401
+ last_run = reloaded_session.runs[-1]
3402
+ log_debug(f"Retrieved latest workflow run: {last_run.run_id}")
3403
+ log_debug(f"Total workflow runs in session: {len(reloaded_session.runs)}")
1929
3404
 
1930
- # Create and start asyncio task for background streaming execution
1931
- loop = asyncio.get_running_loop()
1932
- loop.create_task(execute_workflow_background_stream())
3405
+ # Update the last run with workflow_agent_run
3406
+ last_run.workflow_agent_run = agent_response
1933
3407
 
1934
- # Return SAME object that will be updated by background execution
1935
- return workflow_run_response
3408
+ # Store the full agent RunOutput and establish parent-child relationship
3409
+ if agent_response:
3410
+ agent_response.parent_run_id = last_run.run_id
3411
+ agent_response.workflow_id = last_run.workflow_id
1936
3412
 
1937
- def get_run(self, run_id: str) -> Optional[WorkflowRunOutput]:
1938
- """Get the status and details of a background workflow run - SIMPLIFIED"""
1939
- if self.db is not None and self.session_id is not None:
1940
- session = self.db.get_session(session_id=self.session_id, session_type=SessionType.WORKFLOW)
1941
- if session and isinstance(session, WorkflowSession) and session.runs:
1942
- # Find the run by ID
1943
- for run in session.runs:
1944
- if run.run_id == run_id:
1945
- return run
3413
+ # Save the reloaded session (which has the updated run)
3414
+ if self._has_async_db():
3415
+ await self.asave_session(session=reloaded_session)
3416
+ else:
3417
+ self.save_session(session=reloaded_session)
1946
3418
 
1947
- return None
3419
+ log_debug(f"Agent decision: workflow_executed={workflow_executed}")
3420
+
3421
+ # Return the last run directly (WRO2 from inner workflow)
3422
+ return last_run
3423
+ else:
3424
+ log_warning("Could not reload session or no runs found after workflow execution")
3425
+ # Return a placeholder error response
3426
+ return WorkflowRunOutput(
3427
+ run_id=str(uuid4()),
3428
+ input=execution_input.input,
3429
+ session_id=session.session_id,
3430
+ workflow_id=self.id,
3431
+ workflow_name=self.name,
3432
+ created_at=int(datetime.now().timestamp()),
3433
+ content="Error: Workflow execution failed",
3434
+ status=RunStatus.error,
3435
+ )
1948
3436
 
1949
3437
  def cancel_run(self, run_id: str) -> bool:
1950
3438
  """Cancel a running workflow execution.
@@ -1963,6 +3451,7 @@ class Workflow:
1963
3451
  input: Optional[Union[str, Dict[str, Any], List[Any], BaseModel]] = None,
1964
3452
  additional_data: Optional[Dict[str, Any]] = None,
1965
3453
  user_id: Optional[str] = None,
3454
+ run_id: Optional[str] = None,
1966
3455
  session_id: Optional[str] = None,
1967
3456
  session_state: Optional[Dict[str, Any]] = None,
1968
3457
  audio: Optional[List[Audio]] = None,
@@ -1970,8 +3459,10 @@ class Workflow:
1970
3459
  videos: Optional[List[Video]] = None,
1971
3460
  files: Optional[List[File]] = None,
1972
3461
  stream: Literal[False] = False,
3462
+ stream_events: Optional[bool] = None,
1973
3463
  stream_intermediate_steps: Optional[bool] = None,
1974
3464
  background: Optional[bool] = False,
3465
+ background_tasks: Optional[Any] = None,
1975
3466
  ) -> WorkflowRunOutput: ...
1976
3467
 
1977
3468
  @overload
@@ -1980,6 +3471,7 @@ class Workflow:
1980
3471
  input: Optional[Union[str, Dict[str, Any], List[Any], BaseModel]] = None,
1981
3472
  additional_data: Optional[Dict[str, Any]] = None,
1982
3473
  user_id: Optional[str] = None,
3474
+ run_id: Optional[str] = None,
1983
3475
  session_id: Optional[str] = None,
1984
3476
  session_state: Optional[Dict[str, Any]] = None,
1985
3477
  audio: Optional[List[Audio]] = None,
@@ -1987,8 +3479,10 @@ class Workflow:
1987
3479
  videos: Optional[List[Video]] = None,
1988
3480
  files: Optional[List[File]] = None,
1989
3481
  stream: Literal[True] = True,
3482
+ stream_events: Optional[bool] = None,
1990
3483
  stream_intermediate_steps: Optional[bool] = None,
1991
3484
  background: Optional[bool] = False,
3485
+ background_tasks: Optional[Any] = None,
1992
3486
  ) -> Iterator[WorkflowRunOutputEvent]: ...
1993
3487
 
1994
3488
  def run(
@@ -1996,6 +3490,7 @@ class Workflow:
1996
3490
  input: Optional[Union[str, Dict[str, Any], List[Any], BaseModel]] = None,
1997
3491
  additional_data: Optional[Dict[str, Any]] = None,
1998
3492
  user_id: Optional[str] = None,
3493
+ run_id: Optional[str] = None,
1999
3494
  session_id: Optional[str] = None,
2000
3495
  session_state: Optional[Dict[str, Any]] = None,
2001
3496
  audio: Optional[List[Audio]] = None,
@@ -2003,11 +3498,19 @@ class Workflow:
2003
3498
  videos: Optional[List[Video]] = None,
2004
3499
  files: Optional[List[File]] = None,
2005
3500
  stream: bool = False,
3501
+ stream_events: Optional[bool] = None,
2006
3502
  stream_intermediate_steps: Optional[bool] = None,
2007
3503
  background: Optional[bool] = False,
3504
+ background_tasks: Optional[Any] = None,
2008
3505
  **kwargs: Any,
2009
3506
  ) -> Union[WorkflowRunOutput, Iterator[WorkflowRunOutputEvent]]:
2010
3507
  """Execute the workflow synchronously with optional streaming"""
3508
+ if self._has_async_db():
3509
+ raise Exception("`run()` is not supported with an async DB. Please use `arun()`.")
3510
+
3511
+ # Set the id for the run and register it immediately for cancellation tracking
3512
+ run_id = run_id or str(uuid4())
3513
+ register_run(run_id)
2011
3514
 
2012
3515
  input = self._validate_input(input)
2013
3516
  if background:
@@ -2015,17 +3518,20 @@ class Workflow:
2015
3518
 
2016
3519
  self._set_debug()
2017
3520
 
2018
- run_id = str(uuid4())
2019
-
2020
3521
  self.initialize_workflow()
2021
- session_id, user_id, session_state = self._initialize_session(
2022
- session_id=session_id, user_id=user_id, session_state=session_state, run_id=run_id
2023
- )
3522
+ session_id, user_id = self._initialize_session(session_id=session_id, user_id=user_id)
2024
3523
 
2025
3524
  # Read existing session from database
2026
3525
  workflow_session = self.read_or_create_session(session_id=session_id, user_id=user_id)
2027
3526
  self._update_metadata(session=workflow_session)
2028
3527
 
3528
+ # Initialize session state
3529
+ session_state = self._initialize_session_state(
3530
+ session_state=session_state if session_state is not None else {},
3531
+ user_id=user_id,
3532
+ session_id=session_id,
3533
+ run_id=run_id,
3534
+ )
2029
3535
  # Update session state from DB
2030
3536
  session_state = self._load_session_state(session=workflow_session, session_state=session_state)
2031
3537
 
@@ -2033,11 +3539,13 @@ class Workflow:
2033
3539
 
2034
3540
  # Use simple defaults
2035
3541
  stream = stream or self.stream or False
2036
- stream_intermediate_steps = stream_intermediate_steps or self.stream_intermediate_steps or False
3542
+ stream_events = (stream_events or stream_intermediate_steps) or (
3543
+ self.stream_events or self.stream_intermediate_steps
3544
+ )
2037
3545
 
2038
- # Can't have stream_intermediate_steps if stream is False
2039
- if not stream:
2040
- stream_intermediate_steps = False
3546
+ # Can't stream events if streaming is disabled
3547
+ if stream is False:
3548
+ stream_events = False
2041
3549
 
2042
3550
  log_debug(f"Stream: {stream}")
2043
3551
  log_debug(f"Total steps: {self._get_step_count()}")
@@ -2045,16 +3553,6 @@ class Workflow:
2045
3553
  # Prepare steps
2046
3554
  self._prepare_steps()
2047
3555
 
2048
- # Create workflow run response that will be updated by reference
2049
- workflow_run_response = WorkflowRunOutput(
2050
- run_id=run_id,
2051
- input=input,
2052
- session_id=session_id,
2053
- workflow_id=self.id,
2054
- workflow_name=self.name,
2055
- created_at=int(datetime.now().timestamp()),
2056
- )
2057
-
2058
3556
  inputs = WorkflowExecutionInput(
2059
3557
  input=input,
2060
3558
  additional_data=additional_data,
@@ -2069,13 +3567,47 @@ class Workflow:
2069
3567
 
2070
3568
  self.update_agents_and_teams_session_info()
2071
3569
 
3570
+ # Initialize run context
3571
+ run_context = RunContext(
3572
+ run_id=run_id,
3573
+ session_id=session_id,
3574
+ user_id=user_id,
3575
+ session_state=session_state,
3576
+ )
3577
+
3578
+ # Execute workflow agent if configured
3579
+ if self.agent is not None:
3580
+ return self._execute_workflow_agent(
3581
+ user_input=input, # type: ignore
3582
+ session=workflow_session,
3583
+ execution_input=inputs,
3584
+ run_context=run_context,
3585
+ stream=stream,
3586
+ **kwargs,
3587
+ )
3588
+
3589
+ # Create workflow run response for regular workflow execution
3590
+ workflow_run_response = WorkflowRunOutput(
3591
+ run_id=run_id,
3592
+ input=input,
3593
+ session_id=session_id,
3594
+ workflow_id=self.id,
3595
+ workflow_name=self.name,
3596
+ created_at=int(datetime.now().timestamp()),
3597
+ )
3598
+
3599
+ # Start the run metrics timer
3600
+ workflow_run_response.metrics = WorkflowMetrics(steps={})
3601
+ workflow_run_response.metrics.start_timer()
3602
+
2072
3603
  if stream:
2073
3604
  return self._execute_stream(
2074
3605
  session=workflow_session,
2075
3606
  execution_input=inputs, # type: ignore[arg-type]
2076
3607
  workflow_run_response=workflow_run_response,
2077
- stream_intermediate_steps=stream_intermediate_steps,
2078
- session_state=session_state,
3608
+ stream_events=stream_events,
3609
+ run_context=run_context,
3610
+ background_tasks=background_tasks,
2079
3611
  **kwargs,
2080
3612
  )
2081
3613
  else:
@@ -2083,7 +3615,8 @@ class Workflow:
2083
3615
  session=workflow_session,
2084
3616
  execution_input=inputs, # type: ignore[arg-type]
2085
3617
  workflow_run_response=workflow_run_response,
2086
- session_state=session_state,
3618
+ run_context=run_context,
3619
+ background_tasks=background_tasks,
2087
3620
  **kwargs,
2088
3621
  )
2089
3622
 
@@ -2093,6 +3626,7 @@ class Workflow:
2093
3626
  input: Optional[Union[str, Dict[str, Any], List[Any], BaseModel, List[Message]]] = None,
2094
3627
  additional_data: Optional[Dict[str, Any]] = None,
2095
3628
  user_id: Optional[str] = None,
3629
+ run_id: Optional[str] = None,
2096
3630
  session_id: Optional[str] = None,
2097
3631
  session_state: Optional[Dict[str, Any]] = None,
2098
3632
  audio: Optional[List[Audio]] = None,
@@ -2100,17 +3634,20 @@ class Workflow:
2100
3634
  videos: Optional[List[Video]] = None,
2101
3635
  files: Optional[List[File]] = None,
2102
3636
  stream: Literal[False] = False,
3637
+ stream_events: Optional[bool] = None,
2103
3638
  stream_intermediate_steps: Optional[bool] = None,
2104
3639
  background: Optional[bool] = False,
2105
3640
  websocket: Optional[WebSocket] = None,
3641
+ background_tasks: Optional[Any] = None,
2106
3642
  ) -> WorkflowRunOutput: ...
2107
3643
 
2108
3644
  @overload
2109
- async def arun(
3645
+ def arun(
2110
3646
  self,
2111
3647
  input: Optional[Union[str, Dict[str, Any], List[Any], BaseModel, List[Message]]] = None,
2112
3648
  additional_data: Optional[Dict[str, Any]] = None,
2113
3649
  user_id: Optional[str] = None,
3650
+ run_id: Optional[str] = None,
2114
3651
  session_id: Optional[str] = None,
2115
3652
  session_state: Optional[Dict[str, Any]] = None,
2116
3653
  audio: Optional[List[Audio]] = None,
@@ -2118,16 +3655,19 @@ class Workflow:
2118
3655
  videos: Optional[List[Video]] = None,
2119
3656
  files: Optional[List[File]] = None,
2120
3657
  stream: Literal[True] = True,
3658
+ stream_events: Optional[bool] = None,
2121
3659
  stream_intermediate_steps: Optional[bool] = None,
2122
3660
  background: Optional[bool] = False,
2123
3661
  websocket: Optional[WebSocket] = None,
3662
+ background_tasks: Optional[Any] = None,
2124
3663
  ) -> AsyncIterator[WorkflowRunOutputEvent]: ...
2125
3664
 
2126
- async def arun(
3665
+ def arun( # type: ignore
2127
3666
  self,
2128
3667
  input: Optional[Union[str, Dict[str, Any], List[Any], BaseModel, List[Message]]] = None,
2129
3668
  additional_data: Optional[Dict[str, Any]] = None,
2130
3669
  user_id: Optional[str] = None,
3670
+ run_id: Optional[str] = None,
2131
3671
  session_id: Optional[str] = None,
2132
3672
  session_state: Optional[Dict[str, Any]] = None,
2133
3673
  audio: Optional[List[Audio]] = None,
@@ -2135,9 +3675,11 @@ class Workflow:
2135
3675
  videos: Optional[List[Video]] = None,
2136
3676
  files: Optional[List[File]] = None,
2137
3677
  stream: bool = False,
3678
+ stream_events: Optional[bool] = None,
2138
3679
  stream_intermediate_steps: Optional[bool] = False,
2139
3680
  background: Optional[bool] = False,
2140
3681
  websocket: Optional[WebSocket] = None,
3682
+ background_tasks: Optional[Any] = None,
2141
3683
  **kwargs: Any,
2142
3684
  ) -> Union[WorkflowRunOutput, AsyncIterator[WorkflowRunOutputEvent]]:
2143
3685
  """Execute the workflow synchronously with optional streaming"""
@@ -2152,8 +3694,17 @@ class Workflow:
2152
3694
 
2153
3695
  if background:
2154
3696
  if stream and websocket:
3697
+ # Consider both stream_events and stream_intermediate_steps (deprecated)
3698
+ if stream_intermediate_steps is not None:
3699
+ warnings.warn(
3700
+ "The 'stream_intermediate_steps' parameter is deprecated and will be removed in future versions. Use 'stream_events' instead.",
3701
+ DeprecationWarning,
3702
+ stacklevel=2,
3703
+ )
3704
+ stream_events = stream_events or stream_intermediate_steps or False
3705
+
2155
3706
  # Background + Streaming + WebSocket = Real-time events
2156
- return await self._arun_background_stream(
3707
+ return self._arun_background_stream( # type: ignore
2157
3708
  input=input,
2158
3709
  additional_data=additional_data,
2159
3710
  user_id=user_id,
@@ -2163,7 +3714,7 @@ class Workflow:
2163
3714
  images=images,
2164
3715
  videos=videos,
2165
3716
  files=files,
2166
- stream_intermediate_steps=stream_intermediate_steps or False,
3717
+ stream_events=stream_events,
2167
3718
  websocket_handler=websocket_handler,
2168
3719
  **kwargs,
2169
3720
  )
@@ -2172,7 +3723,7 @@ class Workflow:
2172
3723
  raise ValueError("Background streaming execution requires a WebSocket for real-time events")
2173
3724
  else:
2174
3725
  # Background + Non-streaming = Polling (existing)
2175
- return await self._arun_background(
3726
+ return self._arun_background( # type: ignore
2176
3727
  input=input,
2177
3728
  additional_data=additional_data,
2178
3729
  user_id=user_id,
@@ -2187,45 +3738,38 @@ class Workflow:
2187
3738
 
2188
3739
  self._set_debug()
2189
3740
 
2190
- run_id = str(uuid4())
3741
+ # Set the id for the run and register it immediately for cancellation tracking
3742
+ run_id = run_id or str(uuid4())
3743
+ register_run(run_id)
2191
3744
 
2192
3745
  self.initialize_workflow()
2193
- session_id, user_id, session_state = self._initialize_session(
2194
- session_id=session_id, user_id=user_id, session_state=session_state, run_id=run_id
2195
- )
2196
-
2197
- # Read existing session from database
2198
- workflow_session = self.read_or_create_session(session_id=session_id, user_id=user_id)
2199
- self._update_metadata(session=workflow_session)
3746
+ session_id, user_id = self._initialize_session(session_id=session_id, user_id=user_id)
2200
3747
 
2201
- # Update session state from DB
2202
- session_state = self._load_session_state(session=workflow_session, session_state=session_state)
3748
+ # Initialize run context
3749
+ run_context = RunContext(
3750
+ run_id=run_id,
3751
+ session_id=session_id,
3752
+ user_id=user_id,
3753
+ session_state=session_state,
3754
+ )
2203
3755
 
2204
3756
  log_debug(f"Async Workflow Run Start: {self.name}", center=True)
2205
3757
 
2206
3758
  # Use simple defaults
2207
3759
  stream = stream or self.stream or False
2208
- stream_intermediate_steps = stream_intermediate_steps or self.stream_intermediate_steps or False
3760
+ stream_events = (stream_events or stream_intermediate_steps) or (
3761
+ self.stream_events or self.stream_intermediate_steps
3762
+ )
2209
3763
 
2210
- # Can't have stream_intermediate_steps if stream is False
2211
- if not stream:
2212
- stream_intermediate_steps = False
3764
+ # Can't stream events if streaming is disabled
3765
+ if stream is False:
3766
+ stream_events = False
2213
3767
 
2214
3768
  log_debug(f"Stream: {stream}")
2215
3769
 
2216
3770
  # Prepare steps
2217
3771
  self._prepare_steps()
2218
3772
 
2219
- # Create workflow run response that will be updated by reference
2220
- workflow_run_response = WorkflowRunOutput(
2221
- run_id=run_id,
2222
- input=input,
2223
- session_id=session_id,
2224
- workflow_id=self.id,
2225
- workflow_name=self.name,
2226
- created_at=int(datetime.now().timestamp()),
2227
- )
2228
-
2229
3773
  inputs = WorkflowExecutionInput(
2230
3774
  input=input,
2231
3775
  additional_data=additional_data,
@@ -2240,25 +3784,54 @@ class Workflow:
2240
3784
 
2241
3785
  self.update_agents_and_teams_session_info()
2242
3786
 
3787
+ if self.agent is not None:
3788
+ return self._aexecute_workflow_agent( # type: ignore
3789
+ user_input=input, # type: ignore
3790
+ execution_input=inputs,
3791
+ run_context=run_context,
3792
+ stream=stream,
3793
+ **kwargs,
3794
+ )
3795
+
3796
+ # Create workflow run response for regular workflow execution
3797
+ workflow_run_response = WorkflowRunOutput(
3798
+ run_id=run_id,
3799
+ input=input,
3800
+ session_id=session_id,
3801
+ workflow_id=self.id,
3802
+ workflow_name=self.name,
3803
+ created_at=int(datetime.now().timestamp()),
3804
+ )
3805
+
3806
+ # Start the run metrics timer
3807
+ workflow_run_response.metrics = WorkflowMetrics(steps={})
3808
+ workflow_run_response.metrics.start_timer()
3809
+
2243
3810
  if stream:
2244
- return self._aexecute_stream(
3811
+ return self._aexecute_stream( # type: ignore
2245
3812
  execution_input=inputs,
2246
3813
  workflow_run_response=workflow_run_response,
2247
- session=workflow_session,
2248
- stream_intermediate_steps=stream_intermediate_steps,
3814
+ session_id=session_id,
3815
+ user_id=user_id,
3816
+ stream_events=stream_events,
2249
3817
  websocket=websocket,
2250
3818
  files=files,
2251
3819
  session_state=session_state,
3820
+ run_context=run_context,
3821
+ background_tasks=background_tasks,
2252
3822
  **kwargs,
2253
3823
  )
2254
3824
  else:
2255
- return await self._aexecute(
3825
+ return self._aexecute( # type: ignore
2256
3826
  execution_input=inputs,
2257
3827
  workflow_run_response=workflow_run_response,
2258
- session=workflow_session,
3828
+ session_id=session_id,
3829
+ user_id=user_id,
2259
3830
  websocket=websocket,
2260
3831
  files=files,
2261
3832
  session_state=session_state,
3833
+ run_context=run_context,
3834
+ background_tasks=background_tasks,
2262
3835
  **kwargs,
2263
3836
  )
2264
3837
 
@@ -2270,7 +3843,7 @@ class Workflow:
2270
3843
  if callable(step) and hasattr(step, "__name__"):
2271
3844
  step_name = step.__name__
2272
3845
  log_debug(f"Step {i + 1}: Wrapping callable function '{step_name}'")
2273
- prepared_steps.append(Step(name=step_name, description="User-defined callable step", executor=step))
3846
+ prepared_steps.append(Step(name=step_name, description="User-defined callable step", executor=step)) # type: ignore
2274
3847
  elif isinstance(step, Agent):
2275
3848
  step_name = step.name or f"step_{i + 1}"
2276
3849
  log_debug(f"Step {i + 1}: Agent '{step_name}'")
@@ -2279,6 +3852,12 @@ class Workflow:
2279
3852
  step_name = step.name or f"step_{i + 1}"
2280
3853
  log_debug(f"Step {i + 1}: Team '{step_name}' with {len(step.members)} members")
2281
3854
  prepared_steps.append(Step(name=step_name, description=step.description, team=step))
3855
+ elif isinstance(step, Step) and step.add_workflow_history is True and self.db is None:
3856
+ log_warning(
3857
+ f"Step '{step.name or f'step_{i + 1}'}' has add_workflow_history=True "
3858
+ "but no database is configured in the Workflow. "
3859
+ "History won't be persisted. Add a database to persist runs across executions."
3860
+ )
2282
3861
  elif isinstance(step, (Step, Steps, Loop, Parallel, Condition, Router)):
2283
3862
  step_type = type(step).__name__
2284
3863
  step_name = getattr(step, "name", f"unnamed_{step_type.lower()}")
@@ -2301,7 +3880,6 @@ class Workflow:
2301
3880
  videos: Optional[List[Video]] = None,
2302
3881
  files: Optional[List[File]] = None,
2303
3882
  stream: Optional[bool] = None,
2304
- stream_intermediate_steps: Optional[bool] = None,
2305
3883
  markdown: bool = True,
2306
3884
  show_time: bool = True,
2307
3885
  show_step_details: bool = True,
@@ -2318,19 +3896,21 @@ class Workflow:
2318
3896
  audio: Audio input
2319
3897
  images: Image input
2320
3898
  videos: Video input
3899
+ files: File input
2321
3900
  stream: Whether to stream the response content
2322
- stream_intermediate_steps: Whether to stream intermediate steps
2323
3901
  markdown: Whether to render content as markdown
2324
3902
  show_time: Whether to show execution time
2325
3903
  show_step_details: Whether to show individual step outputs
2326
3904
  console: Rich console instance (optional)
2327
3905
  """
3906
+ if self._has_async_db():
3907
+ raise Exception("`print_response()` is not supported with an async DB. Please use `aprint_response()`.")
2328
3908
 
2329
3909
  if stream is None:
2330
3910
  stream = self.stream or False
2331
3911
 
2332
- if stream_intermediate_steps is None:
2333
- stream_intermediate_steps = self.stream_intermediate_steps or False
3912
+ if "stream_events" in kwargs:
3913
+ kwargs.pop("stream_events")
2334
3914
 
2335
3915
  if stream:
2336
3916
  print_response_stream(
@@ -2343,7 +3923,7 @@ class Workflow:
2343
3923
  images=images,
2344
3924
  videos=videos,
2345
3925
  files=files,
2346
- stream_intermediate_steps=stream_intermediate_steps,
3926
+ stream_events=True,
2347
3927
  markdown=markdown,
2348
3928
  show_time=show_time,
2349
3929
  show_step_details=show_step_details,
@@ -2379,7 +3959,6 @@ class Workflow:
2379
3959
  videos: Optional[List[Video]] = None,
2380
3960
  files: Optional[List[File]] = None,
2381
3961
  stream: Optional[bool] = None,
2382
- stream_intermediate_steps: Optional[bool] = None,
2383
3962
  markdown: bool = True,
2384
3963
  show_time: bool = True,
2385
3964
  show_step_details: bool = True,
@@ -2396,7 +3975,7 @@ class Workflow:
2396
3975
  audio: Audio input
2397
3976
  images: Image input
2398
3977
  videos: Video input
2399
- stream_intermediate_steps: Whether to stream intermediate steps
3978
+ files: Files input
2400
3979
  stream: Whether to stream the response content
2401
3980
  markdown: Whether to render content as markdown
2402
3981
  show_time: Whether to show execution time
@@ -2406,8 +3985,8 @@ class Workflow:
2406
3985
  if stream is None:
2407
3986
  stream = self.stream or False
2408
3987
 
2409
- if stream_intermediate_steps is None:
2410
- stream_intermediate_steps = self.stream_intermediate_steps or False
3988
+ if "stream_events" in kwargs:
3989
+ kwargs.pop("stream_events")
2411
3990
 
2412
3991
  if stream:
2413
3992
  await aprint_response_stream(
@@ -2420,7 +3999,7 @@ class Workflow:
2420
3999
  images=images,
2421
4000
  videos=videos,
2422
4001
  files=files,
2423
- stream_intermediate_steps=stream_intermediate_steps,
4002
+ stream_events=True,
2424
4003
  markdown=markdown,
2425
4004
  show_time=show_time,
2426
4005
  show_step_details=show_step_details,
@@ -2490,7 +4069,7 @@ class Workflow:
2490
4069
  step_dict["team"] = step.team if hasattr(step, "team") else None # type: ignore
2491
4070
 
2492
4071
  # Handle nested steps for Router/Loop
2493
- if isinstance(step, (Router)):
4072
+ if isinstance(step, Router):
2494
4073
  step_dict["steps"] = (
2495
4074
  [serialize_step(step) for step in step.choices] if hasattr(step, "choices") else None
2496
4075
  )
@@ -2546,7 +4125,7 @@ class Workflow:
2546
4125
 
2547
4126
  # If workflow has metrics, convert and add them to session metrics
2548
4127
  if workflow_run_response.metrics:
2549
- run_session_metrics = self._calculate_session_metrics_from_workflow_metrics(workflow_run_response.metrics)
4128
+ run_session_metrics = self._calculate_session_metrics_from_workflow_metrics(workflow_run_response.metrics) # type: ignore[arg-type]
2550
4129
 
2551
4130
  session_metrics += run_session_metrics
2552
4131
 
@@ -2557,6 +4136,18 @@ class Workflow:
2557
4136
  session.session_data = {}
2558
4137
  session.session_data["session_metrics"] = session_metrics.to_dict()
2559
4138
 
4139
+ async def aget_session_metrics(self, session_id: Optional[str] = None) -> Optional[Metrics]:
4140
+ """Get the session metrics for the given session ID and user ID."""
4141
+ session_id = session_id or self.session_id
4142
+ if session_id is None:
4143
+ raise Exception("Session ID is required")
4144
+
4145
+ session = await self.aget_session(session_id=session_id) # type: ignore
4146
+ if session is None:
4147
+ raise Exception("Session not found")
4148
+
4149
+ return self._get_session_metrics(session=session)
4150
+
2560
4151
  def get_session_metrics(self, session_id: Optional[str] = None) -> Optional[Metrics]:
2561
4152
  """Get the session metrics for the given session ID and user ID."""
2562
4153
  session_id = session_id or self.session_id
@@ -2585,10 +4176,60 @@ class Workflow:
2585
4176
 
2586
4177
  # If it's a team, update all members
2587
4178
  if hasattr(active_executor, "members"):
2588
- for member in active_executor.members:
4179
+ for member in active_executor.members: # type: ignore
2589
4180
  if hasattr(member, "workflow_id"):
2590
4181
  member.workflow_id = self.id
2591
4182
 
4183
+ def propagate_run_hooks_in_background(self, run_in_background: bool = True) -> None:
4184
+ """
4185
+ Propagate _run_hooks_in_background setting to this workflow and all agents/teams in steps.
4186
+
4187
+ This method sets _run_hooks_in_background on the workflow and all agents/teams
4188
+ within its steps, including nested teams and their members.
4189
+
4190
+ Args:
4191
+ run_in_background: Whether hooks should run in background. Defaults to True.
4192
+ """
4193
+ self._run_hooks_in_background = run_in_background
4194
+
4195
+ if not self.steps or callable(self.steps):
4196
+ return
4197
+
4198
+ steps_list = self.steps.steps if isinstance(self.steps, Steps) else self.steps
4199
+
4200
+ for step in steps_list:
4201
+ self._propagate_hooks_to_step(step, run_in_background)
4202
+
4203
+ def _propagate_hooks_to_step(self, step: Any, run_in_background: bool) -> None:
4204
+ """Recursively propagate _run_hooks_in_background to a step and its nested content."""
4205
+ # Handle Step objects with active executor
4206
+ if hasattr(step, "active_executor") and step.active_executor:
4207
+ executor = step.active_executor
4208
+ # If it's a team, use its propagation method
4209
+ if hasattr(executor, "propagate_run_hooks_in_background"):
4210
+ executor.propagate_run_hooks_in_background(run_in_background)
4211
+ elif hasattr(executor, "_run_hooks_in_background"):
4212
+ executor._run_hooks_in_background = run_in_background
4213
+
4214
+ # Handle agent/team directly on step
4215
+ if hasattr(step, "agent") and step.agent:
4216
+ if hasattr(step.agent, "_run_hooks_in_background"):
4217
+ step.agent._run_hooks_in_background = run_in_background
4218
+ if hasattr(step, "team") and step.team:
4219
+ # Use team's method to propagate to all nested members
4220
+ if hasattr(step.team, "propagate_run_hooks_in_background"):
4221
+ step.team.propagate_run_hooks_in_background(run_in_background)
4222
+ elif hasattr(step.team, "_run_hooks_in_background"):
4223
+ step.team._run_hooks_in_background = run_in_background
4224
+
4225
+ # Handle nested primitives - check 'steps' and 'choices' attributes
4226
+ for attr_name in ["steps", "choices"]:
4227
+ if hasattr(step, attr_name):
4228
+ attr_value = getattr(step, attr_name)
4229
+ if attr_value and isinstance(attr_value, list):
4230
+ for nested_step in attr_value:
4231
+ self._propagate_hooks_to_step(nested_step, run_in_background)
4232
+
2592
4233
  ###########################################################################
2593
4234
  # Telemetry functions
2594
4235
  ###########################################################################
@@ -2632,3 +4273,139 @@ class Workflow:
2632
4273
  )
2633
4274
  except Exception as e:
2634
4275
  log_debug(f"Could not create Workflow run telemetry event: {e}")
4276
+
4277
+ def cli_app(
4278
+ self,
4279
+ input: Optional[str] = None,
4280
+ session_id: Optional[str] = None,
4281
+ user_id: Optional[str] = None,
4282
+ user: str = "User",
4283
+ emoji: str = ":technologist:",
4284
+ stream: Optional[bool] = None,
4285
+ markdown: bool = True,
4286
+ show_time: bool = True,
4287
+ show_step_details: bool = True,
4288
+ exit_on: Optional[List[str]] = None,
4289
+ **kwargs: Any,
4290
+ ) -> None:
4291
+ """
4292
+ Run an interactive command-line interface to interact with the workflow.
4293
+
4294
+ This method creates a CLI interface that allows users to interact with the workflow
4295
+ either by providing a single input or through continuous interactive prompts.
4296
+
4297
+ Arguments:
4298
+ input: Optional initial input to process before starting interactive mode.
4299
+ session_id: Optional session identifier for maintaining conversation context.
4300
+ user_id: Optional user identifier for tracking user-specific data.
4301
+ user: Display name for the user in the CLI prompt. Defaults to "User".
4302
+ emoji: Emoji to display next to the user name in prompts. Defaults to ":technologist:".
4303
+ stream: Whether to stream the workflow response. If None, uses workflow default.
4304
+ markdown: Whether to render output as markdown. Defaults to True.
4305
+ show_time: Whether to display timestamps in the output. Defaults to True.
4306
+ show_step_details: Whether to show detailed step information. Defaults to True.
4307
+ exit_on: List of commands that will exit the CLI. Defaults to ["exit", "quit", "bye", "stop"].
4308
+ **kwargs: Additional keyword arguments passed to the workflow's print_response method.
4309
+
4310
+ Returns:
4311
+ None: This method runs interactively and does not return a value.
4312
+ """
4313
+
4314
+ from rich.prompt import Prompt
4315
+
4316
+ if input:
4317
+ self.print_response(
4318
+ input=input,
4319
+ stream=stream,
4320
+ markdown=markdown,
4321
+ show_time=show_time,
4322
+ show_step_details=show_step_details,
4323
+ user_id=user_id,
4324
+ session_id=session_id,
4325
+ **kwargs,
4326
+ )
4327
+
4328
+ _exit_on = exit_on or ["exit", "quit", "bye", "stop"]
4329
+ while True:
4330
+ message = Prompt.ask(f"[bold] {emoji} {user} [/bold]")
4331
+ if message in _exit_on:
4332
+ break
4333
+
4334
+ self.print_response(
4335
+ input=message,
4336
+ stream=stream,
4337
+ markdown=markdown,
4338
+ show_time=show_time,
4339
+ show_step_details=show_step_details,
4340
+ user_id=user_id,
4341
+ session_id=session_id,
4342
+ **kwargs,
4343
+ )
4344
+
4345
+ async def acli_app(
4346
+ self,
4347
+ input: Optional[str] = None,
4348
+ session_id: Optional[str] = None,
4349
+ user_id: Optional[str] = None,
4350
+ user: str = "User",
4351
+ emoji: str = ":technologist:",
4352
+ stream: Optional[bool] = None,
4353
+ markdown: bool = True,
4354
+ show_time: bool = True,
4355
+ show_step_details: bool = True,
4356
+ exit_on: Optional[List[str]] = None,
4357
+ **kwargs: Any,
4358
+ ) -> None:
4359
+ """
4360
+ Run an interactive command-line interface to interact with the workflow.
4361
+
4362
+ This method creates a CLI interface that allows users to interact with the workflow
4363
+ either by providing a single input or through continuous interactive prompts.
4364
+
4365
+ Arguments:
4366
+ input: Optional initial input to process before starting interactive mode.
4367
+ session_id: Optional session identifier for maintaining conversation context.
4368
+ user_id: Optional user identifier for tracking user-specific data.
4369
+ user: Display name for the user in the CLI prompt. Defaults to "User".
4370
+ emoji: Emoji to display next to the user name in prompts. Defaults to ":technologist:".
4371
+ stream: Whether to stream the workflow response. If None, uses workflow default.
4372
+ markdown: Whether to render output as markdown. Defaults to True.
4373
+ show_time: Whether to display timestamps in the output. Defaults to True.
4374
+ show_step_details: Whether to show detailed step information. Defaults to True.
4375
+ exit_on: List of commands that will exit the CLI. Defaults to ["exit", "quit", "bye", "stop"].
4376
+ **kwargs: Additional keyword arguments passed to the workflow's print_response method.
4377
+
4378
+ Returns:
4379
+ None: This method runs interactively and does not return a value.
4380
+ """
4381
+
4382
+ from rich.prompt import Prompt
4383
+
4384
+ if input:
4385
+ await self.aprint_response(
4386
+ input=input,
4387
+ stream=stream,
4388
+ markdown=markdown,
4389
+ show_time=show_time,
4390
+ show_step_details=show_step_details,
4391
+ user_id=user_id,
4392
+ session_id=session_id,
4393
+ **kwargs,
4394
+ )
4395
+
4396
+ _exit_on = exit_on or ["exit", "quit", "bye", "stop"]
4397
+ while True:
4398
+ message = Prompt.ask(f"[bold] {emoji} {user} [/bold]")
4399
+ if message in _exit_on:
4400
+ break
4401
+
4402
+ await self.aprint_response(
4403
+ input=message,
4404
+ stream=stream,
4405
+ markdown=markdown,
4406
+ show_time=show_time,
4407
+ show_step_details=show_step_details,
4408
+ user_id=user_id,
4409
+ session_id=session_id,
4410
+ **kwargs,
4411
+ )