{"record":{"id":"7ba115d4fe88134d","repo":"JuliusBrussee/caveman","slug":"pydantic-ai-middleware-requires-a-native-conversation-id","errorCode":null,"errorMessage":"Pydantic AI middleware requires a native conversation_id","messagePattern":"Pydantic AI middleware requires a native conversation_id","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"packages/middleware/python/caveman_middleware/pydantic_ai.py","lineNumber":38,"sourceCode":"    from pydantic_ai.models import Model, ModelRequestContext, ModelRequestParameters\n    from pydantic_ai.models.wrapper import WrapperModel\n    from pydantic_ai.toolsets import FunctionToolset\n    from pydantic_core import PydanticSerializationError\nexcept ModuleNotFoundError as error:\n    raise ImportError(\"Install caveman-middleware[pydantic-ai] to use the Pydantic AI adapter\") from error\n\nfrom caveman_cloud.middleware import Adapter, Candidate, MiddlewareError, MiddlewareRuntime, Scope\nfrom caveman_cloud.middleware.runtime import RECOVERY_DESCRIPTION, RECOVERY_SCHEMA\nfrom ._native import Attempt, manifest, owner\nfrom ._versions import supports_framework\n\nADAPTER = Adapter(\"pydantic-ai\", \"0.1.0\", \"2.42.0\", \"pydantic-ai-message-v1\")\n\n\ndef scope_from_run(ctx: RunContext, *, namespace: str) -> Scope:\n    \"\"\"Use native conversation identity plus application-owned branch metadata.\"\"\"\n    if not isinstance(ctx.conversation_id, str) or not ctx.conversation_id:\n        raise ValueError(\"Pydantic AI middleware requires a native conversation_id\")\n    metadata = ctx.metadata or {}\n    return Scope(namespace, ctx.conversation_id, metadata.get(\"caveman_branch_id\", \"main\"),\n                 metadata.get(\"caveman_cache_epoch\", \"0\"))\n\n\ndef _scope(source, ctx=None):\n    scope = source if isinstance(source, Scope) else source(ctx)\n    if not isinstance(scope, Scope):\n        raise TypeError(\"Pydantic AI scope resolver must return a Caveman Scope\")\n    return scope\n\n\ndef _runtime(runtime):\n    return runtime.as_async() if isinstance(runtime, MiddlewareRuntime) else runtime\n\n\ndef _check_version(runtime):\n    return supports_framework(runtime, (\"pydantic-ai-slim\", \"2.42\", \"3\"))","sourceCodeStart":20,"sourceCodeEnd":56,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/middleware/python/caveman_middleware/pydantic_ai.py#L20-L56","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure the Pydantic AI run has a conversation_id before invoking the middleware (enable the framework's conversation/session id generation)","Construct test RunContext objects with a non-empty conversation_id string","Pass a custom scope resolver to the adapter instead of scope_from_run if your app supplies its own conversation identity"],"exampleFix":"// before\nscope = scope_from_run(ctx, namespace=\"app\")  # ctx.conversation_id is None\n// after\nif ctx.conversation_id:\n    scope = scope_from_run(ctx, namespace=\"app\")\nelse:\n    ctx.conversation_id = str(uuid4())\n    scope = scope_from_run(ctx, namespace=\"app\")","handlingStrategy":"validation","validationCode":"if not isinstance(getattr(ctx, \"conversation_id\", None), str) or not ctx.conversation_id:\n    raise ValueError(\"RunContext needs a non-empty conversation_id before middleware scoping\")","typeGuard":"def has_conversation_id(ctx) -> bool:\n    return isinstance(getattr(ctx, \"conversation_id\", None), str) and bool(ctx.conversation_id)","tryCatchPattern":"try:\n    scope = scope_from_run(ctx, namespace=\"app\")\nexcept ValueError as e:\n    if \"conversation_id\" in str(e):\n        ctx.conversation_id = str(uuid4())\n        scope = scope_from_run(ctx, namespace=\"app\")\n    else:\n        raise","preventionTips":["Enable Pydantic AI conversation/session ids in your agent setup","In tests, always set a non-empty conversation_id on RunContext fixtures","After upgrading pydantic-ai, verify conversation_id is populated on your run paths","Or supply your own resolver returning a Scope built from app-side identity"],"tags":["python","pydantic-ai","scope","missing-identifier"],"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"}