agno 2.3.20__py3-none-any.whl → 2.3.22__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 (58) hide show
  1. agno/agent/agent.py +26 -1
  2. agno/agent/remote.py +233 -72
  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/postgres/async_postgres.py +162 -40
  11. agno/db/postgres/postgres.py +181 -31
  12. agno/db/postgres/utils.py +6 -2
  13. agno/eval/agent_as_judge.py +24 -14
  14. agno/knowledge/chunking/document.py +3 -2
  15. agno/knowledge/chunking/markdown.py +8 -3
  16. agno/knowledge/chunking/recursive.py +2 -2
  17. agno/knowledge/embedder/mistral.py +1 -1
  18. agno/models/openai/chat.py +1 -1
  19. agno/models/openai/responses.py +14 -7
  20. agno/os/middleware/jwt.py +66 -27
  21. agno/os/routers/agents/router.py +2 -2
  22. agno/os/routers/evals/evals.py +0 -9
  23. agno/os/routers/evals/utils.py +6 -6
  24. agno/os/routers/knowledge/knowledge.py +3 -3
  25. agno/os/routers/teams/router.py +2 -2
  26. agno/os/routers/workflows/router.py +2 -2
  27. agno/reasoning/deepseek.py +11 -1
  28. agno/reasoning/gemini.py +6 -2
  29. agno/reasoning/groq.py +8 -3
  30. agno/reasoning/openai.py +2 -0
  31. agno/remote/base.py +105 -8
  32. agno/run/agent.py +19 -19
  33. agno/run/team.py +19 -19
  34. agno/skills/__init__.py +17 -0
  35. agno/skills/agent_skills.py +370 -0
  36. agno/skills/errors.py +32 -0
  37. agno/skills/loaders/__init__.py +4 -0
  38. agno/skills/loaders/base.py +27 -0
  39. agno/skills/loaders/local.py +216 -0
  40. agno/skills/skill.py +65 -0
  41. agno/skills/utils.py +107 -0
  42. agno/skills/validator.py +277 -0
  43. agno/team/remote.py +219 -59
  44. agno/team/team.py +22 -2
  45. agno/tools/mcp/mcp.py +299 -17
  46. agno/tools/mcp/multi_mcp.py +269 -14
  47. agno/utils/mcp.py +49 -8
  48. agno/utils/string.py +43 -1
  49. agno/workflow/condition.py +4 -2
  50. agno/workflow/loop.py +20 -1
  51. agno/workflow/remote.py +172 -32
  52. agno/workflow/router.py +4 -1
  53. agno/workflow/steps.py +4 -0
  54. {agno-2.3.20.dist-info → agno-2.3.22.dist-info}/METADATA +59 -130
  55. {agno-2.3.20.dist-info → agno-2.3.22.dist-info}/RECORD +58 -44
  56. {agno-2.3.20.dist-info → agno-2.3.22.dist-info}/WHEEL +0 -0
  57. {agno-2.3.20.dist-info → agno-2.3.22.dist-info}/licenses/LICENSE +0 -0
  58. {agno-2.3.20.dist-info → agno-2.3.22.dist-info}/top_level.txt +0 -0
@@ -13,6 +13,7 @@ from agno.models.message import Citations, Message, UrlCitation
13
13
  from agno.models.metrics import Metrics
14
14
  from agno.models.response import ModelResponse
15
15
  from agno.run.agent import RunOutput
16
+ from agno.tools.function import Function
16
17
  from agno.utils.http import get_default_async_client, get_default_sync_client
17
18
  from agno.utils.log import log_debug, log_error, log_warning
18
19
  from agno.utils.models.openai_responses import images_to_message
@@ -364,19 +365,25 @@ class OpenAIResponses(Model):
364
365
  return vector_store.id
365
366
 
