agno 2.0.0a1__py3-none-any.whl → 2.0.0rc2__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 (79) hide show
  1. agno/agent/agent.py +416 -41
  2. agno/api/agent.py +2 -2
  3. agno/api/evals.py +2 -2
  4. agno/api/os.py +1 -1
  5. agno/api/settings.py +2 -2
  6. agno/api/team.py +2 -2
  7. agno/db/dynamo/dynamo.py +0 -6
  8. agno/db/firestore/firestore.py +0 -6
  9. agno/db/in_memory/in_memory_db.py +0 -6
  10. agno/db/json/json_db.py +0 -6
  11. agno/db/mongo/mongo.py +8 -9
  12. agno/db/mysql/utils.py +0 -1
  13. agno/db/postgres/postgres.py +0 -10
  14. agno/db/postgres/utils.py +0 -1
  15. agno/db/redis/redis.py +0 -4
  16. agno/db/singlestore/singlestore.py +0 -10
  17. agno/db/singlestore/utils.py +0 -1
  18. agno/db/sqlite/sqlite.py +0 -4
  19. agno/db/sqlite/utils.py +0 -1
  20. agno/eval/accuracy.py +12 -5
  21. agno/integrations/discord/client.py +5 -1
  22. agno/knowledge/chunking/strategy.py +14 -14
  23. agno/knowledge/embedder/aws_bedrock.py +2 -2
  24. agno/knowledge/knowledge.py +156 -120
  25. agno/knowledge/reader/arxiv_reader.py +5 -5
  26. agno/knowledge/reader/csv_reader.py +6 -77
  27. agno/knowledge/reader/docx_reader.py +5 -5
  28. agno/knowledge/reader/firecrawl_reader.py +5 -5
  29. agno/knowledge/reader/json_reader.py +5 -5
  30. agno/knowledge/reader/markdown_reader.py +31 -9
  31. agno/knowledge/reader/pdf_reader.py +10 -123
  32. agno/knowledge/reader/reader_factory.py +65 -72
  33. agno/knowledge/reader/s3_reader.py +44 -114
  34. agno/knowledge/reader/text_reader.py +5 -5
  35. agno/knowledge/reader/url_reader.py +75 -31
  36. agno/knowledge/reader/web_search_reader.py +6 -29
  37. agno/knowledge/reader/website_reader.py +5 -5
  38. agno/knowledge/reader/wikipedia_reader.py +5 -5
  39. agno/knowledge/reader/youtube_reader.py +6 -6
  40. agno/knowledge/utils.py +10 -10
  41. agno/models/anthropic/claude.py +2 -49
  42. agno/models/aws/bedrock.py +3 -7
  43. agno/models/base.py +37 -6
  44. agno/models/message.py +7 -6
  45. agno/os/app.py +168 -64
  46. agno/os/interfaces/agui/agui.py +1 -1
  47. agno/os/interfaces/agui/utils.py +16 -9
  48. agno/os/interfaces/slack/slack.py +2 -3
  49. agno/os/interfaces/whatsapp/whatsapp.py +2 -3
  50. agno/os/mcp.py +235 -0
  51. agno/os/router.py +576 -19
  52. agno/os/routers/evals/evals.py +201 -12
  53. agno/os/routers/knowledge/knowledge.py +455 -18
  54. agno/os/routers/memory/memory.py +260 -29
  55. agno/os/routers/metrics/metrics.py +127 -7
  56. agno/os/routers/session/session.py +398 -25
  57. agno/os/schema.py +55 -2
  58. agno/os/settings.py +0 -1
  59. agno/run/agent.py +96 -2
  60. agno/run/cancel.py +0 -2
  61. agno/run/team.py +93 -2
  62. agno/run/workflow.py +25 -12
  63. agno/team/team.py +863 -1053
  64. agno/tools/function.py +65 -7
  65. agno/tools/linear.py +1 -1
  66. agno/tools/mcp.py +1 -2
  67. agno/utils/gemini.py +31 -1
  68. agno/utils/log.py +52 -2
  69. agno/utils/mcp.py +55 -3
  70. agno/utils/models/claude.py +41 -0
  71. agno/utils/print_response/team.py +177 -73
  72. agno/utils/streamlit.py +481 -0
  73. agno/workflow/workflow.py +17 -1
  74. {agno-2.0.0a1.dist-info → agno-2.0.0rc2.dist-info}/METADATA +1 -1
  75. {agno-2.0.0a1.dist-info → agno-2.0.0rc2.dist-info}/RECORD +78 -77
  76. agno/knowledge/reader/gcs_reader.py +0 -67
  77. {agno-2.0.0a1.dist-info → agno-2.0.0rc2.dist-info}/WHEEL +0 -0
  78. {agno-2.0.0a1.dist-info → agno-2.0.0rc2.dist-info}/licenses/LICENSE +0 -0
  79. {agno-2.0.0a1.dist-info → agno-2.0.0rc2.dist-info}/top_level.txt +0 -0
