{"record":{"id":"1303c611ddf0c20a","repo":"JuliusBrussee/caveman","slug":"scope-resolver-must-return-a-caveman-scope","errorCode":null,"errorMessage":"scope resolver must return a Caveman Scope","messagePattern":"scope resolver must return a Caveman Scope","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"packages/middleware/python/caveman_middleware/langchain.py","lineNumber":43,"sourceCode":"from ._native import Attempt, manifest, owner, plain\nfrom ._versions import matches_framework\n\nADAPTER = Adapter(\"langchain\", \"0.1.0\", \"1.4.0\", \"langchain-message-v1\")\n\n\ndef scope_from_config(config: RunnableConfig, *, namespace: str) -> Scope:\n    \"\"\"Use the caller's checkpoint thread and explicit branch/epoch identity.\"\"\"\n    values = config.get(\"configurable\", {})\n    thread = values.get(\"thread_id\")\n    if not isinstance(thread, str) or not thread:\n        raise ValueError(\"LangGraph middleware requires a nonempty configurable.thread_id\")\n    return Scope(namespace, thread, values.get(\"caveman_branch_id\", \"main\"), values.get(\"caveman_cache_epoch\", \"0\"))\n\n\ndef _scope(source, config=None):\n    result = source if isinstance(source, Scope) else source(ensure_config(config))\n    if not isinstance(result, Scope):\n        raise TypeError(\"scope resolver must return a Caveman Scope\")\n    return result\n\n\ndef _message_view(messages, prefix=()):\n    try:\n        context = manifest([m.model_dump(mode=\"json\") for m in [*prefix, *messages]])\n    except (TypeError, ValueError, AttributeError):\n        return None\n    if context is None:\n        return None\n    candidates, setters = [], {}\n    names = {call[\"id\"]: call[\"name\"] for message in messages for call in getattr(message, \"tool_calls\", [])\n             if plain(call) and type(call.get(\"id\")) is str and type(call.get(\"name\")) is str}\n    for mi, message in enumerate(messages):\n        if type(message) is not ToolMessage or message.name == \"caveman_retrieve\" or names.get(message.tool_call_id) == \"caveman_retrieve\" or message.status == \"error\":\n            continue\n        if not message.name and message.tool_call_id not in names:\n            continue","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/middleware/python/caveman_middleware/langchain.py#L25-L61","documentation":"_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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\ndef resolver(config): return {'thread': config['configurable']['thread_id']}\n// after\nfrom caveman_cloud.middleware import Scope\ndef resolver(config): return Scope('ns', config['configurable']['thread_id'])","handlingStrategy":"type-guard","validationCode":"from caveman_cloud.middleware import Scope\nresolved = scope if isinstance(scope, Scope) else scope(config)\nassert isinstance(resolved, Scope), 'resolver must return caveman Scope'","typeGuard":"def resolves_to_scope(resolver, config):\n    from caveman_cloud.middleware import Scope\n    r = resolver if isinstance(resolver, Scope) else resolver(config)\n    return isinstance(r, Scope)","tryCatchPattern":"try:\n    out = chain.invoke(messages, config=config)\nexcept TypeError as e:\n    if 'Caveman Scope' in str(e):\n        raise RuntimeError('scope resolver misconfigured; return caveman Scope') from e\n    raise","preventionTips":["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."],"tags":["type-mismatch","langchain","scope","python"],"backgroundTag":"incompatible-source-type","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}