366
367
  def _format_tool_params(
367
- self, messages: List[Message], tools: Optional[List[Dict[str, Any]]] = None
368
+ self, messages: List[Message], tools: Optional[List[Union[Function, Dict[str, Any]]]] = None
368
369
  ) -> List[Dict[str, Any]]:
369
370
  """Format the tool parameters for the OpenAI Responses API."""
370
371
  formatted_tools = []
371
372
  if tools:
372
373
  for _tool in tools:
373
- if _tool.get("type") == "function":
374
+ if isinstance(_tool, Function):
375
+ _tool_dict = _tool.to_dict()
376
+ _tool_dict["type"] = "function"
377
+ for prop in _tool_dict.get("parameters", {}).get("properties", {}).values():
378
+ if isinstance(prop.get("type", ""), list):
379
+ prop["type"] = prop["type"][0]
380
+ formatted_tools.append(_tool_dict)
381
+ elif _tool.get("type") == "function":
374
382
  _tool_dict = _tool.get("function", {})
375
383
  _tool_dict["type"] = "function"
376
384
  for prop in _tool_dict.get("parameters", {}).get("properties", {}).values():
377
385
  if isinstance(prop.get("type", ""), list):
378
386
  prop["type"] = prop["type"][0]
379
-
380
387
  formatted_tools.append(_tool_dict)
381
388
  else:
382
389
  formatted_tools.append(_tool)
@@ -395,7 +402,7 @@ class OpenAIResponses(Model):
395
402
 
396
403
  # Add the file IDs to the tool parameters
397
404
  for _tool in formatted_tools:
398
- if _tool["type"] == "file_search" and vector_store_id is not None:
405
+ if _tool.get("type", "") == "file_search" and vector_store_id is not None:
399
406
  _tool["vector_store_ids"] = [vector_store_id]
400
407
 
401
408
  return formatted_tools
@@ -524,12 +531,12 @@ class OpenAIResponses(Model):
524
531
  def count_tokens(
525
532
  self,
526
533
  messages: List[Message],
527
- tools: Optional[List[Dict[str, Any]]] = None,
534
+ tools: Optional[List[Union[Function, Dict[str, Any]]]] = None,
528
535
  output_schema: Optional[Union[Dict, Type[BaseModel]]] = None,
529
536
  ) -> int:
530
537
  try:
531
538
  formatted_input = self._format_messages(messages, compress_tool_results=True)
532
- formatted_tools = self._format_tool_params(messages, tools) if tools else None
539
+ formatted_tools = self._format_tool_params(messages, tools) if tools is not None else None
533
540
 
