JuliusBrussee/caveman · error · TypeError

Synchronous calls require MiddlewareRuntime

Error message

Synchronous calls require MiddlewareRuntime

What it means

Raised inside wrap_model_call in the LangChain middleware: a synchronous model call path was entered that requires MiddlewareRuntime, which is unavailable in this configuration. The middleware refuses to fake a synchronous compress/recover cycle without the engine-backed runtime.

Solutions

  1. Build the adapter/connection with a MiddlewareRuntime so sync is set.
  2. Run the agent asynchronously (ainvoke) if only an async runtime exists.
  3. Create separate adapter instances for sync and async runtimes rather than sharing one.

Example fix

// before
middleware = CavemanLangChainMiddleware(_Connection(async_runtime, scope))
agent.invoke(input)  # wrap_model_call -> TypeError
// after
middleware = CavemanLangChainMiddleware(_Connection(MiddlewareRuntime(...), scope))
agent.invoke(input)
Defensive patterns

Strategy: type-guard

Validate before calling

if connection.sync is None:
    raise RuntimeError('wrap_model_call (sync) requires MiddlewareRuntime; register only the async middleware')

Type guard

def has_sync_middleware(conn): return getattr(conn, 'sync', None) is not None

Try / catch

try:
    out = agent.invoke(input)
except TypeError as e:
    if 'Synchronous calls require MiddlewareRuntime' in str(e):
        raise RuntimeError('Use ainvoke with your async runtime, or build the adapter with MiddlewareRuntime') from e
    raise

Prevention

When it happens

Trigger: Registering the middleware as a synchronous LangChain agent middleware while the connection was created with only an AsyncMiddlewareRuntime (sync=None).

Common situations: Using LangGraph's sync AgentExecutor/stream with an adapter built for async; the same middleware object attached to both sync and async agents but configured with one runtime.

Understand the failure class

Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/88fdaa9c83c573bf. Report an issue: GitHub.

Appendix: source

Thrown at packages/middleware/python/caveman_middleware/langchain.py:215

        except (TypeError, ValueError, AttributeError, RecursionError):
            return False

    def binding(self, request, runtime):
        if self.recovery_tool is None or runtime.mode != "compress" or owner.get() is not None:
            return None
        actual = [tool for tool in request.tools if getattr(tool, "name", None) == "caveman_retrieve" or (plain(tool) and tool.get("name") == "caveman_retrieve")]
        if len(actual) != 1 or actual[0] is not self.recovery_tool or not self._recovery_intact() or request.response_format is not None or request.tool_choice not in (None, "auto"):
            return None
        return runtime.recovery(_scope(self.connection.scope))

    @property
    def overhead(self):
        return json.dumps({"name": "caveman_retrieve", "description": RECOVERY_DESCRIPTION, "input_schema": RECOVERY_SCHEMA}, ensure_ascii=False, separators=(",", ":"))

    def wrap_model_call(self, request, handler):
        runtime = self.connection.sync
        if runtime is None:
            raise TypeError("Synchronous calls require MiddlewareRuntime")
        binding = self.binding(request, runtime)
        messages, attempt = self.connection.prepare(request.messages, binding=binding, overhead=self.overhead if binding else None, prefix=[request.system_message] if request.system_message else [])
        if attempt is None:
            return handler(request)
        attempt.observe("dispatch_intent")
        token = owner.set(attempt)
        try:
            response = handler(request.override(messages=messages))
            _observe_messages(attempt, response)
            return response
        except BaseException:
            attempt.observe("failed")
            raise
        finally:
            owner.reset(token)

    async def awrap_model_call(self, request, handler):
        runtime = self.connection.async_runtime

View on GitHub (pinned to 3ee70a1026)