agno 2.3.25__py3-none-any.whl → 2.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. agno/agent/__init__.py +4 -0
  2. agno/agent/agent.py +1428 -558
  3. agno/agent/remote.py +13 -0
  4. agno/db/base.py +339 -0
  5. agno/db/postgres/async_postgres.py +116 -12
  6. agno/db/postgres/postgres.py +1229 -25
  7. agno/db/postgres/schemas.py +48 -1
  8. agno/db/sqlite/async_sqlite.py +119 -4
  9. agno/db/sqlite/schemas.py +51 -0
  10. agno/db/sqlite/sqlite.py +1173 -13
  11. agno/db/utils.py +37 -1
  12. agno/knowledge/__init__.py +4 -0
  13. agno/knowledge/chunking/code.py +1 -1
  14. agno/knowledge/chunking/semantic.py +1 -1
  15. agno/knowledge/chunking/strategy.py +4 -0
  16. agno/knowledge/filesystem.py +412 -0
  17. agno/knowledge/knowledge.py +2767 -2254
  18. agno/knowledge/protocol.py +134 -0
  19. agno/knowledge/reader/arxiv_reader.py +2 -2
  20. agno/knowledge/reader/base.py +9 -7
  21. agno/knowledge/reader/csv_reader.py +5 -5
  22. agno/knowledge/reader/docx_reader.py +2 -2
  23. agno/knowledge/reader/field_labeled_csv_reader.py +2 -2
  24. agno/knowledge/reader/firecrawl_reader.py +2 -2
  25. agno/knowledge/reader/json_reader.py +2 -2
  26. agno/knowledge/reader/markdown_reader.py +2 -2
  27. agno/knowledge/reader/pdf_reader.py +5 -4
  28. agno/knowledge/reader/pptx_reader.py +2 -2
  29. agno/knowledge/reader/reader_factory.py +110 -0
  30. agno/knowledge/reader/s3_reader.py +2 -2
  31. agno/knowledge/reader/tavily_reader.py +2 -2
  32. agno/knowledge/reader/text_reader.py +2 -2
  33. agno/knowledge/reader/web_search_reader.py +2 -2
  34. agno/knowledge/reader/website_reader.py +5 -3
  35. agno/knowledge/reader/wikipedia_reader.py +2 -2
  36. agno/knowledge/reader/youtube_reader.py +2 -2
  37. agno/knowledge/utils.py +37 -29
  38. agno/learn/__init__.py +6 -0
  39. agno/learn/machine.py +35 -0
  40. agno/learn/schemas.py +82 -11
  41. agno/learn/stores/__init__.py +3 -0
  42. agno/learn/stores/decision_log.py +1156 -0
  43. agno/learn/stores/learned_knowledge.py +6 -6
  44. agno/models/anthropic/claude.py +24 -0
  45. agno/models/aws/bedrock.py +20 -0
  46. agno/models/base.py +48 -4
  47. agno/models/cohere/chat.py +25 -0
  48. agno/models/google/gemini.py +50 -5
  49. agno/models/litellm/chat.py +38 -0
  50. agno/models/openai/chat.py +7 -0
  51. agno/models/openrouter/openrouter.py +46 -0
  52. agno/models/response.py +16 -0
  53. agno/os/app.py +83 -44
  54. agno/os/middleware/__init__.py +2 -0
  55. agno/os/middleware/trailing_slash.py +27 -0
  56. agno/os/router.py +1 -0
  57. agno/os/routers/agents/router.py +29 -16
  58. agno/os/routers/agents/schema.py +6 -4
  59. agno/os/routers/components/__init__.py +3 -0
  60. agno/os/routers/components/components.py +466 -0
  61. agno/os/routers/evals/schemas.py +4 -3
  62. agno/os/routers/health.py +3 -3
  63. agno/os/routers/knowledge/knowledge.py +3 -3
  64. agno/os/routers/memory/schemas.py +4 -2
  65. agno/os/routers/metrics/metrics.py +9 -11
  66. agno/os/routers/metrics/schemas.py +10 -6
  67. agno/os/routers/registry/__init__.py +3 -0
  68. agno/os/routers/registry/registry.py +337 -0
  69. agno/os/routers/teams/router.py +20 -8
  70. agno/os/routers/teams/schema.py +6 -4
  71. agno/os/routers/traces/traces.py +5 -5
  72. agno/os/routers/workflows/router.py +38 -11
  73. agno/os/routers/workflows/schema.py +1 -1
  74. agno/os/schema.py +92 -26
  75. agno/os/utils.py +133 -16
  76. agno/reasoning/anthropic.py +2 -2
  77. agno/reasoning/azure_ai_foundry.py +2 -2
  78. agno/reasoning/deepseek.py +2 -2
  79. agno/reasoning/default.py +6 -7
  80. agno/reasoning/gemini.py +2 -2
  81. agno/reasoning/helpers.py +6 -7
  82. agno/reasoning/manager.py +4 -10
  83. agno/reasoning/ollama.py +2 -2
  84. agno/reasoning/openai.py +2 -2
  85. agno/reasoning/vertexai.py +2 -2
  86. agno/registry/__init__.py +3 -0
  87. agno/registry/registry.py +68 -0
  88. agno/run/agent.py +57 -0
  89. agno/run/base.py +7 -0
  90. agno/run/team.py +57 -0
  91. agno/skills/agent_skills.py +10 -3
  92. agno/team/__init__.py +3 -1
  93. agno/team/team.py +1276 -326
  94. agno/tools/duckduckgo.py +25 -71
  95. agno/tools/exa.py +0 -21
  96. agno/tools/function.py +35 -83
  97. agno/tools/knowledge.py +9 -4
  98. agno/tools/mem0.py +11 -10
  99. agno/tools/memory.py +47 -46
  100. agno/tools/parallel.py +0 -7
  101. agno/tools/reasoning.py +30 -23
  102. agno/tools/tavily.py +4 -1
  103. agno/tools/websearch.py +93 -0
  104. agno/tools/website.py +1 -1
  105. agno/tools/wikipedia.py +1 -1
  106. agno/tools/workflow.py +48 -47
  107. agno/utils/agent.py +42 -5
  108. agno/utils/events.py +160 -2
  109. agno/utils/print_response/agent.py +0 -31
  110. agno/utils/print_response/team.py +0 -2
  111. agno/utils/print_response/workflow.py +0 -2
  112. agno/utils/team.py +61 -11
  113. agno/vectordb/lancedb/lance_db.py +4 -1
  114. agno/vectordb/mongodb/mongodb.py +1 -1
  115. agno/vectordb/qdrant/qdrant.py +4 -4
  116. agno/workflow/__init__.py +3 -1
  117. agno/workflow/condition.py +0 -21
  118. agno/workflow/loop.py +0 -21
  119. agno/workflow/parallel.py +0 -21
  120. agno/workflow/router.py +0 -21
  121. agno/workflow/step.py +117 -24
  122. agno/workflow/steps.py +0 -21
  123. agno/workflow/workflow.py +625 -63
  124. {agno-2.3.25.dist-info → agno-2.4.0.dist-info}/METADATA +46 -76
  125. {agno-2.3.25.dist-info → agno-2.4.0.dist-info}/RECORD +128 -117
  126. {agno-2.3.25.dist-info → agno-2.4.0.dist-info}/WHEEL +0 -0
  127. {agno-2.3.25.dist-info → agno-2.4.0.dist-info}/licenses/LICENSE +0 -0
  128. {agno-2.3.25.dist-info → agno-2.4.0.dist-info}/top_level.txt +0 -0
