JuliusBrussee/caveman · error · TypeError
Agno scope resolver must return a Caveman Scope
Error message
Agno scope resolver must return a Caveman Scope
What it means
_scope normalizes the configured scope resolver: if it is not already a Scope it calls it with the run and requires the result to be a caveman Scope instance. Returning anything else (dict, tuple, None) is a programming error, so it raises TypeError.
Solutions
- Return a Scope(namespace, session, branch_id, cache_epoch) from your resolver (import from caveman_cloud.middleware)
- Or pass a Scope instance directly instead of a callable if the scope is static
- Audit all return paths of the custom resolver so none return None/partial data
- Add an isinstance(result, Scope) assertion in tests around your resolver
Example fix
# before
def resolver(run):
return run.session_id # returns str -> TypeError
# after
from caveman_cloud.middleware import Scope
def resolver(run):
return Scope("my-ns", run.session_id, "main", "0") Defensive patterns
Strategy: validation
Validate before calling
from caveman_cloud.middleware import Scope
def ensure_scope_result(resolver, run):
result = resolver if isinstance(resolver, Scope) else resolver(run)
if not isinstance(result, Scope):
raise TypeError("resolver must return a caveman_cloud Scope")
return result Type guard
def is_scope(obj) -> bool:
return isinstance(obj, Scope) Try / catch
try:
scope = _scope(my_resolver, run)
except TypeError as e:
if "must return a Caveman Scope" in str(e):
raise TypeError(f"{my_resolver.__name__} must return Scope(namespace, session, branch, epoch)") from e
raise Prevention
- Return Scope(...) from custom resolvers; never raw tuples/dicts/strings
- Unit-test every return path of custom resolvers
- Import Scope from caveman_cloud.middleware, not a hand-rolled class
- Annotate resolvers with -> Scope so type checkers catch wrong returns
When it happens
Trigger: Passing a callable to the Agno middleware's scope option that returns a dict/tuple/string/None instead of a caveman_cloud Scope, or a resolver that returns None on some runs.
Common situations: Writing a custom session resolver after upgrading caveman-middleware and returning the old shape; confusing Scope with a plain (namespace, session) tuple; a resolver with early-return paths returning None.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- Agno middleware requires a nonempty native session_id
- AutoGen requires a stable Caveman Scope for each agent or…
- LiteLLM scope must be a trusted Caveman Scope
- Pydantic AI scope resolver must return a Caveman Scope
- Strands scope resolver must return a Caveman Scope
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/b8946a220404e625.
Report an issue: GitHub.
Appendix: source
Thrown at packages/middleware/python/caveman_middleware/agno.py:45
ADAPTER = Adapter("agno", "0.1.0", "3.0.9", "agno-message-v1")
_SIGNATURES = {name: inspect.signature(getattr(Model, name)) for name in
("response", "aresponse", "response_stream", "aresponse_stream")}
def scope_from_run(run, *, namespace: str) -> Scope:
"""Resolve a native Agent or Team run's session, with explicit branch metadata."""
session = getattr(run, "session_id", None)
if not isinstance(session, str) or not session:
raise ValueError("Agno middleware requires a nonempty native session_id")
metadata = getattr(run, "metadata", None) or {}
return Scope(namespace, session, metadata.get("caveman_branch_id", "main"), metadata.get("caveman_cache_epoch", "0"))
def _scope(source, run):
result = source if isinstance(source, Scope) else source(run)
if not isinstance(result, Scope):
raise TypeError("Agno scope resolver must return a Caveman Scope")
return result
def _message_view(messages):
if type(messages) is not list or any(type(message) is not Message for message in messages):
return None
# Metrics/timers and checkpoint bookkeeping are not model input. All other
# native values participate in lineage; opaque values bypass the whole view.
excluded = {"metrics", "created_at", "from_history", "checkpoint_status", "checkpoint_created_at"}
try:
context = manifest([message.model_dump(mode="python", exclude=excluded) for message in messages])
except (TypeError, ValueError):
return None
if context is None:
return None
names = {call["id"]: call["function"]["name"] for message in messages for call in (message.tool_calls or [])
if plain(call) and type(call.get("id")) is str and plain(call.get("function"))
and type(call["function"].get("name")) is str}View on GitHub (pinned to 3ee70a1026)