[BUG] fetch_agent_card(use_cache=False) still returns a cached AgentCard
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 58.8k
- Forks
- 8.5k
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 109
Description
Description
fetch_agent_card(endpoint, use_cache=False) still serves the AgentCard from a cache. The
explicit opt-out is dropped on the way to the implementation, so a caller who asks for a
fresh card can get one that is up to 5 minutes stale.
The uncached branch of the sync entry point delegates to the async entry point without
forwarding use_cache:
def fetch_agent_card(
endpoint: str,
auth: ClientAuthScheme | None = None,
timeout: int = 30,
use_cache: bool = True, # <-- caller passes False
cache_ttl: int = 300,
) -> AgentCard:
if use_cache:
...
return _fetch_agent_card_cached(endpoint, auth_hash, timeout, ttl_hash)
coro = afetch_agent_card(endpoint=endpoint, auth=auth, timeout=timeout)
# ^ use_cache not forwarded
afetch_agent_card declares use_cache: bool = True, so the request re-enters the cached
branch and is answered by _afetch_agent_card_cached, which carries
@cached(ttl=300, serializer=PickleSerializer()). The use_cache=False argument is
silently reinterpreted as use_cache=True.
Reproduction
Patch the network layer with a counter that returns a distinguishable card on every real
fetch, then compare the three code paths:
import asyncio
from a2a.types import AgentCard
from crewai.a2a.utils import agent_card as m
calls = []
async def _impl(endpoint, auth, timeout):
calls.append(endpoint)
return AgentCard.model_validate({
"name": f"probe-agent-{len(calls)}", "description": "probe",
"url": "http://example.com/", "version": "1", "protocol_version": "0.3.0",
"capabilities": {}, "default_input_modes": ["text/plain"],
"default_output_modes": ["text/plain"], "skills": [],
})
m._afetch_agent_card_impl = _impl
def probe(label, fn):
calls.clear()
a, b = fn()
print(f"{label:24} -> real fetches: {len(calls)} | names: {a.name} {b.name}")
probe("sync use_cache=False", lambda: (
m.fetch_agent_card("http://a/.well-known/agent-card.json", use_cache=False),
m.fetch_agent_card("http://a/.well-known/agent-card.json", use_cache=False)))
probe("sync use_cache=True", lambda: (
m.fetch_agent_card("http://b/.well-known/agent-card.json", use_cache=True),
m.fetch_agent_card("http://b/.well-known/agent-card.json", use_cache=True)))
probe("async use_cache=False", lambda: asyncio.run(_two_async()))
Observed on main (ceed4a3):
| path | real fetches | card names returned | correct? |
|---|---|---|---|
fetch_agent_card(..., use_cache=False) |
1 | probe-agent-1, probe-agent-1 |
no |
fetch_agent_card(..., use_cache=True) |
1 | probe-agent-1, probe-agent-1 |
yes |
await afetch_agent_card(..., use_cache=False) |
2 | probe-agent-1, probe-agent-2 |
yes |
The third row is what makes this a bug rather than a design choice: the async entry point
already treats use_cache=False as meaningful, so the two entry points contradict each
other on the same argument. use_cache=False on the sync path is indistinguishable from
use_cache=True.
Expected behaviour
fetch_agent_card(..., use_cache=False) performs a real fetch on every call, matching
afetch_agent_card(..., use_cache=False).
Why this matters in normal use
Both in-tree call sites (lib/crewai/src/crewai/a2a/utils/wrapper.py:252 in
_fetch_card_from_config, and :1426 in _afetch_card_from_config) rely on the default,
so the defect is only reachable when a user explicitly asks for a fresh card — which is
exactly when staleness is the thing they were guarding against:
- A remote agent's capabilities changed. An AgentCard advertises
skillsand
capabilities. After a peer redeploys with a new skill set, a caller who passes
use_cache=Falseto pick up the change keeps routing against the old card for the
remainder of the 300-second TTL. - Auth rotation.
_afetch_agent_card_cachedis keyed on a hash of the auth scheme, so
a rotated credential does produce a new key — but a caller that re-fetches with
unchanged auth to confirm a peer is reachable gets the cached card and no request
leaves the process, so a peer that has gone away looks healthy. - Tests and health checks. Any code that fetches twice with
use_cache=Falseto
observe a change cannot observe it.
The failure is silent: no exception, no warning, and the returned object is a
well-formed AgentCard — just the wrong one.
Fix
Forward the argument:
coro = afetch_agent_card(
endpoint=endpoint, auth=auth, timeout=timeout, use_cache=False
)
I have a PR ready with this one-line change plus three regression tests (one for the bug,
two controls proving the cached path and the async path are unaffected).
Related, not fixed here
fetch_agent_card accepts cache_ttl: int = 300 and honours it — the sync cache derives
ttl_hash = int(time.time() // cache_ttl) as a cache-key component, which I confirmed
works (cache_ttl=1, two calls 1.1s apart → 2 real fetches). afetch_agent_card has no
cache_ttl parameter at all; its cache is a hardcoded @cached(ttl=300). So an async
caller cannot tune the TTL the way a sync caller can. Fixing that means reworking the
decorator rather than forwarding an argument, so it is a feature change and I have left it
out of the PR — flagging it here in case a maintainer wants it tracked.
Environment
- crewAI
main@ceed4a3ff71b5b4cb0ca316b4178ffcce74a53b2 - Python 3.12,
a2a-sdk~=0.3.10
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in lib/crewai/src/crewai/a2a/utils/agent_card.py at the sync fetch_agent_card entry point around line 143, and compare its delegation with afetch_agent_card. Done means the sync use_cache=False path performs a real fetch on every call while the cached sync path and async path remain unchanged; the issue describes three regression tests to verify these cases.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, backend
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 35/100