agno/os/schema.py CHANGED
@@ -16,11 +16,7 @@ from agno.os.config import (
16
16
  SessionConfig,
17
17
  TracesConfig,
18
18
  )
19
- from agno.os.utils import (
20
- extract_input_media,
21
- get_run_input,
22
- get_session_name,
23
- )
19
+ from agno.os.utils import extract_input_media, get_run_input, get_session_name, to_utc_datetime
24
20
  from agno.session import AgentSession, TeamSession, WorkflowSession
25
21
  from agno.team.remote import RemoteTeam
26
22
  from agno.team.team import Team
@@ -79,10 +75,12 @@ class InternalServerErrorResponse(BaseModel):
79
75
 
80
76
 
81
77
  class HealthResponse(BaseModel):
82
- model_config = ConfigDict(json_schema_extra={"example": {"status": "ok", "instantiated_at": "1760169236.778903"}})
78
+ model_config = ConfigDict(
79
+ json_schema_extra={"example": {"status": "ok", "instantiated_at": "2025-06-10T12:00:00Z"}}
80
+ )
83
81
 
84
82
  status: str = Field(..., description="Health status of the service")
85
- instantiated_at: str = Field(..., description="Unix timestamp when service was instantiated")
83
+ instantiated_at: datetime = Field(..., description="Timestamp when service was instantiated")
86
84
 
87
85
 
88
86
  class InterfaceResponse(BaseModel):
@@ -145,7 +143,8 @@ class ConfigResponse(BaseModel):
145
143
  name: Optional[str] = Field(None, description="Name of the OS instance")
146
144
  description: Optional[str] = Field(None, description="Description of the OS instance")
147
145
  available_models: Optional[List[str]] = Field(None, description="List of available models")
148
- databases: List[str] = Field(..., description="List of database IDs")
146
+ os_database: Optional[str] = Field(None, description="ID of the database used for the OS instance")
147
+ databases: List[str] = Field(..., description="List of database IDs used by the components of the OS instance")
149
148
  chat: Optional[ChatConfig] = Field(None, description="Chat configuration")
150
149
 
151
150
  session: Optional[SessionConfig] = Field(None, description="Session configuration")
@@ -212,8 +211,8 @@ class SessionSchema(BaseModel):
212
211
  return None
213
212
  return None
214
213
 
