agno 2.3.21__py3-none-any.whl → 2.3.23__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 (74) hide show
  1. agno/agent/agent.py +48 -2
  2. agno/agent/remote.py +234 -73
  3. agno/client/a2a/__init__.py +10 -0
  4. agno/client/a2a/client.py +554 -0
  5. agno/client/a2a/schemas.py +112 -0
  6. agno/client/a2a/utils.py +369 -0
  7. agno/db/migrations/utils.py +19 -0
  8. agno/db/migrations/v1_to_v2.py +54 -16
  9. agno/db/migrations/versions/v2_3_0.py +92 -53
  10. agno/db/mysql/async_mysql.py +5 -7
  11. agno/db/mysql/mysql.py +5 -7
  12. agno/db/mysql/schemas.py +39 -21
  13. agno/db/postgres/async_postgres.py +172 -42
  14. agno/db/postgres/postgres.py +186 -38
  15. agno/db/postgres/schemas.py +39 -21
  16. agno/db/postgres/utils.py +6 -2
  17. agno/db/singlestore/schemas.py +41 -21
  18. agno/db/singlestore/singlestore.py +14 -3
  19. agno/db/sqlite/async_sqlite.py +7 -2
  20. agno/db/sqlite/schemas.py +36 -21
  21. agno/db/sqlite/sqlite.py +3 -7
  22. agno/knowledge/chunking/document.py +3 -2
  23. agno/knowledge/chunking/markdown.py +8 -3
  24. agno/knowledge/chunking/recursive.py +2 -2
  25. agno/models/base.py +4 -0
  26. agno/models/google/gemini.py +27 -4
  27. agno/models/openai/chat.py +1 -1
  28. agno/models/openai/responses.py +14 -7
  29. agno/os/middleware/jwt.py +66 -27
  30. agno/os/routers/agents/router.py +3 -3
  31. agno/os/routers/evals/evals.py +2 -2
  32. agno/os/routers/knowledge/knowledge.py +5 -5
  33. agno/os/routers/knowledge/schemas.py +1 -1
  34. agno/os/routers/memory/memory.py +4 -4
  35. agno/os/routers/session/session.py +2 -2
  36. agno/os/routers/teams/router.py +4 -4
  37. agno/os/routers/traces/traces.py +3 -3
  38. agno/os/routers/workflows/router.py +3 -3
  39. agno/os/schema.py +1 -1
  40. agno/reasoning/deepseek.py +11 -1
  41. agno/reasoning/gemini.py +6 -2
  42. agno/reasoning/groq.py +8 -3
  43. agno/reasoning/openai.py +2 -0
  44. agno/remote/base.py +106 -9
  45. agno/skills/__init__.py +17 -0
  46. agno/skills/agent_skills.py +370 -0
  47. agno/skills/errors.py +32 -0
  48. agno/skills/loaders/__init__.py +4 -0
  49. agno/skills/loaders/base.py +27 -0
  50. agno/skills/loaders/local.py +216 -0
  51. agno/skills/skill.py +65 -0
  52. agno/skills/utils.py +107 -0
  53. agno/skills/validator.py +277 -0
  54. agno/team/remote.py +220 -60
  55. agno/team/team.py +41 -3
  56. agno/tools/brandfetch.py +27 -18
  57. agno/tools/browserbase.py +150 -13
  58. agno/tools/function.py +6 -1
  59. agno/tools/mcp/mcp.py +300 -17
  60. agno/tools/mcp/multi_mcp.py +269 -14
  61. agno/tools/toolkit.py +89 -21
  62. agno/utils/mcp.py +49 -8
  63. agno/utils/string.py +43 -1
  64. agno/workflow/condition.py +4 -2
  65. agno/workflow/loop.py +20 -1
  66. agno/workflow/remote.py +173 -33
  67. agno/workflow/router.py +4 -1
  68. agno/workflow/steps.py +4 -0
  69. agno/workflow/workflow.py +14 -0
  70. {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/METADATA +13 -14
  71. {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/RECORD +74 -60
  72. {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/WHEEL +0 -0
  73. {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/licenses/LICENSE +0 -0
  74. {agno-2.3.21.dist-info → agno-2.3.23.dist-info}/top_level.txt +0 -0
@@ -156,7 +156,7 @@ class VectorSearchRequestSchema(BaseModel):
156
156
  class Meta(BaseModel):
157
157
  """Inline metadata schema for pagination."""
158
158
 
159
- limit: int = Field(20, description="Number of results per page", ge=1, le=100)
159
+ limit: int = Field(20, description="Number of results per page", ge=1)
160
160
  page: int = Field(1, description="Page number", ge=1)
161
161
 
162
162
  query: str = Field(..., description="The search query text")
@@ -256,8 +256,8 @@ def attach_routes(router: APIRouter, dbs: dict[str, list[Union[BaseDb, AsyncBase
256
256
  team_id: Optional[str] = Query(default=None, description="Filter memories by team ID"),
257
257
  topics: Optional[List[str]] = Depends(parse_topics),
258
258
  search_content: Optional[str] = Query(default=None, description="Fuzzy search within memory content"),
259
- limit: Optional[int] = Query(default=20, description="Number of memories to return per page"),
260
- page: Optional[int] = Query(default=1, description="Page number for pagination"),
259
+ limit: Optional[int] = Query(default=20, description="Number of memories to return per page", ge=1),
260
+ page: Optional[int] = Query(default=1, description="Page number for pagination", ge=0),
261
261
  sort_by: Optional[str] = Query(default="updated_at", description="Field to sort memories by"),
262
262
  sort_order: Optional[SortOrder] = Query(default="desc", description="Sort order (asc or desc)"),
263
263
  db_id: Optional[str] = Query(default=None, description="Database ID to query memories from"),
@@ -558,8 +558,8 @@ def attach_routes(router: APIRouter, dbs: dict[str, list[Union[BaseDb, AsyncBase
558
558
  )
559
559
  async def get_user_memory_stats(
560
560
  request: Request,
561
- limit: Optional[int] = Query(default=20, description="Number of user statistics to return per page"),
562
- page: Optional[int] = Query(default=1, description="Page number for pagination"),
561
+ limit: Optional[int] = Query(default=20, description="Number of user statistics to return per page", ge=1),
562
+ page: Optional[int] = Query(default=1, description="Page number for pagination", ge=0),
563
563
  db_id: Optional[str] = Query(default=None, description="Database ID to query statistics from"),
564
564
  table: Optional[str] = Query(default=None, description="Table to query statistics from"),
565
565
  ) -> PaginatedResponse[UserStatsSchema]:
@@ -107,8 +107,8 @@ def attach_routes(router: APIRouter, dbs: dict[str, list[Union[BaseDb, AsyncBase
107
107
  ),
108
108
  user_id: Optional[str] = Query(default=None, description="Filter sessions by user ID"),
109
109
  session_name: Optional[str] = Query(default=None, description="Filter sessions by name (partial match)"),
110
- limit: Optional[int] = Query(default=20, description="Number of sessions to return per page"),
111
- page: Optional[int] = Query(default=1, description="Page number for pagination"),
110
+ limit: Optional[int] = Query(default=20, description="Number of sessions to return per page", ge=1),
111
+ page: Optional[int] = Query(default=1, description="Page number for pagination", ge=0),
112
112
  sort_by: Optional[str] = Query(default="created_at", description="Field to sort sessions by"),
113
113
  sort_order: Optional[SortOrder] = Query(default="desc", description="Sort order (asc or desc)"),
114
114
  db_id: Optional[str] = Query(default=None, description="Database ID to query sessions from"),
@@ -95,7 +95,7 @@ async def team_response_streamer(
95
95
  )
96
96
  yield format_sse_event(error_response)
97
97
 
98
- except Exception as e:
98
+ except BaseException as e:
99
99
  import traceback
100
100
 
101
101
  traceback.print_exc()
@@ -169,11 +169,11 @@ def get_team_router(
169
169
  kwargs = await get_request_kwargs(request, create_team_run)
170
170
 
171
171
  if hasattr(request.state, "user_id") and request.state.user_id is not None:
172
- if user_id:
172
+ if user_id and user_id != request.state.user_id:
173
173
  log_warning("User ID parameter passed in both request state and kwargs, using request state")
174
174
  user_id = request.state.user_id
175
175
  if hasattr(request.state, "session_id") and request.state.session_id is not None:
176
- if session_id:
176
+ if session_id and session_id != request.state.session_id:
177
177
  log_warning("Session ID parameter passed in both request state and kwargs, using request state")
178
178
  session_id = request.state.session_id
179
179
  if hasattr(request.state, "session_state") and request.state.session_state is not None:
@@ -325,7 +325,7 @@ def get_team_router(
325
325
  if team is None:
326
326
  raise HTTPException(status_code=404, detail="Team not found")
327
327
 
328
- cancelled = team.cancel_run(run_id=run_id)
328
+ cancelled = await team.acancel_run(run_id=run_id)
329
329
  if not cancelled:
330
330
  raise HTTPException(status_code=500, detail="Failed to cancel run - run not found or already completed")
331
331
 
@@ -126,8 +126,8 @@ def attach_routes(router: APIRouter, dbs: dict[str, list[Union[BaseDb, AsyncBase
126
126
  default=None,
127
127
  description="Filter traces ending before this time (ISO 8601 format with timezone, e.g., '2025-11-19T11:00:00Z' or '2025-11-19T16:30:00+05:30'). Times are converted to UTC for comparison.",
128
128
  ),
129
- page: int = Query(default=1, description="Page number (1-indexed)", ge=1),
130
- limit: int = Query(default=20, description="Number of traces per page", ge=1, le=100),
129
+ page: int = Query(default=1, description="Page number (1-indexed)", ge=0),
130
+ limit: int = Query(default=20, description="Number of traces per page", ge=1),
131
131
  db_id: Optional[str] = Query(default=None, description="Database ID to query traces from"),
132
132
  ):
133
133
  """Get list of traces with optional filters and pagination"""
@@ -455,7 +455,7 @@ def attach_routes(router: APIRouter, dbs: dict[str, list[Union[BaseDb, AsyncBase
455
455
  description="Filter sessions with traces created before this time (ISO 8601 format with timezone, e.g., '2025-11-19T11:00:00Z' or '2025-11-19T16:30:00+05:30'). Times are converted to UTC for comparison.",
456
456
  ),
457
457
  page: int = Query(default=1, description="Page number (1-indexed)", ge=1),
458
- limit: int = Query(default=20, description="Number of sessions per page", ge=1, le=100),
458
+ limit: int = Query(default=20, description="Number of sessions per page", ge=1),
459
459
  db_id: Optional[str] = Query(default=None, description="Database ID to query statistics from"),
460
460
  ):
461
461
  """Get trace statistics grouped by session"""
@@ -626,11 +626,11 @@ def get_workflow_router(
626
626
  kwargs = await get_request_kwargs(request, create_workflow_run)
627
627
 
628
628
  if hasattr(request.state, "user_id") and request.state.user_id is not None:
629
- if user_id:
629
+ if user_id and user_id != request.state.user_id:
630
630
  log_warning("User ID parameter passed in both request state and kwargs, using request state")
631
631
  user_id = request.state.user_id
632
632
  if hasattr(request.state, "session_id") and request.state.session_id is not None:
633
- if session_id:
633
+ if session_id and session_id != request.state.session_id:
634
634
  log_warning("Session ID parameter passed in both request state and kwargs, using request state")
635
635
  session_id = request.state.session_id
636
636
  if hasattr(request.state, "session_state") and request.state.session_state is not None:
@@ -721,7 +721,7 @@ def get_workflow_router(
721
721
  if workflow is None:
722
722
  raise HTTPException(status_code=404, detail="Workflow not found")
723
723
 
724
- cancelled = workflow.cancel_run(run_id=run_id)
724
+ cancelled = await workflow.acancel_run(run_id=run_id)
725
725
  if not cancelled:
726
726
  raise HTTPException(status_code=500, detail="Failed to cancel run - run not found or already completed")
727
727
 
agno/os/schema.py CHANGED
@@ -549,7 +549,7 @@ class SortOrder(str, Enum):
549
549
 
550
550
  class PaginationInfo(BaseModel):
551
551
  page: int = Field(0, description="Current page number (0-indexed)", ge=0)
552
- limit: int = Field(20, description="Number of items per page", ge=1, le=100)
552
+ limit: int = Field(20, description="Number of items per page", ge=1)
553
553
  total_pages: int = Field(0, description="Total number of pages", ge=0)
554
554
  total_count: int = Field(0, description="Total count of items", ge=0)
555
555
  search_time_ms: float = Field(0, description="Search execution time in milliseconds", ge=0)
@@ -8,7 +8,17 @@ from agno.utils.log import logger
8
8
 
9
9
 
10
10
  def is_deepseek_reasoning_model(reasoning_model: Model) -> bool:
11
- return reasoning_model.__class__.__name__ == "DeepSeek" and reasoning_model.id.lower() == "deepseek-reasoner"
11
+ """Check if the model is a DeepSeek reasoning model.
12
+
13
+ Matches:
14
+ - deepseek-reasoner
15
+ - deepseek-r1 and variants (deepseek-r1-distill-*, etc.)
16
+ """
17
+ if reasoning_model.__class__.__name__ != "DeepSeek":
18
+ return False
19
+
20
+ model_id = reasoning_model.id.lower()
21
+ return "reasoner" in model_id or "r1" in model_id
12
22
 
13
23
 
14
24
  def get_deepseek_reasoning(reasoning_agent: "Agent", messages: List[Message]) -> Optional[Message]: # type: ignore # noqa: F821
agno/reasoning/gemini.py CHANGED
@@ -13,9 +13,13 @@ def is_gemini_reasoning_model(reasoning_model: Model) -> bool:
13
13
  if not is_gemini_class:
14
14
  return False
15
15
 
16
- # Check if it's a Gemini 2.5+ model (supports thinking)
16
+ # Check if it's a Gemini model with thinking support
17
+ # - Gemini 2.5+ models support thinking
18
+ # - Gemini 3+ models support thinking (including DeepThink variants)
17
19
  model_id = reasoning_model.id.lower()
18
- has_thinking_support = "2.5" in model_id
20
+ has_thinking_support = (
21
+ "2.5" in model_id or "3.0" in model_id or "3.5" in model_id or "deepthink" in model_id or "gemini-3" in model_id
22
+ )
19
23
 
20
24
  # Also check if thinking parameters are set
21
25
  # Note: thinking_budget=0 explicitly disables thinking mode per Google's API docs
agno/reasoning/groq.py CHANGED
@@ -8,7 +8,12 @@ from agno.utils.log import logger
8
8
 
9
9
 
10
10
  def is_groq_reasoning_model(reasoning_model: Model) -> bool:
11
- return reasoning_model.__class__.__name__ == "Groq" and "deepseek" in reasoning_model.id.lower()
11
+ return reasoning_model.__class__.__name__ == "Groq" and (
12
+ "deepseek" in reasoning_model.id.lower()
13
+ or "openai/gpt-oss-20b" in reasoning_model.id.lower()
14
+ or "openai/gpt-oss-120b" in reasoning_model.id.lower()
15
+ or "qwen/qwen3-32b" in reasoning_model.id.lower()
16
+ )
12
17
 
13
18
 
14
19
  def get_groq_reasoning(reasoning_agent: "Agent", messages: List[Message]) -> Optional[Message]: # type: ignore # noqa: F821
@@ -95,7 +100,7 @@ def get_groq_reasoning_stream(
95
100
  reasoning_content: str = ""
96
101
 
97
102
  try:
98
- for event in reasoning_agent.run(input=messages, stream=True, stream_intermediate_steps=True):
103
+ for event in reasoning_agent.run(input=messages, stream=True, stream_events=True):
99
104
  if hasattr(event, "event"):
100
105
  if event.event == RunEvent.run_content:
101
106
  # Check for reasoning_content attribute first (native reasoning)
@@ -146,7 +151,7 @@ async def aget_groq_reasoning_stream(
146
151
  reasoning_content: str = ""
147
152
 
148
153
  try:
149
- async for event in reasoning_agent.arun(input=messages, stream=True, stream_intermediate_steps=True):
154
+ async for event in reasoning_agent.arun(input=messages, stream=True, stream_events=True):
150
155
  if hasattr(event, "event"):
151
156
  if event.event == RunEvent.run_content:
152
157
  # Check for reasoning_content attribute first (native reasoning)
agno/reasoning/openai.py CHANGED
@@ -21,6 +21,8 @@ def is_openai_reasoning_model(reasoning_model: Model) -> bool:
21
21
  or ("o1" in reasoning_model.id)
22
22
  or ("4.1" in reasoning_model.id)
23
23
  or ("4.5" in reasoning_model.id)
24
+ or ("5.1" in reasoning_model.id)
25
+ or ("5.2" in reasoning_model.id)
24
26
  )
25
27
  ) or (isinstance(reasoning_model, OpenAILike) and "deepseek-r1" in reasoning_model.id.lower())
26
28
 
agno/remote/base.py CHANGED
@@ -2,7 +2,7 @@ import time
2
2
  from abc import abstractmethod
3
3
  from dataclasses import dataclass, field
4
4
  from datetime import date
5
- from typing import TYPE_CHECKING, Any, AsyncIterator, Dict, List, Optional, Sequence, Tuple, Union
5
+ from typing import TYPE_CHECKING, Any, AsyncIterator, Dict, List, Literal, Optional, Sequence, Tuple, Union
6
6
 
7
7
  from pydantic import BaseModel
8
8
 
@@ -18,6 +18,8 @@ if TYPE_CHECKING:
18
18
  from fastapi import UploadFile
19
19
 
20
20
  from agno.client import AgentOSClient
21
+ from agno.client.a2a import A2AClient
22
+ from agno.client.a2a.schemas import AgentCard
21
23
  from agno.os.routers.evals.schemas import EvalSchema
22
24
  from agno.os.routers.knowledge.schemas import (
23
25
  ConfigResponseSchema,
@@ -64,7 +66,7 @@ class RemoteDb:
64
66
  """Create a RemoteDb instance from an AgentResponse/TeamResponse/WorkflowResponse and ConfigResponse.
65
67
 
66
68
  Args:
67
- response: The agent, team, or workflow response containing the db_id.
69
+ db_id (str): The id of the remote database
68
70
  client: The AgentOSClient for remote operations.
69
71
  config: The ConfigResponse containing database table information.
70
72
 
@@ -344,31 +346,52 @@ class RemoteKnowledge:
344
346
  class BaseRemote:
345
347
  # Private cache for OS config with TTL: (config, timestamp)
346
348
  _cached_config: Optional[Tuple["ConfigResponse", float]] = field(default=None, init=False, repr=False)
349
+ # Private cache for agent card with TTL: (agent_card, timestamp)
350
+ _cached_agent_card: Optional[Tuple[Optional["AgentCard"], float]] = field(default=None, init=False, repr=False)
347
351
 
348
352
  def __init__(
349
353
  self,
350
354
  base_url: str,
351
355
  timeout: float = 60.0,
356
+ protocol: Literal["agentos", "a2a"] = "agentos",
357
+ a2a_protocol: Literal["json-rpc", "rest"] = "rest",
352
358
  config_ttl: float = 300.0,
353
359
  ):
354
360
  """Initialize BaseRemote for remote execution.
355
361
 
362
+ Supports two protocols:
363
+ - "agentos": Agno's proprietary AgentOS REST API (default)
364
+ - "a2a": A2A (Agent-to-Agent) protocol for cross-framework communication
365
+
356
366
  For local execution, provide agent/team/workflow instances.
357
367
  For remote execution, provide base_url.
358
368
 
359
369
  Args:
360
370
  base_url: Base URL for remote instance (e.g., "http://localhost:7777")
361
371
  timeout: Request timeout in seconds (default: 60)
372
+ protocol: Communication protocol - "agentos" (default) or "a2a"
373
+ a2a_protocol: For A2A protocol only - Whether to use JSON-RPC or REST protocol.
362
374
  config_ttl: Time-to-live for cached config in seconds (default: 300)
363
375
  """
364
376
  self.base_url = base_url.rstrip("/")
365
377
  self.timeout: float = timeout
378
+ self.protocol = protocol
379
+ self.a2a_protocol = a2a_protocol
366
380
  self.config_ttl: float = config_ttl
367
381
  self._cached_config = None
382
+ self._cached_agent_card = None
383
+
384
+ self.agentos_client = None
385
+ self.a2a_client = None
368
386
 
369
- self.client = self.get_client()
387
+ if protocol == "agentos":
388
+ self.agentos_client = self.get_os_client()
389
+ elif protocol == "a2a":
390
+ self.a2a_client = self.get_a2a_client()
391
+ else:
392
+ raise ValueError(f"Invalid protocol: {protocol}")
370
393
 
371
- def get_client(self) -> "AgentOSClient":
394
+ def get_os_client(self) -> "AgentOSClient":
372
395
  """Get an AgentOSClient for fetching remote configuration.
373
396
 
374
397
  This is used internally by AgentOS to fetch configuration from remote
@@ -384,11 +407,31 @@ class BaseRemote:
384
407
  timeout=self.timeout,
385
408
  )
386
409
 
410
+ def get_a2a_client(self) -> "A2AClient":
411
+ """Get an A2AClient for A2A protocol communication.
412
+
413
+ Returns cached client if available, otherwise creates a new one.
414
+ This method provides lazy initialization of the A2A client.
415
+
416
+ Returns:
417
+ A2AClient: Client configured for A2A protocol communication
418
+ """
419
+ from agno.client.a2a import A2AClient
420
+
421
+ return A2AClient(
422
+ base_url=self.base_url,
423
+ timeout=int(self.timeout),
424
+ protocol=self.a2a_protocol,
425
+ )
426
+
387
427
  @property
388
- def _config(self) -> "ConfigResponse":
428
+ def _config(self) -> Optional["ConfigResponse"]:
389
429
  """Get the OS config from remote, cached with TTL."""
390
430
  from agno.os.schema import ConfigResponse
391
431
 
432
+ if self.protocol == "a2a":
433
+ return None
434
+
392
435
  current_time = time.time()
393
436
 
394
437
  # Check if cache is valid
@@ -398,15 +441,15 @@ class BaseRemote:
398
441
  return config
399
442
 
400
443
  # Fetch fresh config
401
- config: ConfigResponse = self.client.get_config() # type: ignore
444
+ config: ConfigResponse = self.agentos_client.get_config() # type: ignore
402
445
  self._cached_config = (config, current_time)
403
446
  return config
404
447
 
405
- def refresh_os_config(self) -> "ConfigResponse":
448
+ async def refresh_os_config(self) -> "ConfigResponse":
406
449
  """Force refresh the cached OS config."""
407
450
  from agno.os.schema import ConfigResponse
408
451
 
409
- config: ConfigResponse = self.client.get_config()
452
+ config: ConfigResponse = await self.agentos_client.aget_config() # type: ignore
410
453
  self._cached_config = (config, time.time())
411
454
  return config
412
455
 
@@ -437,6 +480,60 @@ class BaseRemote:
437
480
  return {"Authorization": f"Bearer {auth_token}"}
438
481
  return None
439
482
 
483
+ def get_agent_card(self) -> Optional["AgentCard"]:
484
+ """Get agent card for A2A protocol agents, cached with TTL.
485
+
486
+ Fetches the agent card from the standard /.well-known/agent.json endpoint
487
+ to populate agent metadata (name, description, etc.) for A2A agents.
488
+
489
+ Returns None for non-A2A protocols or if the server doesn't support agent cards.
490
+ """
491
+ if self.protocol != "a2a":
492
+ return None
493
+
494
+ current_time = time.time()
495
+
496
+ # Check if cache is valid
497
+ if self._cached_agent_card is not None:
498
+ agent_card, cached_at = self._cached_agent_card
499
+ if current_time - cached_at < self.config_ttl:
500
+ return agent_card
501
+
502
+ try:
503
+ agent_card = self.a2a_client.get_agent_card() # type: ignore
504
+ self._cached_agent_card = (agent_card, current_time)
505
+ return agent_card
506
+ except Exception:
507
+ self._cached_agent_card = (None, current_time)
508
+ return None
509
+
510
+ async def aget_agent_card(self) -> Optional["AgentCard"]:
511
+ """Get agent card for A2A protocol agents, cached with TTL.
512
+
513
+ Fetches the agent card from the standard /.well-known/agent.json endpoint
514
+ to populate agent metadata (name, description, etc.) for A2A agents.
515
+
516
+ Returns None for non-A2A protocols or if the server doesn't support agent cards.
517
+ """
518
+ if self.protocol != "a2a":
519
+ return None
520
+
521
+ current_time = time.time()
522
+
523
+ # Check if cache is valid
524
+ if self._cached_agent_card is not None:
525
+ agent_card, cached_at = self._cached_agent_card
526
+ if current_time - cached_at < self.config_ttl:
527
+ return agent_card
528
+
529
+ try:
530
+ agent_card = await self.a2a_client.aget_agent_card() # type: ignore
531
+ self._cached_agent_card = (agent_card, current_time)
532
+ return agent_card
533
+ except Exception:
534
+ self._cached_agent_card = (None, current_time)
535
+ return None
536
+
440
537
  @abstractmethod
441
538
  def arun( # type: ignore
442
539
  self,
@@ -480,5 +577,5 @@ class BaseRemote:
480
577
  raise NotImplementedError("acontinue_run method must be implemented by the subclass")
481
578
 
482
579
  @abstractmethod
483
- async def cancel_run(self, run_id: str) -> bool:
580
+ async def acancel_run(self, run_id: str) -> bool:
484
581
  raise NotImplementedError("cancel_run method must be implemented by the subclass")
@@ -0,0 +1,17 @@
1
+ from agno.skills.agent_skills import Skills
2
+ from agno.skills.errors import SkillError, SkillParseError, SkillValidationError
3
+ from agno.skills.loaders import LocalSkills, SkillLoader
4
+ from agno.skills.skill import Skill
5
+ from agno.skills.validator import validate_metadata, validate_skill_directory
6
+
7
+ __all__ = [
8
+ "Skills",
9
+ "LocalSkills",
10
+ "SkillLoader",
11
+ "Skill",
12
+ "SkillError",
13
+ "SkillParseError",
14
+ "SkillValidationError",
15
+ "validate_metadata",
16
+ "validate_skill_directory",
17
+ ]