JuliusBrussee/caveman · error · ValueError
LangGraph middleware requires a nonempty…
Error message
LangGraph middleware requires a nonempty configurable.thread_id
What it means
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.
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.
Example fix
// before
model.invoke(messages)
// after
model.invoke(messages, config={'configurable': {'thread_id': 'thread-123'}}) Defensive patterns
Strategy: validation
Validate before calling
cfg = config.get('configurable', {})
thread = cfg.get('thread_id')
if not isinstance(thread, str) or not thread:
raise ValueError('configurable.thread_id must be a nonempty string') Type guard
def has_thread_id(config):
t = (config or {}).get('configurable', {}).get('thread_id')
return isinstance(t, str) and bool(t) Try / catch
try:
result = model.invoke(messages, config=config)
except ValueError as e:
if 'thread_id' in str(e):
config = {'configurable': {'thread_id': str(uuid4())}, **(config or {})}
result = model.invoke(messages, config=config)
else:
raise Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.
Related errors
- artifact_id is required
- ASGI context resolver or request bounds are invalid
- assemble requires model and session_id
- assembly slot id must be non-empty and unique
- assembly slot has unknown stability
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/21e93d737d672e60.
Report an issue: GitHub.
Appendix: source
Thrown at packages/middleware/python/caveman_middleware/langchain.py:36
from langchain_core.runnables import RunnableConfig, ensure_config
from langchain_core.tools import StructuredTool
except ModuleNotFoundError as error:
raise ImportError("Install caveman-middleware[langchain] to use the LangChain adapter") from error
from caveman_cloud.middleware import Adapter, Candidate, Scope, MiddlewareRuntime, RecoveryBinding
from caveman_cloud.middleware.runtime import RECOVERY_DESCRIPTION, RECOVERY_SCHEMA
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 = [], {}View on GitHub (pinned to 3ee70a1026)