215
- created_at = parse_datetime(session.get("created_at", 0))
216
- updated_at = parse_datetime(session.get("updated_at", created_at))
214
+ created_at = to_utc_datetime(session.get("created_at", 0))
215
+ updated_at = to_utc_datetime(session.get("updated_at", created_at))
217
216
  return cls(
218
217
  session_id=session.get("session_id", ""),
219
218
  session_name=session_name,
@@ -282,8 +281,8 @@ class AgentSessionDetailSchema(BaseModel):
282
281
  metrics=session.session_data.get("session_metrics", {}) if session.session_data else None, # type: ignore
283
282
  metadata=session.metadata,
284
283
  chat_history=[message.to_dict() for message in session.get_chat_history()],
285
- created_at=created_at,
286
- updated_at=updated_at,
284
+ created_at=to_utc_datetime(created_at),
285
+ updated_at=to_utc_datetime(updated_at),
287
286
  )
288
287
 
289
288
 
@@ -322,8 +321,8 @@ class TeamSessionDetailSchema(BaseModel):
322
321
  metrics=session.session_data.get("session_metrics", {}) if session.session_data else None,
323
322
  metadata=session.metadata,
324
323
  chat_history=[message.to_dict() for message in session.get_chat_history()],
325
- created_at=created_at,
326
- updated_at=updated_at,
324
+ created_at=to_utc_datetime(created_at),
325
+ updated_at=to_utc_datetime(updated_at),
327
326
  )
328
327
 
329
328
 
@@ -339,13 +338,15 @@ class WorkflowSessionDetailSchema(BaseModel):
339
338
  workflow_data: Optional[dict] = Field(None, description="Workflow-specific data")
340
339
  metadata: Optional[dict] = Field(None, description="Additional metadata")
341
340
 
342
- created_at: Optional[int] = Field(None, description="Unix timestamp of session creation")
343
- updated_at: Optional[int] = Field(None, description="Unix timestamp of last update")
341
+ created_at: Optional[datetime] = Field(None, description="Session creation timestamp")
342
+ updated_at: Optional[datetime] = Field(None, description="Last update timestamp")
344
343
 
345
344
  @classmethod
346
345
  def from_session(cls, session: WorkflowSession) -> "WorkflowSessionDetailSchema":
347
346
  session_dict = session.to_dict()
348
347
  session_name = get_session_name({**session_dict, "session_type": "workflow"})
348
+ created_at = datetime.fromtimestamp(session.created_at, tz=timezone.utc) if session.created_at else None
349
+ updated_at = datetime.fromtimestamp(session.updated_at, tz=timezone.utc) if session.updated_at else created_at
349
350
  return cls(
350
351
  session_id=session.session_id,
351
352
  user_id=session.user_id,
@@ -356,8 +357,8 @@ class WorkflowSessionDetailSchema(BaseModel):
356
357
  session_state=session.session_data.get("session_state", None) if session.session_data else None,
357
358
  workflow_data=session.workflow_data,
358
359
  metadata=session.metadata,
359
- created_at=session.created_at,
360
- updated_at=session.updated_at or session.created_at,
360
+ created_at=to_utc_datetime(created_at),
361
+ updated_at=to_utc_datetime(updated_at),
361
362
  )
362
363
 
363
364
 
@@ -418,9 +419,7 @@ class RunSchema(BaseModel):
418
419
  files=run_dict.get("files", []),
419
420
  response_audio=run_dict.get("response_audio", None),
420
421
  input_media=extract_input_media(run_dict),
421
- created_at=datetime.fromtimestamp(run_dict.get("created_at", 0), tz=timezone.utc)
422
- if run_dict.get("created_at") is not None
423
- else None,
422
+ created_at=to_utc_datetime(run_dict.get("created_at")),
424
423
  )
425
424
 
426
425
 
@@ -468,9 +467,7 @@ class TeamRunSchema(BaseModel):
468
467
  messages=[message for message in run_dict.get("messages", [])] if run_dict.get("messages") else None,
469
468
  tools=[tool for tool in run_dict.get("tools", [])] if run_dict.get("tools") else None,
470
469
  events=[event for event in run_dict["events"]] if run_dict.get("events") else None,
471
- created_at=datetime.fromtimestamp(run_dict.get("created_at", 0), tz=timezone.utc)
472
- if run_dict.get("created_at") is not None
473
- else None,
470
+ created_at=to_utc_datetime(run_dict.get("created_at")),
474
471
  references=run_dict.get("references", []),
475
472
  citations=run_dict.get("citations", None),
476
473
  reasoning_messages=run_dict.get("reasoning_messages", []),
@@ -496,7 +493,7 @@ class WorkflowRunSchema(BaseModel):
496
493
  step_results: Optional[list[dict]] = Field(None, description="Results from each workflow step")
497
494
  step_executor_runs: Optional[list[dict]] = Field(None, description="Executor runs for each step")
498
495
  metrics: Optional[dict] = Field(None, description="Performance and usage metrics")
499
- created_at: Optional[int] = Field(None, description="Unix timestamp of run creation")
496
+ created_at: Optional[datetime] = Field(None, description="Run creation timestamp")
500
497
  reasoning_content: Optional[str] = Field(None, description="Reasoning content if reasoning was enabled")
501
498
  reasoning_steps: Optional[List[dict]] = Field(None, description="List of reasoning steps")
