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

  1. Return a Scope(namespace, session, branch_id, cache_epoch) from your resolver (import from caveman_cloud.middleware)
  2. Or pass a Scope instance directly instead of a callable if the scope is static
  3. Audit all return paths of the custom resolver so none return None/partial data
  4. 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

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


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)