agno 2.4.6__py3-none-any.whl → 2.4.8__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 (51) hide show
  1. agno/agent/agent.py +5 -1
  2. agno/db/base.py +2 -0
  3. agno/db/postgres/postgres.py +5 -5
  4. agno/db/singlestore/singlestore.py +4 -5
  5. agno/db/sqlite/sqlite.py +4 -4
  6. agno/knowledge/embedder/aws_bedrock.py +325 -106
  7. agno/knowledge/knowledge.py +83 -1853
  8. agno/knowledge/loaders/__init__.py +29 -0
  9. agno/knowledge/loaders/azure_blob.py +423 -0
  10. agno/knowledge/loaders/base.py +187 -0
  11. agno/knowledge/loaders/gcs.py +267 -0
  12. agno/knowledge/loaders/github.py +415 -0
  13. agno/knowledge/loaders/s3.py +281 -0
  14. agno/knowledge/loaders/sharepoint.py +439 -0
  15. agno/knowledge/reader/website_reader.py +2 -2
  16. agno/knowledge/remote_knowledge.py +151 -0
  17. agno/knowledge/reranker/aws_bedrock.py +299 -0
  18. agno/learn/machine.py +5 -6
  19. agno/learn/stores/session_context.py +10 -2
  20. agno/models/azure/openai_chat.py +6 -11
  21. agno/models/neosantara/__init__.py +5 -0
  22. agno/models/neosantara/neosantara.py +42 -0
  23. agno/models/utils.py +5 -0
  24. agno/os/app.py +4 -1
  25. agno/os/interfaces/agui/router.py +1 -1
  26. agno/os/routers/components/components.py +2 -0
  27. agno/os/routers/knowledge/knowledge.py +0 -1
  28. agno/os/routers/registry/registry.py +340 -192
  29. agno/os/routers/workflows/router.py +7 -1
  30. agno/os/schema.py +104 -0
  31. agno/registry/registry.py +4 -0
  32. agno/run/workflow.py +3 -0
  33. agno/session/workflow.py +1 -1
  34. agno/skills/utils.py +100 -2
  35. agno/team/team.py +6 -3
  36. agno/tools/mcp/mcp.py +26 -1
  37. agno/vectordb/lancedb/lance_db.py +22 -7
  38. agno/workflow/__init__.py +4 -0
  39. agno/workflow/cel.py +299 -0
  40. agno/workflow/condition.py +280 -58
  41. agno/workflow/loop.py +177 -46
  42. agno/workflow/parallel.py +75 -4
  43. agno/workflow/router.py +260 -44
  44. agno/workflow/step.py +14 -7
  45. agno/workflow/steps.py +43 -0
  46. agno/workflow/workflow.py +104 -46
  47. {agno-2.4.6.dist-info → agno-2.4.8.dist-info}/METADATA +25 -37
  48. {agno-2.4.6.dist-info → agno-2.4.8.dist-info}/RECORD +51 -39
  49. {agno-2.4.6.dist-info → agno-2.4.8.dist-info}/WHEEL +0 -0
  50. {agno-2.4.6.dist-info → agno-2.4.8.dist-info}/licenses/LICENSE +0 -0
  51. {agno-2.4.6.dist-info → agno-2.4.8.dist-info}/top_level.txt +0 -0
@@ -3,6 +3,7 @@ from dataclasses import dataclass
3
3
  from typing import Any, AsyncIterator, Awaitable, Callable, Dict, Iterator, List, Optional, Union
4
4
  from uuid import uuid4
5
5
 
6
+ from agno.registry import Registry
6
7
  from agno.run.agent import RunOutputEvent
7
8
  from agno.run.base import RunContext
8
9
  from agno.run.team import TeamRunOutputEvent
@@ -14,9 +15,14 @@ from agno.run.workflow import (
14
15
  )
15
16
  from agno.session.workflow import WorkflowSession
16
17
  from agno.utils.log import log_debug, logger