502
499
  references: Optional[List[dict]] = Field(None, description="References cited in the workflow")
@@ -525,7 +522,7 @@ class WorkflowRunSchema(BaseModel):
525
522
  metrics=run_response.get("metrics", {}),
526
523
  step_results=run_response.get("step_results", []),
527
524
  step_executor_runs=run_response.get("step_executor_runs", []),
528
- created_at=run_response["created_at"],
525
+ created_at=to_utc_datetime(run_response.get("created_at")),
529
526
  reasoning_content=run_response.get("reasoning_content", ""),
530
527
  reasoning_steps=run_response.get("reasoning_steps", []),
531
528
  references=run_response.get("references", []),
@@ -560,3 +557,72 @@ class PaginatedResponse(BaseModel, Generic[T]):
560
557
 
561
558
  data: List[T] = Field(..., description="List of items for the current page")
562
559
  meta: PaginationInfo = Field(..., description="Pagination metadata")
560
+
561
+
562
+ class ComponentType(str, Enum):
563
+ AGENT = "agent"
564
+ TEAM = "team"
565
+ WORKFLOW = "workflow"
566
+
567
+
568
+ class ComponentCreate(BaseModel):
569
+ name: str = Field(..., description="Display name")
570
+ component_id: Optional[str] = Field(
571
+ None, description="Unique identifier for the entity. Auto-generated from name if not provided."
572
+ )
573
+ component_type: ComponentType = Field(..., description="Type of entity: agent, team, or workflow")
574
+ description: Optional[str] = Field(None, description="Optional description")
575
+ metadata: Optional[Dict[str, Any]] = Field(None, description="Optional metadata")
576
+ # Config parameters are optional, but if provided, they will be used to create the initial config
577
+ config: Optional[Dict[str, Any]] = Field(None, description="Optional configuration")
578
+ label: Optional[str] = Field(None, description="Optional label (e.g., 'stable')")
579
+ stage: str = Field("draft", description="Stage: 'draft' or 'published'")
580
+ notes: Optional[str] = Field(None, description="Optional notes")
581
+ set_current: bool = Field(True, description="Set as current version")
582
+
583
+
584
+ class ComponentResponse(BaseModel):
585
+ component_id: str
586
+ component_type: ComponentType
587
+ name: Optional[str] = None
588
+ description: Optional[str] = None
589
+ current_version: Optional[int] = None
590
+ metadata: Optional[Dict[str, Any]] = None
591
+ created_at: int
592
+ updated_at: Optional[int] = None
593
+
594
+
595
+ class ConfigCreate(BaseModel):
596
+ config: Dict[str, Any] = Field(..., description="The configuration data")
597
+ version: Optional[int] = Field(None, description="Optional version number")
598
+ label: Optional[str] = Field(None, description="Optional label (e.g., 'stable')")
599
+ stage: str = Field("draft", description="Stage: 'draft' or 'published'")
600
+ notes: Optional[str] = Field(None, description="Optional notes")
601
+ links: Optional[List[Dict[str, Any]]] = Field(None, description="Optional links to child components")
602
+ set_current: bool = Field(True, description="Set as current version")
603
+
604
+
605
+ class ComponentConfigResponse(BaseModel):
606
+ component_id: str
607
+ version: int
608
+ label: Optional[str] = None
609
+ stage: str
610
+ config: Dict[str, Any]
611
+ notes: Optional[str] = None
612
+ created_at: int
613
+ updated_at: Optional[int] = None
614
+
615
+
616
+ class ComponentUpdate(BaseModel):
617
+ name: Optional[str] = None
618
+ description: Optional[str] = None
619
+ component_type: Optional[str] = None
620
+ metadata: Optional[Dict[str, Any]] = None
621
+
622
+
623
+ class ConfigUpdate(BaseModel):
624
+ config: Optional[Dict[str, Any]] = None
625
+ label: Optional[str] = None
626
+ stage: Optional[str] = None
627
+ notes: Optional[str] = None
628
+ links: Optional[List[Dict[str, Any]]] = None
agno/os/utils.py CHANGED
@@ -14,6 +14,7 @@ from agno.media import Audio, Image, Video
14
14
  from agno.media import File as FileMedia
15
15
  from agno.models.message import Message
16
16
  from agno.os.config import AgentOSConfig
17
+ from agno.registry import Registry
17
18
  from agno.remote.base import RemoteDb, RemoteKnowledge
18
19
  from agno.run.agent import RunOutputEvent
19
20
  from agno.run.team import TeamRunOutputEvent
@@ -24,6 +25,20 @@ from agno.utils.log import log_warning, logger
24
25
  from agno.workflow import RemoteWorkflow, Workflow
25
26
 
26
27
 
28
+ def to_utc_datetime(value: Optional[Union[int, float, datetime]]) -> Optional[datetime]:
29
+ """Convert a timestamp to a UTC datetime."""
30
+ if value is None:
31
+ return None
32
+
33
+ if isinstance(value, datetime):
34
+ # If already a datetime, make sure the timezone is UTC
35
+ if value.tzinfo is None:
36
+ return value.replace(tzinfo=timezone.utc)
37
+ return value
38
+
39
+ return datetime.fromtimestamp(value, tz=timezone.utc)
40
+
41
+
27
42
  async def get_request_kwargs(request: Request, endpoint_func: Callable) -> Dict[str, Any]:
28
43
  """Given a Request and an endpoint function, return a dictionary with all extra form data fields.
29
44
 
@@ -414,38 +429,140 @@ def extract_format(file: UploadFile) -> Optional[str]:
414
429
 
415
430
 
416
431
  def get_agent_by_id(
417
- agent_id: str, agents: Optional[List[Union[Agent, RemoteAgent]]] = None
432
+ agent_id: str,
433
+ agents: Optional[List[Union[Agent, RemoteAgent]]] = None,
434
+ db: Optional[Union[BaseDb, AsyncBaseDb]] = None,
435
+ registry: Optional[Registry] = None,
436
+ version: Optional[int] = None,
437
+ create_fresh: bool = False,
418
438
  ) -> Optional[Union[Agent, RemoteAgent]]:
419
- if agent_id is None or agents is None:
439
+ """Get an agent by ID, optionally creating a fresh instance for request isolation.
440
+
441
+ When create_fresh=True, creates a new agent instance using deep_copy() to prevent
442
+ state contamination between concurrent requests. The new instance shares heavy
443
+ resources (db, model, MCP tools) but has isolated mutable state.
444
+
445
+ Args:
446
+ agent_id: The agent ID to look up
447
+ agents: List of agents to search
448
+ create_fresh: If True, creates a new instance using deep_copy()
449
+
450
+ Returns:
451
+ The agent instance (shared or fresh copy based on create_fresh)
452
+ """
453
+ if agent_id is None:
420
454
  return None
421
455
 
422
- for agent in agents:
423
- if agent.id == agent_id:
424
- return agent
456
+ # Try to get the agent from the list of agents
457
+ if agents:
458
+ for agent in agents:
459
+ if agent.id == agent_id:
460
+ if create_fresh and isinstance(agent, Agent):
461
+ return agent.deep_copy()
462
+ return agent
463
+
464
+ # Try to get the agent from the database
465
+ if db and isinstance(db, BaseDb):
466
+ from agno.agent.agent import get_agent_by_id as get_agent_by_id_db
467
+
468
+ try:
469
+ db_agent = get_agent_by_id_db(db=db, id=agent_id, version=version, registry=registry)
470
+ return db_agent
471
+ except Exception as e:
472
+ logger.error(f"Error getting agent {agent_id} from database: {e}")
473
+ return None
474
+
425
475
  return None
426
476
 
427
477
 
428
478
  def get_team_by_id(
429
- team_id: str, teams: Optional[List[Union[Team, RemoteTeam]]] = None
479
+ team_id: str,
480
+ teams: Optional[List[Union[Team, RemoteTeam]]] = None,
481
+ create_fresh: bool = False,
482
+ db: Optional[Union[BaseDb, AsyncBaseDb]] = None,
483
+ version: Optional[int] = None,
484
+ registry: Optional[Registry] = None,
430
485
  ) -> Optional[Union[Team, RemoteTeam]]:
431
- if team_id is None or teams is None:
486
+ """Get a team by ID, optionally creating a fresh instance for request isolation.
487
+
488
+ When create_fresh=True, creates a new team instance using deep_copy() to prevent
489
+ state contamination between concurrent requests. Member agents are also deep copied.
490
+
491
+ Args:
492
+ team_id: The team ID to look up
493
+ teams: List of teams to search
494
+ create_fresh: If True, creates a new instance using deep_copy()
495
+
496
+ Returns:
497
+ The team instance (shared or fresh copy based on create_fresh)
498
+ """
499
+ if team_id is None:
432
500
  return None
433
501
 
434
- for team in teams:
435
- if team.id == team_id:
436
- return team
502
+ if teams:
503
+ for team in teams:
504
+ if team.id == team_id:
505
+ if create_fresh and isinstance(team, Team):
506
+ return team.deep_copy()
507
+ return team
508
+
509
+ if db and isinstance(db, BaseDb):
510
+ from agno.team.team import get_team_by_id as get_team_by_id_db
511
+
512
+ try:
513
+ db_team = get_team_by_id_db(db=db, id=team_id, version=version, registry=registry)
514
+ return db_team
515
+ except Exception as e:
516
+ logger.error(f"Error getting team {team_id} from database: {e}")
517
+ return None
518
+
437
519
  return None
438
520
 
439
521
 
440
522
  def get_workflow_by_id(
441
- workflow_id: str, workflows: Optional[List[Union[Workflow, RemoteWorkflow]]] = None
523
+ workflow_id: str,
524
+ workflows: Optional[List[Union[Workflow, RemoteWorkflow]]] = None,
525
+ create_fresh: bool = False,
526
+ db: Optional[Union[BaseDb, AsyncBaseDb]] = None,
527
+ version: Optional[int] = None,
528
+ registry: Optional[Registry] = None,
442
529
  ) -> Optional[Union[Workflow, RemoteWorkflow]]:
443
- if workflow_id is None or workflows is None:
530
+ """Get a workflow by ID, optionally creating a fresh instance for request isolation.
531
+
532
+ When create_fresh=True, creates a new workflow instance using deep_copy() to prevent
533
+ state contamination between concurrent requests. Steps containing agents/teams are also deep copied.
534
+
535
+ Args:
536
+ workflow_id: The workflow ID to look up
537
+ workflows: List of workflows to search
538
+ create_fresh: If True, creates a new instance using deep_copy()
539
+ db: Optional database interface
540
+ version: Workflow version, if needed
541
+ registry: Optional Registry instance
542
+
543
+ Returns:
544
+ The workflow instance (shared or fresh copy based on create_fresh)
545
+ """
546
+ if workflow_id is None:
444
547
  return None
445
548
 
446
- for workflow in workflows:
447
- if workflow.id == workflow_id:
448
- return workflow
549
+ if workflows:
550
+ for workflow in workflows:
551
+ if workflow.id == workflow_id:
552
+ if create_fresh and isinstance(workflow, Workflow):
553
+ return workflow.deep_copy()
554
+ return workflow
555
+
556
+ if db and isinstance(db, BaseDb):
557
+ from agno.workflow.workflow import get_workflow_by_id as get_workflow_by_id_db
558
+
559
+ try:
560
+ db_workflow = get_workflow_by_id_db(db=db, id=workflow_id, version=version, registry=registry)
561
+ return db_workflow
562
+ except Exception as e:
563
+ logger.error(f"Error getting workflow {workflow_id} from database: {e}")
564
+ return None
565
+
449
566
  return None
450
567
 
451
568
 
@@ -819,7 +936,7 @@ def format_duration_ms(duration_ms: Optional[int]) -> str:
819
936
  return f"{duration_ms / 1000:.2f}s"
820
937
 
821
938
 
822
- def parse_datetime_to_utc(datetime_str: str, param_name: str = "datetime") -> "datetime":
939
+ def timestamp_to_datetime(datetime_str: str, param_name: str = "datetime") -> "datetime":
823
940
  """Parse an ISO 8601 datetime string and convert to UTC.
