{"record":{"id":"f6d9225c75fb8db4","repo":"JuliusBrussee/caveman","slug":"synchronous-recovery-requires-middlewareruntime","errorCode":null,"errorMessage":"Synchronous recovery requires MiddlewareRuntime","messagePattern":"Synchronous recovery requires MiddlewareRuntime","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"packages/middleware/python/caveman_middleware/langchain.py","lineNumber":174,"sourceCode":"\n    with_caveman_agent() performs collision-safe model/tool registration together.\n    scope may be a fixed Scope or a trusted Callable[[RunnableConfig], Scope].\n    \"\"\"\n    tools = ()\n\n    def __init__(self, *, runtime, scope):\n        self.connection = _Connection(runtime, scope)\n        self.recovery_tool = None\n        if not _supported(runtime):\n            if runtime.mode != \"off\":\n                runtime.decline(\"unsupported_version\")\n            return\n        if runtime.mode == \"off\":\n            return\n\n        def recover(handle: str, config: RunnableConfig, offset: int = 0, limit: int = 262144, query: str = \"\"):\n            if self.connection.sync is None:\n                raise TypeError(\"Synchronous recovery requires MiddlewareRuntime\")\n            return json.dumps(self.connection.sync.retrieve(_scope(scope, config), handle=handle, offset=offset, limit=limit, query=query), ensure_ascii=False, separators=(\",\", \":\"))\n\n        async def arecover(handle: str, config: RunnableConfig, offset: int = 0, limit: int = 262144, query: str = \"\"):\n            return json.dumps(await self.connection.async_runtime.retrieve(_scope(scope, config), handle=handle, offset=offset, limit=limit, query=query), ensure_ascii=False, separators=(\",\", \":\"))\n\n        # Native tools expose mutable schemas and executor fields. Give the\n        # framework its own schema, and attest the callable registration each\n        # time before asking the runtime to issue lossy source grants.\n        schema = copy.deepcopy(RECOVERY_SCHEMA)\n        self.recovery_tool = StructuredTool.from_function(func=recover, coroutine=arecover, name=\"caveman_retrieve\", description=RECOVERY_DESCRIPTION, args_schema=schema)\n        self._recovery_schema = json.dumps(schema, sort_keys=True, allow_nan=False)\n        self._recovery_methods = {name: getattr(self.recovery_tool, name) for name in\n                                  (\"func\", \"coroutine\", \"invoke\", \"ainvoke\", \"run\", \"arun\")}\n\n    def _recovery_intact(self):\n        tool = self.recovery_tool\n        try:\n            return (type(tool) is StructuredTool and tool.name == \"caveman_retrieve\"","sourceCodeStart":156,"sourceCodeEnd":192,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/middleware/python/caveman_middleware/langchain.py#L156-L192","documentation":"Raised by the sync recovery path in the LangChain middleware: synchronous recovery (the recover tool invoked outside async context) needs a real MiddlewareRuntime, but the registration was built without one (or with an unsupported runtime that declined). Sync recovery cannot be simulated without the engine-backed runtime.","triggerScenarios":"Agent executes the caveman_recover tool during a synchronous run whose connection has sync=None (built from an async-only runtime).","commonSituations":"Binding recovery tools while constructing the connection with an async runtime, then running a sync agent executor; enabling recovery in mixed sync/async pipelines.","solutions":["Provide a MiddlewareRuntime when constructing the connection/adapter.","Use the async agent path (arecover/ainvoke) with the async runtime instead.","Disable the recovery tool binding for sync-only deployments lacking a sync runtime."],"exampleFix":"// before\nconn = _Connection(async_runtime, scope)\nagent.invoke({...})  # caveman_recover tool -> TypeError\n// after\nconn = _Connection(MiddlewareRuntime(...), scope)\nagent.invoke({...})","handlingStrategy":"type-guard","validationCode":"if connection.sync is None:\n    raise RuntimeError('caveman_recover requires a sync MiddlewareRuntime; bind arecover instead')","typeGuard":"def can_recover_sync(conn): return getattr(conn, 'sync', None) is not None","tryCatchPattern":"try:\n    data = recover(handle, config)\nexcept TypeError as e:\n    if 'Synchronous recovery' in str(e):\n        data = await arecover(handle, config)\n    else:\n        raise","preventionTips":["Only bind the sync caveman_recover tool when a MiddlewareRuntime is configured.","In mixed apps, construct the connection with both sync and async runtimes.","Cover recovery tool execution with a sync-agent integration test."],"tags":["type-mismatch","recovery","sync-async","langchain"],"backgroundTag":"type-mismatch","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"}