18
+ from agno.workflow.cel import CEL_AVAILABLE, evaluate_cel_condition_evaluator, is_cel_expression
17
19
  from agno.workflow.step import Step
18
20
  from agno.workflow.types import StepInput, StepOutput, StepType
19
21
 
22
+ # Constants for condition branch identifiers
23
+ CONDITION_BRANCH_IF = "if"
24
+ CONDITION_BRANCH_ELSE = "else"
25
+
20
26
  WorkflowSteps = List[
21
27
  Union[
22
28
  Callable[
@@ -34,19 +40,130 @@ WorkflowSteps = List[
34
40
 
35
41
  @dataclass
36
42
  class Condition:
37
- """A condition that executes a step (or list of steps) if the condition is met"""
43
+ """A condition that executes a step (or list of steps) if the condition is met.
44
+
45
+ If the condition evaluates to True, the `steps` are executed.
46
+ If the condition evaluates to False and `else_steps` is provided (and not empty),
47
+ the `else_steps` are executed instead.
48
+
49
+ The evaluator can be:
50
+ - A callable function that returns bool
51
+ - A boolean literal (True/False)
52
+ - A CEL (Common Expression Language) expression string
53
+
54
+ CEL expressions have access to these variables:
55
+ - input: The workflow input as a string
56
+ - previous_step_content: Content from the previous step
57
+ - previous_step_outputs: Map of step name to content string from all previous steps
58
+ - additional_data: Map of additional data passed to the workflow
59
+ - session_state: Map of session state values
60
+
61
+ Example CEL expressions:
62
+ - 'input.contains("urgent")'
63
+ - 'session_state.retry_count < 3'
64
+ - 'additional_data.priority > 5'
65
+ - 'previous_step_outputs.research.contains("error")'
66
+ """
38
67
 
39
68
  # Evaluator should only return boolean
69
+ # Can be a callable, a bool, or a CEL expression string
40
70
  evaluator: Union[
41
71
  Callable[[StepInput], bool],
42
72
  Callable[[StepInput], Awaitable[bool]],
43
73
  bool,
74
+ str, # CEL expression
44
75
  ]
45
76
  steps: WorkflowSteps
46
77
 
78
+ # Steps to execute when condition is False (optional)
79
+ else_steps: Optional[WorkflowSteps] = None
80
+
47
81
  name: Optional[str] = None
48
82
  description: Optional[str] = None
49
83
 
84
+ def to_dict(self) -> Dict[str, Any]:
85
+ result: Dict[str, Any] = {
86
+ "type": "Condition",
87
+ "name": self.name,
88
+ "description": self.description,
89
+ "steps": [step.to_dict() for step in self.steps if hasattr(step, "to_dict")],
90
+ "else_steps": [step.to_dict() for step in (self.else_steps or []) if hasattr(step, "to_dict")],
91
+ }
92
+ if callable(self.evaluator):
93
+ result["evaluator"] = self.evaluator.__name__
94
+ result["evaluator_type"] = "function"
95
+ elif isinstance(self.evaluator, bool):
96
+ result["evaluator"] = self.evaluator
97
+ result["evaluator_type"] = "bool"
98
+ elif isinstance(self.evaluator, str):
99
+ # CEL expression string
100
+ result["evaluator"] = self.evaluator
101
+ result["evaluator_type"] = "cel"
102
+ else:
103
+ raise ValueError(f"Invalid evaluator type: {type(self.evaluator).__name__}")
104
+
105
+ return result
106
+
107
+ @classmethod
108
+ def from_dict(
109
+ cls,
110
+ data: Dict[str, Any],
111
+ registry: Optional["Registry"] = None,
112
+ db: Optional[Any] = None,
113
+ links: Optional[List[Dict[str, Any]]] = None,
114
+ ) -> "Condition":
115
+ from agno.workflow.loop import Loop
116
+ from agno.workflow.parallel import Parallel
117
+ from agno.workflow.router import Router
118
+ from agno.workflow.steps import Steps
119
+
120
+ def deserialize_step(step_data: Dict[str, Any]) -> Any:
121
+ step_type = step_data.get("type", "Step")
122
+ if step_type == "Loop":
123
+ return Loop.from_dict(step_data, registry=registry, db=db, links=links)
124
+ elif step_type == "Parallel":
125
+ return Parallel.from_dict(step_data, registry=registry, db=db, links=links)
126
+ elif step_type == "Steps":
127
+ return Steps.from_dict(step_data, registry=registry, db=db, links=links)
128
+ elif step_type == "Condition":
129
+ return cls.from_dict(step_data, registry=registry, db=db, links=links)
130
+ elif step_type == "Router":
131
+ return Router.from_dict(step_data, registry=registry, db=db, links=links)
132
+ else:
133
+ return Step.from_dict(step_data, registry=registry, db=db, links=links)
134
+
135
+ evaluator_data = data.get("evaluator", True)
136
+ evaluator_type = data.get("evaluator_type")
137
+ evaluator: Union[Callable[[StepInput], bool], Callable[[StepInput], Awaitable[bool]], bool, str]
138
+
139
+ if isinstance(evaluator_data, bool):
140
+ evaluator = evaluator_data
141
+ elif isinstance(evaluator_data, str):
142
+ # Determine if this is a CEL expression or a function name
143
+ # Use evaluator_type if provided, otherwise detect
144
+ if evaluator_type == "cel" or (evaluator_type is None and is_cel_expression(evaluator_data)):
145
+ # CEL expression - use as-is
146
+ evaluator = evaluator_data
147
+ else:
148
+ # Function name - look up in registry
149
+ if registry:
150
+ func = registry.get_function(evaluator_data)
151
+ if func is None:
152
+ raise ValueError(f"Evaluator function '{evaluator_data}' not found in registry")
153
+ evaluator = func
154
+ else:
155
+ raise ValueError(f"Registry required to deserialize evaluator function '{evaluator_data}'")
156
+ else:
157
+ raise ValueError(f"Invalid evaluator type in data: {type(evaluator_data).__name__}")
158
+
159
+ return cls(
160
+ evaluator=evaluator,
161
+ steps=[deserialize_step(step) for step in data.get("steps", [])],
162
+ else_steps=[deserialize_step(step) for step in data.get("else_steps", [])],
163
+ name=data.get("name"),
164
+ description=data.get("description"),
165
+ )
166
+
50
167
  def _prepare_steps(self):
51
168
  """Prepare the steps for execution - mirrors workflow logic"""
52
169
  from agno.agent.agent import Agent
@@ -57,20 +174,27 @@ class Condition:
57
174
  from agno.workflow.step import Step
58
175
  from agno.workflow.steps import Steps
59
176
 
60
- prepared_steps: WorkflowSteps = []
61
- for step in self.steps:
62
- if callable(step) and hasattr(step, "__name__"):
63
- prepared_steps.append(Step(name=step.__name__, description="User-defined callable step", executor=step))
64
- elif isinstance(step, Agent):
65
- prepared_steps.append(Step(name=step.name, description=step.description, agent=step))
66
- elif isinstance(step, Team):
67
- prepared_steps.append(Step(name=step.name, description=step.description, team=step))
68
- elif isinstance(step, (Step, Steps, Loop, Parallel, Condition, Router)):
69
- prepared_steps.append(step)
70
- else:
71
- raise ValueError(f"Invalid step type: {type(step).__name__}")
177
+ def prepare_step_list(steps: WorkflowSteps) -> WorkflowSteps:
178
+ """Helper to prepare a list of steps."""
179
+ prepared: WorkflowSteps = []
180
+ for step in steps:
181
+ if callable(step) and hasattr(step, "__name__"):
182
+ prepared.append(Step(name=step.__name__, description="User-defined callable step", executor=step))
183
+ elif isinstance(step, Agent):
184
+ prepared.append(Step(name=step.name, description=step.description, agent=step))
185
+ elif isinstance(step, Team):
186
+ prepared.append(Step(name=step.name, description=step.description, team=step))
187
+ elif isinstance(step, (Step, Steps, Loop, Parallel, Condition, Router)):
188
+ prepared.append(step)
189
+ else:
190
+ raise ValueError(f"Invalid step type: {type(step).__name__}")
191
+ return prepared
192
+
193
+ self.steps = prepare_step_list(self.steps)
72
194
 
73
- self.steps = prepared_steps
195
+ # Also prepare else_steps if provided and not empty
196
+ if self.else_steps and len(self.else_steps) > 0:
197
+ self.else_steps = prepare_step_list(self.else_steps)
74
198
 
75
199
  def _update_step_input_from_outputs(
76
200
  self,
@@ -113,10 +237,29 @@ class Condition:
113
237
  )
114
238
 
115
239
  def _evaluate_condition(self, step_input: StepInput, session_state: Optional[Dict[str, Any]] = None) -> bool:
116
- """Evaluate the condition and return boolean result"""
240
+ """Evaluate the condition and return boolean result.
241
+
242
+ Supports:
243
+ - Boolean literals (True/False)
244
+ - Callable functions
245
+ - CEL expression strings
246
+ """
117
247
  if isinstance(self.evaluator, bool):
118
248
  return self.evaluator
119
249
 
250
+ if isinstance(self.evaluator, str):
251
+ # CEL expression
252
+ if not CEL_AVAILABLE:
253
+ logger.error(
254
+ "CEL expression used but cel-python is not installed. Install with: pip install cel-python"
255
+ )
256
+ return False
257
+ try:
258
+ return evaluate_cel_condition_evaluator(self.evaluator, step_input, session_state)
259
+ except Exception as e:
260
+ logger.error(f"CEL expression evaluation failed: {e}")
261
+ return False
262
+
120
263
  if callable(self.evaluator):
121
264
  if session_state is not None and self._evaluator_has_session_state_param():
122
265
  result = self.evaluator(step_input, session_state=session_state) # type: ignore[call-arg]
@@ -132,10 +275,29 @@ class Condition:
132
275
  return False
133
276
 
134
277
  async def _aevaluate_condition(self, step_input: StepInput, session_state: Optional[Dict[str, Any]] = None) -> bool:
135
- """Async version of condition evaluation"""
278
+ """Async version of condition evaluation.
279
+
280
+ Supports:
281
+ - Boolean literals (True/False)
282
+ - Callable functions (sync and async)
283
+ - CEL expression strings
284
+ """
136
285
  if isinstance(self.evaluator, bool):
137
286
  return self.evaluator
138
287
 
288
+ if isinstance(self.evaluator, str):
289
+ # CEL expression - CEL evaluation is synchronous
290
+ if not CEL_AVAILABLE:
291
+ logger.error(
292
+ "CEL expression used but cel-python is not installed. Install with: pip install cel-python"
293
+ )
294
+ return False
295
+ try:
296
+ return evaluate_cel_condition_evaluator(self.evaluator, step_input, session_state)
297
+ except Exception as e:
298
+ logger.error(f"CEL expression evaluation failed: {e}")
299
+ return False
300
+
139
301
  if callable(self.evaluator):
140
302
  has_session_state = session_state is not None and self._evaluator_has_session_state_param()
141
303
 
@@ -169,6 +331,10 @@ class Condition:
169
331
  except Exception:
170
332
  return False
171
333
 
334
+ def _has_else_steps(self) -> bool:
335
+ """Check if else_steps is provided and not empty."""
336
+ return self.else_steps is not None and len(self.else_steps) > 0
337
+
172
338
  def execute(
173
339
  self,
174
340
  step_input: StepInput,
@@ -183,7 +349,12 @@ class Condition:
183
349
  num_history_runs: int = 3,
184
350
  background_tasks: Optional[Any] = None,
185
351
  ) -> StepOutput:
186
- """Execute the condition and its steps with sequential chaining if condition is true"""
352
+ """Execute the condition and its steps with sequential chaining.
353
+
354
+ If condition is True, executes `steps`.
355
+ If condition is False and `else_steps` is provided (and not empty), executes `else_steps`.
356
+ If condition is False and no `else_steps`, returns a "not met" message.
357
+ """
187
358
  log_debug(f"Condition Start: {self.name}", center=True, symbol="-")
188
359
 
189
360
  conditional_step_id = str(uuid4())
@@ -198,7 +369,17 @@ class Condition:
198
369
 
199
370
  log_debug(f"Condition {self.name} evaluated to: {condition_result}")
200
371
 
201
- if not condition_result:
372
+ # Determine which steps to execute
373
+ if condition_result:
374
+ steps_to_execute = self.steps
375
+ branch = CONDITION_BRANCH_IF
376
+ log_debug(f"Condition {self.name} met, executing {len(steps_to_execute)} steps (if branch)")
377
+ elif self._has_else_steps():
378
+ steps_to_execute = self.else_steps # type: ignore[assignment]
379
+ branch = CONDITION_BRANCH_ELSE
380
+ log_debug(f"Condition {self.name} not met, executing {len(steps_to_execute)} else_steps (else branch)")
381
+ else:
382
+ # No else_steps provided, return "not met" message
202
383
  log_debug(f"Condition {self.name} not met, skipping {len(self.steps)} steps")
203
384
  return StepOutput(
204
385
  step_name=self.name,
@@ -208,12 +389,11 @@ class Condition:
208
389
  success=True,
209
390
  )
210
391
 
211
- log_debug(f"Condition {self.name} met, executing {len(self.steps)} steps")
212
392
  all_results: List[StepOutput] = []
213
393
  current_step_input = step_input
214
- condition_step_outputs = {}
394
+ condition_step_outputs: Dict[str, StepOutput] = {}
215
395
 
216
- for i, step in enumerate(self.steps):
396
+ for i, step in enumerate(steps_to_execute):
217
397
  try:
218
398
  step_output = step.execute( # type: ignore[union-attr]
219
399
  current_step_input,
@@ -234,7 +414,7 @@ class Condition:
234
414
  all_results.extend(step_output)
235
415
  if step_output:
236
416
  step_name = getattr(step, "name", f"step_{i}")
237
- log_debug(f"Executing condition step {i + 1}/{len(self.steps)}: {step_name}")
417
+ log_debug(f"Executing condition step {i + 1}/{len(steps_to_execute)}: {step_name}")
238
418
 
239
419
  condition_step_outputs[step_name] = step_output[-1]
240
420
 
@@ -269,13 +449,13 @@ class Condition:
269
449
  all_results.append(error_output)
270
450
  break
271
451
 
272
- log_debug(f"Condition End: {self.name} ({len(all_results)} results)", center=True, symbol="-")
452
+ log_debug(f"Condition End: {self.name} ({len(all_results)} results, {branch} branch)", center=True, symbol="-")
273
453
 
274
454
  return StepOutput(
275
455
  step_name=self.name,
276
456
  step_id=conditional_step_id,
277
457
  step_type=StepType.CONDITION,
278
- content=f"Condition {self.name} completed with {len(all_results)} results",
458
+ content=f"Condition {self.name} completed with {len(all_results)} results ({branch} branch)",
279
459
  success=all(result.success for result in all_results) if all_results else True,
280
460
  error=None,
281
461
  stop=any(result.stop for result in all_results) if all_results else False,
@@ -300,7 +480,12 @@ class Condition:
300
480
  num_history_runs: int = 3,
301
481
  background_tasks: Optional[Any] = None,
302
482
  ) -> Iterator[Union[WorkflowRunOutputEvent, StepOutput]]:
303
- """Execute the condition with streaming support - mirrors Loop logic"""
483
+ """Execute the condition with streaming support.
484
+
485
+ If condition is True, executes `steps`.
486
+ If condition is False and `else_steps` is provided (and not empty), executes `else_steps`.
487
+ If condition is False and no `else_steps`, yields completed event and returns.
488
+ """
304
489
  log_debug(f"Condition Start: {self.name}", center=True, symbol="-")
305
490
 
306
491
  conditional_step_id = str(uuid4())
@@ -328,9 +513,18 @@ class Condition:
328
513
  parent_step_id=parent_step_id,
329
514
  )
330
515
 
331
- if not condition_result:
516
+ # Determine which steps to execute
517
+ if condition_result:
518
+ steps_to_execute = self.steps
519
+ branch = CONDITION_BRANCH_IF
520
+ log_debug(f"Condition {self.name} met, executing {len(steps_to_execute)} steps (if branch)")
521
+ elif self._has_else_steps():
522
+ steps_to_execute = self.else_steps # type: ignore[assignment]
523
+ branch = CONDITION_BRANCH_ELSE
524
+ log_debug(f"Condition {self.name} not met, executing {len(steps_to_execute)} else_steps (else branch)")
525
+ else:
526
+ # No else_steps provided, yield completed event and return
332
527
  if stream_events and workflow_run_response:
333
- # Yield condition completed event for empty case
334
528
  yield ConditionExecutionCompletedEvent(
335
529
  run_id=workflow_run_response.run_id or "",
336
530
  workflow_name=workflow_run_response.workflow_name or "",
@@ -340,20 +534,20 @@ class Condition:
340
534
  step_index=step_index,
341
535
  condition_result=False,
342
536
  executed_steps=0,
537
+ branch=None,
343
538
  step_results=[],
344
539
  step_id=conditional_step_id,
345
540
  parent_step_id=parent_step_id,
346
541
  )
347
542
  return
348
543
 
349
- log_debug(f"Condition {self.name} met, executing {len(self.steps)} steps")
350
- all_results = []
544
+ all_results: List[StepOutput] = []
351
545
  current_step_input = step_input
352
- condition_step_outputs = {}
546
+ condition_step_outputs: Dict[str, StepOutput] = {}
353
547
 
354
- for i, step in enumerate(self.steps):
548
+ for i, step in enumerate(steps_to_execute):
355
549
  try:
356
- step_outputs_for_step = []
550
+ step_outputs_for_step: List[StepOutput] = []
357
551
 
358
552
  # Create child index for each step within condition
359
553
  if step_index is None or isinstance(step_index, int):
@@ -426,7 +620,7 @@ class Condition:
426
620
  all_results.append(error_output)
427
621
  break
428
622
 
429
- log_debug(f"Condition End: {self.name} ({len(all_results)} results)", center=True, symbol="-")
623
+ log_debug(f"Condition End: {self.name} ({len(all_results)} results, {branch} branch)", center=True, symbol="-")
430
624
  if stream_events and workflow_run_response:
431
625
  # Yield condition completed event
432
626
  yield ConditionExecutionCompletedEvent(
@@ -436,8 +630,9 @@ class Condition:
436
630
  session_id=workflow_run_response.session_id or "",
437
631
  step_name=self.name,
438
632
  step_index=step_index,
439
- condition_result=True,
440
- executed_steps=len(self.steps),
633
+ condition_result=condition_result,
634
+ executed_steps=len(steps_to_execute),
635
+ branch=branch,
441
636
  step_results=all_results,
442
637
  step_id=conditional_step_id,
443
638
  parent_step_id=parent_step_id,
@@ -447,7 +642,7 @@ class Condition:
447
642
  step_name=self.name,
448
643
  step_id=conditional_step_id,
449
644
  step_type=StepType.CONDITION,
450
- content=f"Condition {self.name} completed with {len(all_results)} results",
645
+ content=f"Condition {self.name} completed with {len(all_results)} results ({branch} branch)",
451
646
  success=all(result.success for result in all_results) if all_results else True,
452
647
  stop=any(result.stop for result in all_results) if all_results else False,
453
648
  steps=all_results,
@@ -467,7 +662,12 @@ class Condition:
467
662
  num_history_runs: int = 3,
468
663
  background_tasks: Optional[Any] = None,
469
664
  ) -> StepOutput:
470
- """Async execute the condition and its steps with sequential chaining"""
665
+ """Async execute the condition and its steps with sequential chaining.
666
+
667
+ If condition is True, executes `steps`.
668
+ If condition is False and `else_steps` is provided (and not empty), executes `else_steps`.
669
+ If condition is False and no `else_steps`, returns a "not met" message.
670
+ """
471
671
  log_debug(f"Condition Start: {self.name}", center=True, symbol="-")
472
672
 
473
673
  conditional_step_id = str(uuid4())
@@ -481,24 +681,32 @@ class Condition:
481
681
  condition_result = await self._aevaluate_condition(step_input, session_state=session_state)
482
682
  log_debug(f"Condition {self.name} evaluated to: {condition_result}")
483
683
 
484
- if not condition_result:
684
+ # Determine which steps to execute
685
+ if condition_result:
686
+ steps_to_execute = self.steps
687
+ branch = CONDITION_BRANCH_IF
688
+ log_debug(f"Condition {self.name} met, executing {len(steps_to_execute)} steps (if branch)")
689
+ elif self._has_else_steps():
690
+ steps_to_execute = self.else_steps # type: ignore[assignment]
691
+ branch = CONDITION_BRANCH_ELSE
692
+ log_debug(f"Condition {self.name} not met, executing {len(steps_to_execute)} else_steps (else branch)")
693
+ else:
694
+ # No else_steps provided, return "not met" message
485
695
  log_debug(f"Condition {self.name} not met, skipping {len(self.steps)} steps")
486
696
  return StepOutput(
487
697
  step_name=self.name,
488
- step_id=str(uuid4()),
698
+ step_id=conditional_step_id,
489
699
  step_type=StepType.CONDITION,
490
700
  content=f"Condition {self.name} not met - skipped {len(self.steps)} steps",
491
701
  success=True,
492
702
  )
493
703
 
494
- log_debug(f"Condition {self.name} met, executing {len(self.steps)} steps")
495
-
496
704
  # Chain steps sequentially like Loop does
497
705
  all_results: List[StepOutput] = []
498
706
  current_step_input = step_input
499
- condition_step_outputs = {}
707
+ condition_step_outputs: Dict[str, StepOutput] = {}
500
708
 
501
- for i, step in enumerate(self.steps):
709
+ for i, step in enumerate(steps_to_execute):
502
710
  try:
503
711
  step_output = await step.aexecute( # type: ignore[union-attr]
504
712
  current_step_input,
@@ -552,13 +760,13 @@ class Condition:
552
760
  all_results.append(error_output)
553
761
  break
554
762
 
555
- log_debug(f"Condition End: {self.name} ({len(all_results)} results)", center=True, symbol="-")
763
+ log_debug(f"Condition End: {self.name} ({len(all_results)} results, {branch} branch)", center=True, symbol="-")
556
764
 
557
765
  return StepOutput(
558
766
  step_name=self.name,
559
767
  step_id=conditional_step_id,
560
768
  step_type=StepType.CONDITION,
561
- content=f"Condition {self.name} completed with {len(all_results)} results",
769
+ content=f"Condition {self.name} completed with {len(all_results)} results ({branch} branch)",
562
770
  success=all(result.success for result in all_results) if all_results else True,
563
771
  error=None,
564
772
  stop=any(result.stop for result in all_results) if all_results else False,
@@ -583,7 +791,12 @@ class Condition:
583
791
  num_history_runs: int = 3,
584
792
  background_tasks: Optional[Any] = None,
585
793
  ) -> AsyncIterator[Union[WorkflowRunOutputEvent, TeamRunOutputEvent, RunOutputEvent, StepOutput]]:
586
- """Async execute the condition with streaming support - mirrors Loop logic"""
794
+ """Async execute the condition with streaming support.
795
+
796
+ If condition is True, executes `steps`.
797
+ If condition is False and `else_steps` is provided (and not empty), executes `else_steps`.
798
+ If condition is False and no `else_steps`, yields completed event and returns.
799
+ """
587
800
  log_debug(f"Condition Start: {self.name}", center=True, symbol="-")
588
801
 
589
802
  conditional_step_id = str(uuid4())
@@ -611,9 +824,18 @@ class Condition:
611
824
  parent_step_id=parent_step_id,
612
825
  )
613
826
 
614
- if not condition_result:
827
+ # Determine which steps to execute
828
+ if condition_result:
829
+ steps_to_execute = self.steps
830
+ branch = CONDITION_BRANCH_IF
831
+ log_debug(f"Condition {self.name} met, executing {len(steps_to_execute)} steps (if branch)")
832
+ elif self._has_else_steps():
833
+ steps_to_execute = self.else_steps # type: ignore[assignment]
834
+ branch = CONDITION_BRANCH_ELSE
835
+ log_debug(f"Condition {self.name} not met, executing {len(steps_to_execute)} else_steps (else branch)")
836
+ else:
837
+ # No else_steps provided, yield completed event and return
615
838
  if stream_events and workflow_run_response:
616
- # Yield condition completed event for empty case
617
839
  yield ConditionExecutionCompletedEvent(
618
840
  run_id=workflow_run_response.run_id or "",
619
841
  workflow_name=workflow_run_response.workflow_name or "",
@@ -623,22 +845,21 @@ class Condition:
623
845
  step_index=step_index,
624
846
  condition_result=False,
625
847
  executed_steps=0,
848
+ branch=None,
626
849
  step_results=[],
627
850
  step_id=conditional_step_id,
628
851
  parent_step_id=parent_step_id,
629
852
  )
630
853
  return
631
854
 
632
- log_debug(f"Condition {self.name} met, executing {len(self.steps)} steps")
633
-
634
855
  # Chain steps sequentially like Loop does
635
- all_results = []
856
+ all_results: List[StepOutput] = []
636
857
  current_step_input = step_input
637
- condition_step_outputs = {}
858
+ condition_step_outputs: Dict[str, StepOutput] = {}
638
859
 
639
- for i, step in enumerate(self.steps):
860
+ for i, step in enumerate(steps_to_execute):
640
861
  try:
641
- step_outputs_for_step = []
862
+ step_outputs_for_step: List[StepOutput] = []
642
863
 
643
864
  # Create child index for each step within condition
644
865
  if step_index is None or isinstance(step_index, int):
@@ -711,7 +932,7 @@ class Condition:
711
932
  all_results.append(error_output)
712
933
  break
713
934
 
714
- log_debug(f"Condition End: {self.name} ({len(all_results)} results)", center=True, symbol="-")
935
+ log_debug(f"Condition End: {self.name} ({len(all_results)} results, {branch} branch)", center=True, symbol="-")
715
936
 
716
937
  if stream_events and workflow_run_response:
717
938
  # Yield condition completed event
@@ -722,8 +943,9 @@ class Condition:
722
943
  session_id=workflow_run_response.session_id or "",
723
944
  step_name=self.name,
724
945
  step_index=step_index,
725
- condition_result=True,
726
- executed_steps=len(self.steps),
946
+ condition_result=condition_result,
947
+ executed_steps=len(steps_to_execute),
948
+ branch=branch,
727
949
  step_results=all_results,
728
950
  step_id=conditional_step_id,
729
951
  parent_step_id=parent_step_id,
@@ -733,7 +955,7 @@ class Condition:
733
955
  step_name=self.name,
734
956
  step_id=conditional_step_id,
735
957
  step_type=StepType.CONDITION,
736
- content=f"Condition {self.name} completed with {len(all_results)} results",
958
+ content=f"Condition {self.name} completed with {len(all_results)} results ({branch} branch)",
737
959
  success=all(result.success for result in all_results) if all_results else True,
738
960
  stop=any(result.stop for result in all_results) if all_results else False,
739
961
  steps=all_results,