824
941
 
825
942
  Args:
@@ -69,7 +69,7 @@ def get_anthropic_reasoning_stream(
69
69
  redacted_reasoning_content: Optional[str] = None
70
70
 
71
71
  try:
72
- for event in reasoning_agent.run(input=messages, stream=True, stream_intermediate_steps=True):
72
+ for event in reasoning_agent.run(input=messages, stream=True, stream_events=True):
73
73
  if hasattr(event, "event"):
74
74
  if event.event == RunEvent.run_content:
75
75
  # Stream reasoning content as it arrives
@@ -140,7 +140,7 @@ async def aget_anthropic_reasoning_stream(
140
140
  redacted_reasoning_content: Optional[str] = None
141
141
 
142
142
  try:
143
- async for event in reasoning_agent.arun(input=messages, stream=True, stream_intermediate_steps=True):
143
+ async for event in reasoning_agent.arun(input=messages, stream=True, stream_events=True):
144
144
  if hasattr(event, "event"):
145
145
  if event.event == RunEvent.run_content:
146
146
  # Stream reasoning content as it arrives
@@ -86,7 +86,7 @@ def get_ai_foundry_reasoning_stream(
86
86
  reasoning_content: str = ""
87
87
 
88
88
  try:
89
- for event in reasoning_agent.run(input=messages, stream=True, stream_intermediate_steps=True):
89
+ for event in reasoning_agent.run(input=messages, stream=True, stream_events=True):
90
90
  if hasattr(event, "event"):
91
91
  if event.event == RunEvent.run_content:
92
92
  # Check for reasoning_content attribute first (native reasoning)
@@ -132,7 +132,7 @@ async def aget_ai_foundry_reasoning_stream(
132
132
  reasoning_content: str = ""
133
133
 
134
134
  try:
135
- async for event in reasoning_agent.arun(input=messages, stream=True, stream_intermediate_steps=True):
135
+ async for event in reasoning_agent.arun(input=messages, stream=True, stream_events=True):
136
136
  if hasattr(event, "event"):
137
137
  if event.event == RunEvent.run_content:
138
138
  # Check for reasoning_content attribute first (native reasoning)
@@ -69,7 +69,7 @@ def get_deepseek_reasoning_stream(
69
69
  reasoning_content: str = ""
70
70
 
71
71
  try:
72
- for event in reasoning_agent.run(input=messages, stream=True, stream_intermediate_steps=True):
72
+ for event in reasoning_agent.run(input=messages, stream=True, stream_events=True):
73
73
  if hasattr(event, "event"):
74
74
  if event.event == RunEvent.run_content:
75
75
  # Stream reasoning content as it arrives
@@ -140,7 +140,7 @@ async def aget_deepseek_reasoning_stream(
140
140
  reasoning_content: str = ""
141
141
 
142
142
  try:
143
- async for event in reasoning_agent.arun(input=messages, stream=True, stream_intermediate_steps=True):
143
+ async for event in reasoning_agent.arun(input=messages, stream=True, stream_events=True):
144
144
  if hasattr(event, "event"):
145
145
  if event.event == RunEvent.run_content:
146
146
  # Stream reasoning content as it arrives
agno/reasoning/default.py CHANGED
@@ -1,10 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from textwrap import dedent
4
- from typing import Any, Callable, Dict, List, Literal, Optional, Union
4
+ from typing import Callable, Dict, List, Literal, Optional, Union
5
5
 
6
6
  from agno.models.base import Model
7
7
  from agno.reasoning.step import ReasoningSteps
8
+ from agno.run.base import RunContext
8
9
  from agno.tools import Toolkit
9
10
  from agno.tools.function import Function
10
11
 
@@ -19,9 +20,7 @@ def get_default_reasoning_agent(
19
20
  telemetry: bool = True,
20
21
  debug_mode: bool = False,
21
22
  debug_level: Literal[1, 2] = 1,
22
- session_state: Optional[Dict[str, Any]] = None,
23
- dependencies: Optional[Dict[str, Any]] = None,
24
- metadata: Optional[Dict[str, Any]] = None,
23
+ run_context: Optional[RunContext] = None,
25
24
  ) -> Optional["Agent"]: # type: ignore # noqa: F821
26
25
  from agno.agent import Agent
27
26
 
@@ -89,9 +88,9 @@ def get_default_reasoning_agent(
89
88
  telemetry=telemetry,
90
89
  debug_mode=debug_mode,
91
90
  debug_level=debug_level,
92
- session_state=session_state,
93
- dependencies=dependencies,
94
- metadata=metadata,
91
+ session_state=run_context.session_state if run_context else None,
92
+ dependencies=run_context.dependencies if run_context else None,
93
+ metadata=run_context.metadata if run_context else None,
95
94
  )
96
95
 
97
96
  return agent
agno/reasoning/gemini.py CHANGED
@@ -94,7 +94,7 @@ def get_gemini_reasoning_stream(
94
94
  reasoning_content: str = ""
95
95
 
96
96
  try:
97
- for event in reasoning_agent.run(input=messages, stream=True, stream_intermediate_steps=True):
97
+ for event in reasoning_agent.run(input=messages, stream=True, stream_events=True):
98
98
  if hasattr(event, "event"):
99
99
  if event.event == RunEvent.run_content:
100
100
  # Stream reasoning content as it arrives
@@ -134,7 +134,7 @@ async def aget_gemini_reasoning_stream(
134
134
  reasoning_content: str = ""
135
135
 
136
136
  try:
137
- async for event in reasoning_agent.arun(input=messages, stream=True, stream_intermediate_steps=True):
137
+ async for event in reasoning_agent.arun(input=messages, stream=True, stream_events=True):
138
138
  if hasattr(event, "event"):
139
139
  if event.event == RunEvent.run_content:
140
140
  # Stream reasoning content as it arrives
agno/reasoning/helpers.py CHANGED
@@ -1,8 +1,9 @@
1
- from typing import Any, Dict, List, Literal, Optional
1
+ from typing import List, Literal, Optional
2
2
 
3
3
  from agno.models.base import Model
4
4
  from agno.models.message import Message
5
5
  from agno.reasoning.step import NextAction, ReasoningStep
6
+ from agno.run.base import RunContext
6
7
  from agno.run.messages import RunMessages
7
8
  from agno.utils.log import logger
8
9
 
@@ -12,9 +13,7 @@ def get_reasoning_agent(
12
13
  telemetry: bool = False,
13
14
  debug_mode: bool = False,
14
15
  debug_level: Literal[1, 2] = 1,
15
- session_state: Optional[Dict[str, Any]] = None,
16
- dependencies: Optional[Dict[str, Any]] = None,
17
- metadata: Optional[Dict[str, Any]] = None,
16
+ run_context: Optional[RunContext] = None,
18
17
  ) -> "Agent": # type: ignore # noqa: F821
19
18
  from agno.agent import Agent
20
19
 
@@ -23,9 +22,9 @@ def get_reasoning_agent(
23
22
  telemetry=telemetry,
24
23
  debug_mode=debug_mode,
25
24
  debug_level=debug_level,
26
- session_state=session_state,
27
- dependencies=dependencies,
28
- metadata=metadata,
25
+ session_state=run_context.session_state if run_context else None,
26
+ dependencies=run_context.dependencies if run_context else None,
27
+ metadata=run_context.metadata if run_context else None,
29
28
  )
30
29
 
31
30
 
agno/reasoning/manager.py CHANGED
@@ -14,7 +14,6 @@ from dataclasses import dataclass, field
14
14
  from enum import Enum
15
15
  from typing import (
16
16
  TYPE_CHECKING,
17
- Any,
18
17
  AsyncIterator,
19
18
  Callable,
20
19
  Dict,
@@ -29,6 +28,7 @@ from typing import (
29
28
  from agno.models.base import Model
30
29
  from agno.models.message import Message
31
30
  from agno.reasoning.step import NextAction, ReasoningStep, ReasoningSteps
31
+ from agno.run.base import RunContext
32
32
  from agno.run.messages import RunMessages
33
33
  from agno.tools import Toolkit
34
34
  from agno.tools.function import Function
@@ -87,9 +87,7 @@ class ReasoningConfig:
87
87
  telemetry: bool = True
88
88
  debug_mode: bool = False
89
89
  debug_level: Literal[1, 2] = 1
90
- session_state: Optional[Dict[str, Any]] = None
91
- dependencies: Optional[Dict[str, Any]] = None
92
- metadata: Optional[Dict[str, Any]] = None
90
+ run_context: Optional[RunContext] = None
93
91
 
94
92
 
95
93
  @dataclass
@@ -161,9 +159,7 @@ class ReasoningManager:
161
159
  telemetry=self.config.telemetry,
162
160
  debug_mode=self.config.debug_mode,
163
161
  debug_level=self.config.debug_level,
164
- session_state=self.config.session_state,
165
- dependencies=self.config.dependencies,
166
- metadata=self.config.metadata,
162
+ run_context=self.config.run_context,
167
163
  )
168
164
 
169
165
  def _get_default_reasoning_agent(self, model: Model) -> Optional["Agent"]:
@@ -183,9 +179,7 @@ class ReasoningManager:
183
179
  telemetry=self.config.telemetry,
184
180
  debug_mode=self.config.debug_mode,
185
181
  debug_level=self.config.debug_level,
186
- session_state=self.config.session_state,
187
- dependencies=self.config.dependencies,
188
- metadata=self.config.metadata,
182
+ run_context=self.config.run_context,
189
183
  )
190
184
 
191
185
  def is_native_reasoning_model(self, model: Optional[Model] = None) -> bool:
agno/reasoning/ollama.py CHANGED
@@ -86,7 +86,7 @@ def get_ollama_reasoning_stream(
86
86
  reasoning_content: str = ""
87
87
 
88
88
  try:
89
- for event in reasoning_agent.run(input=messages, stream=True, stream_intermediate_steps=True):
89
+ for event in reasoning_agent.run(input=messages, stream=True, stream_events=True):
90
90
  if hasattr(event, "event"):
91
91
  if event.event == RunEvent.run_content:
92
92
  # Check for reasoning_content attribute first (native reasoning)
@@ -132,7 +132,7 @@ async def aget_ollama_reasoning_stream(
132
132
  reasoning_content: str = ""
133
133
 
134
134
  try:
135
- async for event in reasoning_agent.arun(input=messages, stream=True, stream_intermediate_steps=True):
135
+ async for event in reasoning_agent.arun(input=messages, stream=True, stream_events=True):
136
136
  if hasattr(event, "event"):
137
137
  if event.event == RunEvent.run_content:
138
138
  # Check for reasoning_content attribute first (native reasoning)
agno/reasoning/openai.py CHANGED
@@ -112,7 +112,7 @@ def get_openai_reasoning_stream(
112
112
  reasoning_content: str = ""
113
113
 
114
114
  try:
115
- for event in reasoning_agent.run(input=messages, stream=True, stream_intermediate_steps=True):
115
+ for event in reasoning_agent.run(input=messages, stream=True, stream_events=True):
116
116
  if hasattr(event, "event"):
117
117
  if event.event == RunEvent.run_content:
118
118
  # Check for reasoning_content attribute first (native reasoning)
@@ -168,7 +168,7 @@ async def aget_openai_reasoning_stream(
168
168
  reasoning_content: str = ""
169
169
 
170
170
  try:
171
- async for event in reasoning_agent.arun(input=messages, stream=True, stream_intermediate_steps=True):
171
+ async for event in reasoning_agent.arun(input=messages, stream=True, stream_events=True):
172
172
  if hasattr(event, "event"):
173
173
  if event.event == RunEvent.run_content:
174
174
  # Check for reasoning_content attribute first (native reasoning)
@@ -94,7 +94,7 @@ def get_vertexai_reasoning_stream(
94
94
  redacted_reasoning_content: Optional[str] = None
95
95
 
96
96
  try:
97
- for event in reasoning_agent.run(input=messages, stream=True, stream_intermediate_steps=True):
97
+ for event in reasoning_agent.run(input=messages, stream=True, stream_events=True):
98
98
  if hasattr(event, "event"):
99
99
  if event.event == RunEvent.run_content:
100
100
  # Stream reasoning content as it arrives
@@ -136,7 +136,7 @@ async def aget_vertexai_reasoning_stream(
136
136
  redacted_reasoning_content: Optional[str] = None
137
137
 
138
138
  try:
139
- async for event in reasoning_agent.arun(input=messages, stream=True, stream_intermediate_steps=True):
139
+ async for event in reasoning_agent.arun(input=messages, stream=True, stream_events=True):
140
140
  if hasattr(event, "event"):
141
141
  if event.event == RunEvent.run_content:
142
142
  # Stream reasoning content as it arrives
@@ -0,0 +1,3 @@
1
+ from agno.registry.registry import Registry
2
+
3
+ __all__ = ["Registry"]