agno/run/agent.py CHANGED
@@ -1,11 +1,11 @@
1
1
  from dataclasses import asdict, dataclass, field
2
2
  from enum import Enum
3
3
  from time import time
4
- from typing import Any, Dict, List, Optional, Union
4
+ from typing import Any, Dict, List, Optional, Sequence, Union
5
5
 
6
6
  from pydantic import BaseModel
7
7
 
8
- from agno.media import AudioArtifact, AudioResponse, ImageArtifact, VideoArtifact
8
+ from agno.media import AudioArtifact, AudioResponse, File, ImageArtifact, VideoArtifact
9
9
  from agno.models.message import Citations, Message
10
10
  from agno.models.metrics import Metrics
11
11
  from agno.models.response import ToolExecution
@@ -43,6 +43,8 @@ class RunEvent(str, Enum):
43
43
  output_model_response_started = "OutputModelResponseStarted"
44
44
  output_model_response_completed = "OutputModelResponseCompleted"
45
45
 
46
+ custom_event = "CustomEvent"
47
+
46
48
 
47
49
  @dataclass
48
50
  class BaseAgentRunEvent(BaseRunOutputEvent):
@@ -226,6 +228,11 @@ class OutputModelResponseCompletedEvent(BaseAgentRunEvent):
226
228
  event: str = RunEvent.output_model_response_completed.value
227
229
 
228
230
 
