JuliusBrussee/caveman · error · TypeError

Pydantic AI scope resolver must return a Caveman Scope

Error message

Pydantic AI scope resolver must return a Caveman Scope

What it means

The Pydantic AI adapter accepts either a Scope instance or a callable resolver that returns one. _scope validates the resolver's output; returning any other object (dict, tuple, None) means the middleware cannot address sessions, so a TypeError is raised.

Solutions

  1. Make the resolver construct and return a Scope(namespace, conversation_id, branch_id, cache_epoch)
  2. Ensure every code path in the resolver returns a Scope, including error paths
  3. Verify Scope is imported from caveman_cloud.middleware, not a look-alike class

Example fix

// before
def my_scope(ctx):
    return {"namespace": "app", "id": ctx.conversation_id}
// after
def my_scope(ctx):
    return Scope("app", ctx.conversation_id, "main", "0")
Defensive patterns

Strategy: validation

Validate before calling

from caveman_cloud.middleware import Scope
result = my_scope_resolver(ctx)
assert isinstance(result, Scope), "resolver must return a Caveman Scope"

Type guard

def is_caveman_scope(x) -> bool:
    return isinstance(x, Scope)

Try / catch

try:
    model = CavemanModel(wrapped, runtime=rt, scope=my_scope_resolver)
except TypeError as e:
    if "must return a Caveman Scope" in str(e):
        my_scope_resolver = make_default_scope_resolver(namespace="app")
        model = CavemanModel(wrapped, runtime=rt, scope=my_scope_resolver)
    else:
        raise

Prevention

When it happens

Trigger: Passing a scope resolver callable to the Pydantic AI adapter that returns something other than caveman_cloud.middleware.Scope (e.g. a dict of namespace/conversation_id, or forgetting the return statement).

Common situations: Hand-rolled resolver functions written against an older adapter API that returned tuples; a resolver that conditionally returns None on cache miss; typos importing Scope so isinstance always fails.

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/979e2f7b867b0457. Report an issue: GitHub.

Appendix: source

Thrown at packages/middleware/python/caveman_middleware/pydantic_ai.py:47

from ._native import Attempt, manifest, owner
from ._versions import supports_framework

ADAPTER = Adapter("pydantic-ai", "0.1.0", "2.42.0", "pydantic-ai-message-v1")


def scope_from_run(ctx: RunContext, *, namespace: str) -> Scope:
    """Use native conversation identity plus application-owned branch metadata."""
    if not isinstance(ctx.conversation_id, str) or not ctx.conversation_id:
        raise ValueError("Pydantic AI middleware requires a native conversation_id")
    metadata = ctx.metadata or {}
    return Scope(namespace, ctx.conversation_id, metadata.get("caveman_branch_id", "main"),
                 metadata.get("caveman_cache_epoch", "0"))


def _scope(source, ctx=None):
    scope = source if isinstance(source, Scope) else source(ctx)
    if not isinstance(scope, Scope):
        raise TypeError("Pydantic AI scope resolver must return a Caveman Scope")
    return scope


def _runtime(runtime):
    return runtime.as_async() if isinstance(runtime, MiddlewareRuntime) else runtime


def _check_version(runtime):
    return supports_framework(runtime, ("pydantic-ai-slim", "2.42", "3"))


def _protocol(model):
    # Capability-only integration wraps the model selected for this request.
    # Routing containers and other providers stay opaque until separately tested.
    while isinstance(model, WrapperModel):
        model = model.wrapped
    if type(model).__name__ == "OpenAIChatModel" and type(model).__module__ == "pydantic_ai.models.openai":
        return "openai-chat"

View on GitHub (pinned to 3ee70a1026)