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

  1. Pass config={'configurable': {'thread_id': '<nonempty string>'}} to invoke/ainvoke.
  2. Convert non-string ids: thread_id=str(thread_uuid).
  3. 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

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


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)