534
541
  response = self.get_client().responses.input_tokens.count(
535
542
  model=self.id,
@@ -545,7 +552,7 @@ class OpenAIResponses(Model):
545
552
  async def acount_tokens(
546
553
  self,
547
554
  messages: List[Message],
548
- tools: Optional[List[Dict[str, Any]]] = None,
555
+ tools: Optional[List[Union[Function, Dict[str, Any]]]] = None,
549
556
  output_schema: Optional[Union[Dict, Type[BaseModel]]] = None,
550
557
  ) -> int:
551
558
  """Async version of count_tokens using the async client."""
agno/os/middleware/jwt.py CHANGED
@@ -5,7 +5,7 @@ import json
5
5
  import re
6
6
  from enum import Enum
7
7
  from os import getenv
8
- from typing import Any, Dict, List, Optional
8
+ from typing import Any, Dict, Iterable, List, Optional, Union
9
9
 
10
10
  import jwt
11
11
  from fastapi import Request, Response
@@ -168,7 +168,9 @@ class JWTValidator:
168
168
  except Exception as e:
169
169
  log_warning(f"Failed to parse JWKS key: {e}")
170
170
 
171
- def validate_token(self, token: str, expected_audience: Optional[str] = None) -> Dict[str, Any]:
171
+ def validate_token(
172
+ self, token: str, expected_audience: Optional[Union[str, Iterable[str]]] = None
173
+ ) -> Dict[str, Any]:
172
174
  """
173
175
  Validate JWT token and extract claims.
174
176
 
@@ -191,10 +193,9 @@ class JWTValidator:
191
193
  }
192
194
 
193
195
  # Configure audience verification
194
- if expected_audience:
195
- decode_kwargs["audience"] = expected_audience
196
- else:
197
- decode_options["verify_aud"] = False
196
+ # We'll decode without audience verification and if we need to verify the audience,
197
+ # we'll manually verify the audience to provide better error messages
198
+ decode_options["verify_aud"] = False
198
199
 
199
200
  # If validation is disabled, decode without signature verification
200
201
  if not self.validate:
@@ -206,6 +207,7 @@ class JWTValidator:
206
207
  decode_kwargs["options"] = decode_options
207
208
 
208
209
  last_exception: Optional[Exception] = None
210
+ payload: Optional[Dict[str, Any]] = None
209
211
 
210
212
  # Try JWKS keys first if configured
211
213
  if self.jwks_keys:
@@ -222,9 +224,7 @@ class JWTValidator:
222
224
  jwk = self.jwks_keys["_default"]
223
225
 
224
226
  if jwk:
225
- return jwt.decode(token, jwk.key, **decode_kwargs)
226
- except jwt.InvalidAudienceError:
227
- raise
227
+ payload = jwt.decode(token, jwk.key, **decode_kwargs)
228
228
  except jwt.ExpiredSignatureError:
229
229
  raise
230
230
  except jwt.InvalidTokenError as e:
@@ -233,20 +233,54 @@ class JWTValidator:
233
233
  last_exception = e
234
234
 
235
235
  # Try each static verification key until one succeeds
236
- for key in self.verification_keys:
237
- try:
238
- return jwt.decode(token, key, **decode_kwargs)
239
- except jwt.InvalidAudienceError:
240
- raise
241
- except jwt.ExpiredSignatureError:
242
- raise
243
- except jwt.InvalidTokenError as e:
244
- last_exception = e
245
- continue
236
+ if payload is None:
237
+ for key in self.verification_keys:
238
+ try:
239
+ payload = jwt.decode(token, key, **decode_kwargs)
240
+ break
241
+ except jwt.ExpiredSignatureError:
242
+ raise
243
+ except jwt.InvalidTokenError as e:
244
+ last_exception = e
245
+ continue
246
246
 
247
- if last_exception:
248
- raise last_exception
249
- raise jwt.InvalidTokenError("No verification keys configured")
247
+ if payload is None:
248
+ if last_exception:
249
+ raise last_exception
250
+ raise jwt.InvalidTokenError("No verification keys configured")
251
+
252
+ # Manually verify audience if expected_audience was provided
253
+ if expected_audience:
254
+ token_audience = payload.get(self.audience_claim)
255
+ if token_audience is None:
256
+ raise jwt.InvalidTokenError(
257
+ f'Token is missing the "{self.audience_claim}" claim. '
258
+ f"Audience verification requires this claim to be present in the token."
259
+ )
260
+
261
+ # Normalize expected_audience to a list
262
+ if isinstance(expected_audience, str):
263
+ expected_audiences = [expected_audience]
264
+ elif isinstance(expected_audience, Iterable):
265
+ expected_audiences = list(expected_audience)
266
+ else:
267
+ expected_audiences = []
268
+
269
+ # Normalize token_audience to a list
270
+ if isinstance(token_audience, str):
271
+ token_audiences = [token_audience]
272
+ elif isinstance(token_audience, list):
273
+ token_audiences = token_audience
274
+ else:
275
+ token_audiences = [token_audience] if token_audience else []
276
+
277
+ # Check if any token audience matches any expected audience
278
+ if not any(aud in expected_audiences for aud in token_audiences):
279
+ raise jwt.InvalidAudienceError(
280
+ f"Invalid audience. Expected one of: {expected_audiences}, got: {token_audiences}"
281
+ )
282
+
283
+ return payload
250
284
 
251
285
  def extract_claims(self, payload: Dict[str, Any]) -> Dict[str, Any]:
252
286
  """
@@ -364,6 +398,7 @@ class JWTMiddleware(BaseHTTPMiddleware):
364
398
  user_id_claim: str = "sub",
365
399
  session_id_claim: str = "session_id",
366
400
  audience_claim: str = "aud",
401
+ audience: Optional[Union[str, Iterable[str]]] = None,
367
402
  verify_audience: bool = False,
368
403
  dependencies_claims: Optional[List[str]] = None,
369
404
  session_state_claims: Optional[List[str]] = None,
@@ -400,7 +435,8 @@ class JWTMiddleware(BaseHTTPMiddleware):
400
435
  user_id_claim: JWT claim name for user ID (default: "sub")
401
436
  session_id_claim: JWT claim name for session ID (default: "session_id")
402
437
  audience_claim: JWT claim name for audience/OS ID (default: "aud")
403
- verify_audience: Whether to verify the audience claim matches AgentOS ID (default: False)
438
+ audience: Optional expected audience claim to validate against the token's audience claim (default: AgentOS ID)
439
+ verify_audience: Whether to verify the token's audience claim matches the expected audience claim (default: False)
404
440
  dependencies_claims: A list of claims to extract from the JWT token for dependencies
405
441
  session_state_claims: A list of claims to extract from the JWT token for session state
406
442
  scope_mappings: Optional dictionary mapping route patterns to required scopes.
@@ -453,6 +489,8 @@ class JWTMiddleware(BaseHTTPMiddleware):
453
489
  self.dependencies_claims: List[str] = dependencies_claims or []
454
490
  self.session_state_claims: List[str] = session_state_claims or []
455
491
 
492
+ self.audience = audience
493
+
456
494
  # RBAC configuration (opt-in via scope_mappings)
457
495
  self.authorization = authorization
458
496
 
@@ -648,7 +686,9 @@ class JWTMiddleware(BaseHTTPMiddleware):
648
686
 
649
687
  try:
650
688
  # Validate token and extract claims (with audience verification if configured)
651
- expected_audience = agent_os_id if self.verify_audience else None
689
+ expected_audience = None
690
+ if self.verify_audience:
691
+ expected_audience = self.audience or agent_os_id
652
692
  payload: Dict[str, Any] = self.validator.validate_token(token, expected_audience) # type: ignore
653
693
 
654
694
  # Extract standard claims and store in request.state
@@ -755,11 +795,10 @@ class JWTMiddleware(BaseHTTPMiddleware):
755
795
  request.state.authenticated = True
756
796
 
757
797
  except jwt.InvalidAudienceError:
758
- log_warning(f"Invalid audience - expected: {agent_os_id}")
798
+ log_warning(f"Invalid token audience - expected: {expected_audience}")
759
799
  return self._create_error_response(
760
- 401, "Invalid audience - token not valid for this AgentOS instance", origin, cors_allowed_origins
800
+ 401, "Invalid token audience - token not valid for this AgentOS instance", origin, cors_allowed_origins
761
801
  )
762
-
763
802
  except jwt.ExpiredSignatureError as e:
764
803
  if self.validate:
765
804
  log_warning(f"Token has expired: {str(e)}")
@@ -220,11 +220,11 @@ def get_agent_router(
220
220
  kwargs = await get_request_kwargs(request, create_agent_run)
221
221
 
222
222
  if hasattr(request.state, "user_id") and request.state.user_id is not None:
223
- if user_id:
223
+ if user_id and user_id != request.state.user_id:
224
224
  log_warning("User ID parameter passed in both request state and kwargs, using request state")
225
225
  user_id = request.state.user_id
226
226
  if hasattr(request.state, "session_id") and request.state.session_id is not None:
227
- if session_id:
227
+ if session_id and session_id != request.state.session_id:
228
228
  log_warning("Session ID parameter passed in both request state and kwargs, using request state")
229
229
  session_id = request.state.session_id
230
230
  if hasattr(request.state, "session_state") and request.state.session_state is not None:
@@ -144,15 +144,6 @@ def attach_routes(
144
144
  headers=headers,
145
145
  )
146
146
 
147
- # TODO: Delete me:
148
- # Filtering out agent-as-judge by default for now,
149
- # as they are not supported yet in the AgentOS UI.
150
- eval_types = eval_types or [
151
- EvalType.ACCURACY,
152
- EvalType.PERFORMANCE,
153
- EvalType.RELIABILITY,
154
- ]
155
-
156
147
  if isinstance(db, AsyncBaseDb):
157
148
  db = cast(AsyncBaseDb, db)
158
149
  eval_runs, total_count = await db.get_eval_runs(
@@ -68,15 +68,11 @@ async def run_agent_as_judge_eval(
68
68
  if agent:
69
69
  agent_response = await agent.arun(eval_run_input.input, stream=False)
70
70
  output = str(agent_response.content) if agent_response.content else ""
71
- model_id = agent.model.id if agent and agent.model else None
72
- model_provider = agent.model.provider if agent and agent.model else None
73
71
  agent_id = agent.id
74
72
  team_id = None
75
73
  elif team:
76
74
  team_response = await team.arun(eval_run_input.input, stream=False)
77
75
  output = str(team_response.content) if team_response.content else ""
78
- model_id = team.model.id if team and team.model else None
79
- model_provider = team.model.provider if team and team.model else None
80
76
  agent_id = None
81
77
  team_id = team.id
82
78
  else:
@@ -98,13 +94,17 @@ async def run_agent_as_judge_eval(
98
94
  if not result:
99
95
  raise HTTPException(status_code=500, detail="Failed to run agent as judge evaluation")
100
96
 
97
+ # Use evaluator's model
98
+ eval_model_id = agent_as_judge_eval.model.id if agent_as_judge_eval.model is not None else None
99
+ eval_model_provider = agent_as_judge_eval.model.provider if agent_as_judge_eval.model is not None else None
100
+
101
101
  eval_run = EvalSchema.from_agent_as_judge_eval(
102
102
  agent_as_judge_eval=agent_as_judge_eval,
103
103
  result=result,
104
104
  agent_id=agent_id,
105
105
  team_id=team_id,
106
- model_id=model_id,
107
- model_provider=model_provider,
106
+ model_id=eval_model_id,
107
+ model_provider=eval_model_provider,
108
108
  )
109
109
 
110
110
  # Restore original model after eval
@@ -297,7 +297,7 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Union[Knowledge,
297
297
  else:
298
298
  raise HTTPException(status_code=400, detail=f"Invalid reader_id: {update_data.reader_id}")
299
299
 
300
- updated_content_dict = knowledge.patch_content(content)
300
+ updated_content_dict = await knowledge.apatch_content(content)
301
301
  if not updated_content_dict:
302
302
  raise HTTPException(status_code=404, detail=f"Content not found: {content_id}")
303
303
 
@@ -1029,13 +1029,13 @@ def attach_routes(router: APIRouter, knowledge_instances: List[Union[Knowledge,
1029
1029
  search_types=search_types,
1030
1030
  )
1031
1031
  )
1032
-
1032
+ filters = await knowledge.async_get_valid_filters()
1033
1033
  return ConfigResponseSchema(
1034
1034
  readers=reader_schemas,
1035
1035
  vector_dbs=vector_dbs,
1036
1036
  readersForType=types_of_readers,
1037
1037
  chunkers=chunkers_dict,
1038
- filters=knowledge.get_valid_filters(),
1038
+ filters=filters,
1039
1039
  )
1040
1040
 
1041
1041
  return router
@@ -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:
@@ -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:
@@ -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,