231
+ @dataclass
232
+ class CustomEvent(BaseAgentRunEvent):
233
+ event: str = RunEvent.custom_event.value
234
+
235
+
229
236
  RunOutputEvent = Union[
230
237
  RunStartedEvent,
231
238
  RunContentEvent,
@@ -246,6 +253,7 @@ RunOutputEvent = Union[
246
253
  ParserModelResponseCompletedEvent,
247
254
  OutputModelResponseStartedEvent,
248
255
  OutputModelResponseCompletedEvent,
256
+ CustomEvent,
249
257
  ]
250
258
 
251
259
 
@@ -270,6 +278,7 @@ RUN_EVENT_TYPE_REGISTRY = {
270
278
  RunEvent.parser_model_response_completed.value: ParserModelResponseCompletedEvent,
271
279
  RunEvent.output_model_response_started.value: OutputModelResponseStartedEvent,
272
280
  RunEvent.output_model_response_completed.value: OutputModelResponseCompletedEvent,
281
+ RunEvent.custom_event.value: CustomEvent,
273
282
  }
274
283
 
275
284
 
@@ -281,6 +290,78 @@ def run_output_event_from_dict(data: dict) -> BaseRunOutputEvent:
281
290
  return cls.from_dict(data) # type: ignore
282
291
 
283
292
 
293
+ @dataclass
294
+ class RunInput:
295
+ """Container for the raw input data passed to Agent.run().
296
+
297
+ This captures the original input exactly as provided by the user,
298
+ separate from the processed messages that go to the model.
299
+
300
+ Attributes:
301
+ input_content: The literal input message/content passed to run()
302
+ images: Images directly passed to run()
303
+ videos: Videos directly passed to run()
304
+ audios: Audio files directly passed to run()
305
+ files: Files directly passed to run()
306
+ """
307
+
308
+ input_content: Optional[Union[str, List, Dict, Message, BaseModel, List[Message]]] = None
309
+ images: Optional[Sequence[ImageArtifact]] = None
310
+ videos: Optional[Sequence[VideoArtifact]] = None
311
+ audios: Optional[Sequence[AudioArtifact]] = None
312
+ files: Optional[Sequence[File]] = None
313
+
314
+ def to_dict(self) -> Dict[str, Any]:
315
+ """Convert to dictionary representation"""
316
+ result: Dict[str, Any] = {}
317
+
318
+ if self.input_content is not None:
319
+ if isinstance(self.input_content, (str)):
320
+ result["input_content"] = self.input_content
321
+ elif isinstance(self.input_content, BaseModel):
322
+ result["input_content"] = self.input_content.model_dump(exclude_none=True)
323
+ elif isinstance(self.input_content, Message):
324
+ result["input_content"] = self.input_content.to_dict()
325
+ elif (
326
+ isinstance(self.input_content, list)
327
+ and self.input_content
328
+ and isinstance(self.input_content[0], Message)
329
+ ):
330
+ result["input_content"] = [m.to_dict() for m in self.input_content]
331
+ else:
332
+ result["input_content"] = self.input_content
333
+
334
+ if self.images:
335
+ result["images"] = [img.to_dict() for img in self.images]
336
+ if self.videos:
337
+ result["videos"] = [vid.to_dict() for vid in self.videos]
338
+ if self.audios:
339
+ result["audios"] = [aud.to_dict() for aud in self.audios]
340
+
341
+ return result
342
+
343
+ @classmethod
344
+ def from_dict(cls, data: Dict[str, Any]) -> "RunInput":
345
+ """Create RunInput from dictionary"""
346
+ images = None
347
+ if data.get("images"):
348
+ images = [ImageArtifact.model_validate(img_data) for img_data in data["images"]]
349
+
350
+ videos = None
351
+ if data.get("videos"):
352
+ videos = [VideoArtifact.model_validate(vid_data) for vid_data in data["videos"]]
353
+
354
+ audios = None
355
+ if data.get("audios"):
356
+ audios = [AudioArtifact.model_validate(aud_data) for aud_data in data["audios"]]
357
+
358
+ files = None
359
+ if data.get("files"):
360
+ files = [File.model_validate(file_data) for file_data in data["files"]]
361
+
362
+ return cls(input_content=data.get("input_content"), images=images, videos=videos, audios=audios, files=files)
363
+
364
+
284
365
  @dataclass
285
366
  class RunOutput:
286
367
  """Response returned by Agent.run() or Workflow.run() functions"""
@@ -313,6 +394,9 @@ class RunOutput:
313
394
  audio: Optional[List[AudioArtifact]] = None # Audio attached to the response
314
395
  response_audio: Optional[AudioResponse] = None # Model audio response
315
396
 
397
+ # Input media and messages from user
398
+ input: Optional[RunInput] = None
399
+
316
400
  citations: Optional[Citations] = None
317
401
  references: Optional[List[MessageReferences]] = None
318
402
 
@@ -363,6 +447,7 @@ class RunOutput:
363
447
  "videos",
364
448
  "audio",
365
449
  "response_audio",
450
+ "input",
366
451
  "citations",
367
452
  "events",
368
453
  "additional_input",
@@ -446,6 +531,9 @@ class RunOutput:
446
531
  else:
447
532
  _dict["tools"].append(tool)
448
533
 
534
+ if self.input is not None:
535
+ _dict["input"] = self.input.to_dict()
536
+
449
537
  return _dict
450
538
 
451
539
  def to_json(self) -> str:
@@ -488,6 +576,11 @@ class RunOutput:
488
576
  response_audio = data.pop("response_audio", None)
489
577
  response_audio = AudioResponse.model_validate(response_audio) if response_audio else None
490
578
 
579
+ input_data = data.pop("input", None)
580
+ input_obj = None
581
+ if input_data:
582
+ input_obj = RunInput.from_dict(input_data)
583
+
491
584
  metrics = data.pop("metrics", None)
492
585
  if metrics:
493
586
  metrics = Metrics(**metrics)
@@ -518,6 +611,7 @@ class RunOutput:
518
611
  audio=audio,
519
612
  videos=videos,
520
613
  response_audio=response_audio,
614
+ input=input_obj,
521
615
  events=events,
522
616
  additional_input=additional_input,
523
617
  reasoning_steps=reasoning_steps,
agno/run/cancel.py CHANGED
@@ -18,7 +18,6 @@ class RunCancellationManager:
18
18
  """Register a new run as not cancelled."""
19
19
  with self._lock:
20
20
  self._cancelled_runs[run_id] = False
21
- logger.debug(f"Registered run {run_id} for cancellation tracking")
22
21
 
23
22
  def cancel_run(self, run_id: str) -> bool:
24
23
  """Cancel a run by marking it as cancelled.
@@ -45,7 +44,6 @@ class RunCancellationManager:
45
44
  with self._lock:
46
45
  if run_id in self._cancelled_runs:
47
46
  del self._cancelled_runs[run_id]
48
- logger.debug(f"Cleaned up cancellation tracking for run {run_id}")
49
47
 
50
48
  def raise_if_cancelled(self, run_id: str) -> None:
51
49
  """Check if a run should be cancelled and raise exception if so."""
agno/run/team.py CHANGED
@@ -1,11 +1,11 @@
1
1
  from dataclasses import asdict, dataclass, field
2
2
  from enum import Enum
3
3
  from time import time
4
- from typing import Any, Dict, List, Optional, Union
4
+ from typing import Any, Dict, List, Optional, Sequence, Union
5
5
 
6
6
  from pydantic import BaseModel
7
7
 
8
- from agno.media import AudioArtifact, AudioResponse, ImageArtifact, VideoArtifact
8
+ from agno.media import AudioArtifact, AudioResponse, File, ImageArtifact, VideoArtifact
9
9
  from agno.models.message import Citations, Message
10
10
  from agno.models.metrics import Metrics
11
11
  from agno.models.response import ToolExecution
@@ -40,6 +40,8 @@ class TeamRunEvent(str, Enum):
40
40
  output_model_response_started = "TeamOutputModelResponseStarted"
41
41
  output_model_response_completed = "TeamOutputModelResponseCompleted"
42
42
 
43
+ custom_event = "CustomEvent"
44
+
43
45
 
44
46
  @dataclass
45
47
  class BaseTeamRunEvent(BaseRunOutputEvent):
@@ -213,6 +215,11 @@ class OutputModelResponseCompletedEvent(BaseTeamRunEvent):
213
215
  event: str = TeamRunEvent.output_model_response_completed.value
214
216
 
215
217
 
218
+ @dataclass
219
+ class CustomEvent(BaseTeamRunEvent):
220
+ event: str = TeamRunEvent.custom_event.value
221
+
222
+
216
223
  TeamRunOutputEvent = Union[
217
224
  RunStartedEvent,
218
225
  RunContentEvent,
@@ -231,6 +238,7 @@ TeamRunOutputEvent = Union[
231
238
  ParserModelResponseCompletedEvent,
232
239
  OutputModelResponseStartedEvent,
233
240
  OutputModelResponseCompletedEvent,
241
+ CustomEvent,
234
242
  ]
235
243
 
236
244
  # Map event string to dataclass for team events
@@ -252,6 +260,7 @@ TEAM_RUN_EVENT_TYPE_REGISTRY = {
252
260
  TeamRunEvent.parser_model_response_completed.value: ParserModelResponseCompletedEvent,
253
261
  TeamRunEvent.output_model_response_started.value: OutputModelResponseStartedEvent,
254
262
  TeamRunEvent.output_model_response_completed.value: OutputModelResponseCompletedEvent,
263
+ TeamRunEvent.custom_event.value: CustomEvent,
255
264
  }
256
265
 
257
266
 
@@ -266,6 +275,76 @@ def team_run_output_event_from_dict(data: dict) -> BaseTeamRunEvent:
266
275
  return event_class.from_dict(data) # type: ignore
267
276
 
268
277
 
278
+ @dataclass
279
+ class TeamRunInput:
280
+ """Container for the raw input data passed to Agent.run().
281
+ This captures the original input exactly as provided by the user,
282
+ separate from the processed messages that go to the model.
283
+ Attributes:
284
+ input_content: The literal input message/content passed to run()
285
+ images: Images directly passed to run()
286
+ videos: Videos directly passed to run()
287
+ audios: Audio files directly passed to run()
288
+ files: Files directly passed to run()
289
+ """
290
+
291
+ input_content: Optional[Union[str, List, Dict, Message, BaseModel, List[Message]]] = None
292
+ images: Optional[Sequence[ImageArtifact]] = None
293
+ videos: Optional[Sequence[VideoArtifact]] = None
294
+ audios: Optional[Sequence[AudioArtifact]] = None
295
+ files: Optional[Sequence[File]] = None
296
+
297
+ def to_dict(self) -> Dict[str, Any]:
298
+ """Convert to dictionary representation"""
299
+ result: Dict[str, Any] = {}
300
+
301
+ if self.input_content is not None:
302
+ if isinstance(self.input_content, (str)):
303
+ result["input_content"] = self.input_content
304
+ elif isinstance(self.input_content, BaseModel):
305
+ result["input_content"] = self.input_content.model_dump(exclude_none=True)
306
+ elif isinstance(self.input_content, Message):
307
+ result["input_content"] = self.input_content.to_dict()
308
+ elif (
309
+ isinstance(self.input_content, list)
310
+ and self.input_content
311
+ and isinstance(self.input_content[0], Message)
312
+ ):
313
+ result["input_content"] = [m.to_dict() for m in self.input_content]
314
+ else:
315
+ result["input_content"] = self.input_content
316
+
317
+ if self.images:
318
+ result["images"] = [img.to_dict() for img in self.images]
319
+ if self.videos:
320
+ result["videos"] = [vid.to_dict() for vid in self.videos]
321
+ if self.audios:
322
+ result["audios"] = [aud.to_dict() for aud in self.audios]
323
+
324
+ return result
325
+
326
+ @classmethod
327
+ def from_dict(cls, data: Dict[str, Any]) -> "TeamRunInput":
328
+ """Create TeamRunInput from dictionary"""
329
+ images = None
330
+ if data.get("images"):
331
+ images = [ImageArtifact.model_validate(img_data) for img_data in data["images"]]
332
+
333
+ videos = None
334
+ if data.get("videos"):
335
+ videos = [VideoArtifact.model_validate(vid_data) for vid_data in data["videos"]]
336
+
337
+ audios = None
338
+ if data.get("audios"):
339
+ audios = [AudioArtifact.model_validate(aud_data) for aud_data in data["audios"]]
340
+
341
+ files = None
342
+ if data.get("files"):
343
+ files = [File.model_validate(file_data) for file_data in data["files"]]
344
+
345
+ return cls(input_content=data.get("input_content"), images=images, videos=videos, audios=audios, files=files)
346
+
347
+
269
348
  @dataclass
270
349
  class TeamRunOutput:
271
350
  """Response returned by Team.run() functions"""
@@ -293,6 +372,9 @@ class TeamRunOutput:
293
372
 
294
373
  response_audio: Optional[AudioResponse] = None # Model audio response
295
374
 
375
+ # Input media and messages from user
376
+ input: Optional[TeamRunInput] = None
377
+
296
378
  reasoning_content: Optional[str] = None
297
379
 
298
380
  citations: Optional[Citations] = None
@@ -401,6 +483,9 @@ class TeamRunOutput:
401
483
  else:
402
484
  _dict["tools"].append(tool)
403
485
 
486
+ if self.input is not None:
487
+ _dict["input"] = self.input.to_dict()
488
+
404
489
  return _dict
405
490
 
406
491
  def to_json(self) -> str:
@@ -468,6 +553,11 @@ class TeamRunOutput:
468
553
  response_audio = data.pop("response_audio", None)
469
554
  response_audio = AudioResponse.model_validate(response_audio) if response_audio else None
470
555
 
556
+ input_data = data.pop("input", None)
557
+ input_obj = None
558
+ if input_data:
559
+ input_obj = TeamRunInput.from_dict(input_data)
560
+
471
561
  metrics = data.pop("metrics", None)
472
562
  if metrics:
473
563
  metrics = Metrics(**metrics)
@@ -487,6 +577,7 @@ class TeamRunOutput:
487
577
  videos=videos,
488
578
  audio=audio,
489
579
  response_audio=response_audio,
580
+ input=input_obj,
490
581
  citations=citations,
491
582
  tools=tools,
492
583
  events=events,
agno/run/workflow.py CHANGED
@@ -13,6 +13,9 @@ from agno.utils.log import log_error
13
13
 
14
14
  if TYPE_CHECKING:
15
15
  from agno.workflow.types import StepOutput, WorkflowMetrics
16
+ else:
17
+ StepOutput = Any
18
+ WorkflowMetrics = Any
16
19
 
17
20
 
18
21
  class WorkflowRunEvent(str, Enum):
@@ -46,6 +49,8 @@ class WorkflowRunEvent(str, Enum):
46
49
 
47
50
  step_output = "StepOutput"
48
51
 
52
+ custom_event = "CustomEvent"
53
+
49
54
 
50
55
  @dataclass
51
56
  class BaseWorkflowRunOutputEvent:
@@ -132,7 +137,7 @@ class WorkflowCompletedEvent(BaseWorkflowRunOutputEvent):
132
137
  content_type: str = "str"
133
138
 
134
139
  # Store actual step execution results as StepOutput objects
135
- step_results: List["StepOutput"] = field(default_factory=list) # noqa: F821
140
+ step_results: List[StepOutput] = field(default_factory=list)
136
141
  metadata: Optional[Dict[str, Any]] = None
137
142
 
138
143
 
@@ -183,7 +188,7 @@ class StepCompletedEvent(BaseWorkflowRunOutputEvent):
183
188
  response_audio: Optional[AudioResponse] = None
184
189
 
185
190
  # Store actual step execution results as StepOutput objects
186
- step_response: Optional["StepOutput"] = None # noqa: F821
191
+ step_response: Optional[StepOutput] = None
187
192
 
188
193
 
189
194
  @dataclass
@@ -226,7 +231,7 @@ class LoopIterationCompletedEvent(BaseWorkflowRunOutputEvent):
226
231
  step_index: Optional[Union[int, tuple]] = None
227
232
  iteration: int = 0
228
233
  max_iterations: Optional[int] = None
229
- iteration_results: List["StepOutput"] = field(default_factory=list) # noqa: F821
234
+ iteration_results: List[StepOutput] = field(default_factory=list)
230
235
  should_continue: bool = True
231
236
 
232
237
 
@@ -239,7 +244,7 @@ class LoopExecutionCompletedEvent(BaseWorkflowRunOutputEvent):
239
244
  step_index: Optional[Union[int, tuple]] = None
240
245
  total_iterations: int = 0
241
246
  max_iterations: Optional[int] = None
242
- all_results: List[List["StepOutput"]] = field(default_factory=list) # noqa: F821
247
+ all_results: List[List[StepOutput]] = field(default_factory=list)
243
248
 
244
249
 
245
250
  @dataclass
@@ -262,7 +267,7 @@ class ParallelExecutionCompletedEvent(BaseWorkflowRunOutputEvent):
262
267
  parallel_step_count: Optional[int] = None
263
268
 
264
269
  # Results from all parallel steps
265
- step_results: List["StepOutput"] = field(default_factory=list) # noqa: F821
270
+ step_results: List[StepOutput] = field(default_factory=list)
266
271
 
267
272
 
268
273
  @dataclass
@@ -286,7 +291,7 @@ class ConditionExecutionCompletedEvent(BaseWorkflowRunOutputEvent):
286
291
  executed_steps: Optional[int] = None
287
292
 
288
293
  # Results from executed steps
289
- step_results: List["StepOutput"] = field(default_factory=list) # noqa: F821
294
+ step_results: List[StepOutput] = field(default_factory=list)
290
295
 
291
296
 
292
297
  @dataclass
@@ -312,7 +317,7 @@ class RouterExecutionCompletedEvent(BaseWorkflowRunOutputEvent):
312
317
  executed_steps: Optional[int] = None
313
318
 
314
319
  # Results from executed steps
315
- step_results: List["StepOutput"] = field(default_factory=list) # noqa: F821
320
+ step_results: List[StepOutput] = field(default_factory=list)
316
321
 
317
322
 
318
323
  @dataclass
@@ -336,7 +341,7 @@ class StepsExecutionCompletedEvent(BaseWorkflowRunOutputEvent):
336
341
  executed_steps: Optional[int] = None
337
342
 
338
343
  # Results from executed steps
339
- step_results: List["StepOutput"] = field(default_factory=list) # noqa: F821
344
+ step_results: List[StepOutput] = field(default_factory=list)
340
345
 
341
346
 
342
347
  @dataclass
@@ -348,7 +353,7 @@ class StepOutputEvent(BaseWorkflowRunOutputEvent):
348
353
  step_index: Optional[Union[int, tuple]] = None
349
354
 
350
355
  # Store actual step execution result as StepOutput object
351
- step_output: Optional["StepOutput"] = None # noqa: F821
356
+ step_output: Optional[StepOutput] = None
352
357
 
353
358
  # Properties for backward compatibility
354
359
  @property
@@ -380,6 +385,13 @@ class StepOutputEvent(BaseWorkflowRunOutputEvent):
380
385
  return self.step_output.stop if self.step_output else False
381
386
 
382
387
 
388
+ @dataclass
389
+ class CustomEvent(BaseWorkflowRunOutputEvent):
390
+ """Event sent when a custom event is produced"""
391
+
392
+ event: str = WorkflowRunEvent.custom_event.value
393
+
394
+
383
395
  # Union type for all workflow run response events
384
396
  WorkflowRunOutputEvent = Union[
385
397
  WorkflowStartedEvent,
@@ -402,6 +414,7 @@ WorkflowRunOutputEvent = Union[
402
414
  StepsExecutionStartedEvent,
403
415
  StepsExecutionCompletedEvent,
404
416
  StepOutputEvent,
417
+ CustomEvent,
405
418
  ]
406
419
 
407
420
 
@@ -426,7 +439,7 @@ class WorkflowRunOutput:
426
439
  response_audio: Optional[AudioResponse] = None
427
440
 
428
441
  # Store actual step execution results as StepOutput objects
429
- step_results: List[Union["StepOutput", List["StepOutput"]]] = field(default_factory=list) # noqa: F821
442
+ step_results: List[Union[StepOutput, List[StepOutput]]] = field(default_factory=list)
430
443
 
431
444
  # Store agent/team responses separately with parent_run_id references
432
445
  step_executor_runs: Optional[List[Union[RunOutput, TeamRunOutput]]] = None
@@ -435,7 +448,7 @@ class WorkflowRunOutput:
435
448
  events: Optional[List[WorkflowRunOutputEvent]] = None
436
449
 
437
450
  # Workflow metrics aggregated from all steps
438
- metrics: Optional["WorkflowMetrics"] = None
451
+ metrics: Optional[WorkflowMetrics] = None
439
452
 
440
453
  metadata: Optional[Dict[str, Any]] = None
441
454
  created_at: int = field(default_factory=lambda: int(time()))
@@ -521,7 +534,7 @@ class WorkflowRunOutput:
521
534
  workflow_metrics = WorkflowMetrics.from_dict(workflow_metrics_dict)
522
535
 
523
536
  step_results = data.pop("step_results", [])
524
- parsed_step_results: List[Union["StepOutput", List["StepOutput"]]] = []
537
+ parsed_step_results: List[Union[StepOutput, List[StepOutput]]] = []
525
538
  if step_results:
526
539
  for step_output_dict in step_results:
527
540
  # Reconstruct StepOutput from dict