{"record":{"id":"21e93d737d672e60","repo":"JuliusBrussee/caveman","slug":"langgraph-middleware-requires-a-nonempty-configurable-thread","errorCode":null,"errorMessage":"LangGraph middleware requires a nonempty configurable.thread_id","messagePattern":"LangGraph middleware requires a nonempty configurable\\.thread_id","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"packages/middleware/python/caveman_middleware/langchain.py","lineNumber":36,"sourceCode":"    from langchain_core.runnables import RunnableConfig, ensure_config\n    from langchain_core.tools import StructuredTool\nexcept ModuleNotFoundError as error:\n    raise ImportError(\"Install caveman-middleware[langchain] to use the LangChain adapter\") from error\n\nfrom caveman_cloud.middleware import Adapter, Candidate, Scope, MiddlewareRuntime, RecoveryBinding\nfrom caveman_cloud.middleware.runtime import RECOVERY_DESCRIPTION, RECOVERY_SCHEMA\nfrom ._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 = [], {}","sourceCodeStart":18,"sourceCodeEnd":54,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/middleware/python/caveman_middleware/langchain.py#L18-L54","documentation":"scope_from_config builds a Caveman Scope from a LangGraph RunnableConfig and keys the checkpoint identity on configurable.thread_id. A missing, empty, or non-string thread_id would make traces/recovery indistinguishable across threads, so a ValueError is raised.","triggerScenarios":"Passing a RunnableConfig whose 'configurable' dict lacks thread_id, has thread_id=None or '', or a non-string (e.g. int/UUID object) — via invoke/config/callbacks or the scope resolver path.","commonSituations":"Calling model.invoke() without config; building config manually instead of through LangGraph's checkpointer; using a UUID object instead of str(uuid); LangGraph migrations where thread_id moved to another key.","solutions":["Pass config={'configurable': {'thread_id': '<nonempty string>'}} to invoke/ainvoke.","Convert non-string ids: thread_id=str(thread_uuid).","Let LangGraph's checkpointer produce the config instead of hand-building it."],"exampleFix":"// before\nmodel.invoke(messages)\n// after\nmodel.invoke(messages, config={'configurable': {'thread_id': 'thread-123'}})","handlingStrategy":"validation","validationCode":"cfg = config.get('configurable', {})\nthread = cfg.get('thread_id')\nif not isinstance(thread, str) or not thread:\n    raise ValueError('configurable.thread_id must be a nonempty string')","typeGuard":"def has_thread_id(config): \n    t = (config or {}).get('configurable', {}).get('thread_id')\n    return isinstance(t, str) and bool(t)","tryCatchPattern":"try:\n    result = model.invoke(messages, config=config)\nexcept ValueError as e:\n    if 'thread_id' in str(e):\n        config = {'configurable': {'thread_id': str(uuid4())}, **(config or {})}\n        result = model.invoke(messages, config=config)\n    else:\n        raise","preventionTips":["Always route invokes through LangGraph's checkpointer-supplied config.","Coerce ids with str(...) at config-build time, never pass UUID/int directly.","Add a small helper that defaults thread_id to a generated value when absent."],"tags":["validation","langgraph","config","python"],"backgroundTag":"empty-required-field","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"}