JuliusBrussee/caveman · error · ValueError

Pydantic AI middleware requires a native conversation_id

Error message

Pydantic AI middleware requires a native conversation_id

What it means

scope_from_run builds a Caveman Scope from a Pydantic AI RunContext and requires the framework's native conversation_id to identify the conversation. Pydantic AI omitted or supplied an empty conversation_id, so no stable scope key can be derived and a ValueError is raised.

Solutions

  1. Ensure the Pydantic AI run has a conversation_id before invoking the middleware (enable the framework's conversation/session id generation)
  2. Construct test RunContext objects with a non-empty conversation_id string
  3. Pass a custom scope resolver to the adapter instead of scope_from_run if your app supplies its own conversation identity

Example fix

// before
scope = scope_from_run(ctx, namespace="app")  # ctx.conversation_id is None
// after
if ctx.conversation_id:
    scope = scope_from_run(ctx, namespace="app")
else:
    ctx.conversation_id = str(uuid4())
    scope = scope_from_run(ctx, namespace="app")
Defensive patterns

Strategy: validation

Validate before calling

if not isinstance(getattr(ctx, "conversation_id", None), str) or not ctx.conversation_id:
    raise ValueError("RunContext needs a non-empty conversation_id before middleware scoping")

Type guard

def has_conversation_id(ctx) -> bool:
    return isinstance(getattr(ctx, "conversation_id", None), str) and bool(ctx.conversation_id)

Try / catch

try:
    scope = scope_from_run(ctx, namespace="app")
except ValueError as e:
    if "conversation_id" in str(e):
        ctx.conversation_id = str(uuid4())
        scope = scope_from_run(ctx, namespace="app")
    else:
        raise

Prevention

When it happens

Trigger: Calling scope_from_run(ctx, namespace=...) with ctx.conversation_id set to None or "" — typically when the RunContext was constructed manually or the framework version does not populate conversation_id for this run path.

Common situations: Unit tests building RunContext fixtures without conversation_id; upgrading pydantic-ai to a version where conversation_id is populated later in the run lifecycle; custom agent loops that never set a conversation id.

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/7ba115d4fe88134d. Report an issue: GitHub.

Appendix: source

Thrown at packages/middleware/python/caveman_middleware/pydantic_ai.py:38

    from pydantic_ai.models import Model, ModelRequestContext, ModelRequestParameters
    from pydantic_ai.models.wrapper import WrapperModel
    from pydantic_ai.toolsets import FunctionToolset
    from pydantic_core import PydanticSerializationError
except ModuleNotFoundError as error:
    raise ImportError("Install caveman-middleware[pydantic-ai] to use the Pydantic AI adapter") from error

from caveman_cloud.middleware import Adapter, Candidate, MiddlewareError, MiddlewareRuntime, Scope
from caveman_cloud.middleware.runtime import RECOVERY_DESCRIPTION, RECOVERY_SCHEMA
from ._native import Attempt, manifest, owner
from ._versions import supports_framework

ADAPTER = Adapter("pydantic-ai", "0.1.0", "2.42.0", "pydantic-ai-message-v1")


def scope_from_run(ctx: RunContext, *, namespace: str) -> Scope:
    """Use native conversation identity plus application-owned branch metadata."""
    if not isinstance(ctx.conversation_id, str) or not ctx.conversation_id:
        raise ValueError("Pydantic AI middleware requires a native conversation_id")
    metadata = ctx.metadata or {}
    return Scope(namespace, ctx.conversation_id, metadata.get("caveman_branch_id", "main"),
                 metadata.get("caveman_cache_epoch", "0"))


def _scope(source, ctx=None):
    scope = source if isinstance(source, Scope) else source(ctx)
    if not isinstance(scope, Scope):
        raise TypeError("Pydantic AI scope resolver must return a Caveman Scope")
    return scope


def _runtime(runtime):
    return runtime.as_async() if isinstance(runtime, MiddlewareRuntime) else runtime


def _check_version(runtime):
    return supports_framework(runtime, ("pydantic-ai-slim", "2.42", "3"))

View on GitHub (pinned to 3ee70a1026)