JuliusBrussee/caveman · error · TypeError
scope resolver must return a Caveman Scope
Error message
scope resolver must return a Caveman Scope
What it means
_scope normalizes either a Scope instance or a callable resolver (invoked with ensure_config(config)) and enforces the result is a Caveman Scope. Any other return type — dict, tuple, None — cannot carry namespace/thread/branch identity, so a TypeError is raised. It backs state, recover, arecover, binding, and _options.
Solutions
- Make the resolver return a caveman_cloud.middleware Scope instance for every code path.
- Or pass a Scope instance directly instead of a callable.
- Check imports: use Scope from caveman_cloud.middleware, not another module.
Example fix
// before
def resolver(config): return {'thread': config['configurable']['thread_id']}
// after
from caveman_cloud.middleware import Scope
def resolver(config): return Scope('ns', config['configurable']['thread_id']) Defensive patterns
Strategy: type-guard
Validate before calling
from caveman_cloud.middleware import Scope resolved = scope if isinstance(scope, Scope) else scope(config) assert isinstance(resolved, Scope), 'resolver must return caveman Scope'
Type guard
def resolves_to_scope(resolver, config):
from caveman_cloud.middleware import Scope
r = resolver if isinstance(resolver, Scope) else resolver(config)
return isinstance(r, Scope) Try / catch
try:
out = chain.invoke(messages, config=config)
except TypeError as e:
if 'Caveman Scope' in str(e):
raise RuntimeError('scope resolver misconfigured; return caveman Scope') from e
raise Prevention
- Import Scope only from caveman_cloud.middleware.
- Ensure custom resolvers return Scope on every branch, including error paths.
- Unit-test resolvers with ensure_config({}) as the minimal config.
When it happens
Trigger: Passing a custom scope resolver callable to the LangChain adapter that returns something other than Scope (e.g. a dict, a tuple, or None on some branch), or returning a similarly-named Scope class from a different library.
Common situations: Custom resolver written before upgrading the library (old Scope signature); resolver that returns None when a config key is missing; importing Scope from the wrong package.
Understand the failure class
Background: "is not a compatible type" / "cannot merge" errors: when a value's type doesn't match what the library requires — this error's family across 65 libraries.
Related errors
- Expected a native LangChain BaseChatModel
- Synchronous LangChain calls require MiddlewareRuntime
- Agno middleware requires a nonempty native session_id
- Agno scope resolver must return a Caveman Scope
- ASGI context must come from authenticated server state
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/1303c611ddf0c20a.
Report an issue: GitHub.
Appendix: source
Thrown at packages/middleware/python/caveman_middleware/langchain.py:43
from ._native import Attempt, manifest, owner, plain
from ._versions import matches_framework
ADAPTER = Adapter("langchain", "0.1.0", "1.4.0", "langchain-message-v1")
def scope_from_config(config: RunnableConfig, *, namespace: str) -> Scope:
"""Use the caller's checkpoint thread and explicit branch/epoch identity."""
values = config.get("configurable", {})
thread = values.get("thread_id")
if not isinstance(thread, str) or not thread:
raise ValueError("LangGraph middleware requires a nonempty configurable.thread_id")
return Scope(namespace, thread, values.get("caveman_branch_id", "main"), values.get("caveman_cache_epoch", "0"))
def _scope(source, config=None):
result = source if isinstance(source, Scope) else source(ensure_config(config))
if not isinstance(result, Scope):
raise TypeError("scope resolver must return a Caveman Scope")
return result
def _message_view(messages, prefix=()):
try:
context = manifest([m.model_dump(mode="json") for m in [*prefix, *messages]])
except (TypeError, ValueError, AttributeError):
return None
if context is None:
return None
candidates, setters = [], {}
names = {call["id"]: call["name"] for message in messages for call in getattr(message, "tool_calls", [])
if plain(call) and type(call.get("id")) is str and type(call.get("name")) is str}
for mi, message in enumerate(messages):
if type(message) is not ToolMessage or message.name == "caveman_retrieve" or names.get(message.tool_call_id) == "caveman_retrieve" or message.status == "error":
continue
if not message.name and message.tool_call_id not in names:
continueView on GitHub (pinned to 3ee70a1026)