JuliusBrussee/caveman · error · TypeError

Synchronous LangChain calls require MiddlewareRuntime

Error message

Synchronous LangChain calls require MiddlewareRuntime

What it means

prepare() drives the synchronous LangChain invoke path and requires self.sync (a MiddlewareRuntime) to be set. When the connection was built only with an async runtime, sync calls cannot proceed and a TypeError is raised.

Solutions

  1. Construct the adapter with a MiddlewareRuntime so connection.sync is populated.
  2. Use the async API (ainvoke/arecover) when only an async runtime is available.
  3. If mode is intentionally off, gate your calls instead of invoking through the adapter.

Example fix

// before
adapter = with_caveman_model(async_runtime, scope)
chain.invoke(messages)
// after
adapter = with_caveman_model(MiddlewareRuntime(...), scope)
chain.invoke(messages)  # or use ainvoke with the async runtime
Defensive patterns

Strategy: type-guard

Validate before calling

if connection.sync is None:
    raise RuntimeError('sync runtime not configured; use ainvoke or build with MiddlewareRuntime')
chain.invoke(messages, config=config)

Type guard

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

Try / catch

try:
    out = chain.invoke(messages, config=config)
except TypeError as e:
    if 'Synchronous LangChain calls' in str(e):
        out = await chain.ainvoke(messages, config=config)
    else:
        raise

Prevention

When it happens

Trigger: Calling synchronous methods (invoke / prepare path) on an adapter constructed with only an AsyncMiddlewareRuntime, or with the sync runtime slot left None.

Common situations: Mixing async setup with sync model.invoke(); constructing _Connection with runtime=None for 'off' mode then invoking sync; framework code paths that call sync invoke inside an async app.

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

Appendix: source

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

            return None
        reason = "off" if runtime.mode == "off" else "unsupported_version" if not _supported(runtime) else None
        if reason:
            return Attempt(runtime, Scope("caveman-passive", "report-only"), str(uuid.uuid4()), str(uuid.uuid4()),
                           passive=True, reason=reason, adapter="langchain"), {}, None
        view = _message_view(messages, prefix)
        if view is None:
            return Attempt(runtime, Scope("caveman-passive", "report-only"), str(uuid.uuid4()), str(uuid.uuid4()),
                           passive=True, reason="unsupported_shape", adapter="langchain"), {}, None
        scope = _scope(self.scope, config)
        attempt = Attempt(runtime, scope, str(uuid.uuid4()), str(uuid.uuid4()), adapter="langchain")
        context, candidates, setters = view
        options = dict(scope=scope, adapter=ADAPTER, manifest=context, candidates=candidates, binding=binding,
                       model=model, recovery_overhead_text=overhead, logical_call_id=attempt.logical_call_id, attempt_id=attempt.attempt_id)
        return attempt, setters, options

    def prepare(self, messages, config=None, binding=None, model=None, overhead=None, prefix=()):
        if self.sync is None:
            raise TypeError("Synchronous LangChain calls require MiddlewareRuntime")
        state = self.state(messages, config, self.sync, binding, model, overhead, prefix)
        if state is None:
            return messages, None
        attempt, setters, options = state
        if options is None:
            return messages, attempt
        attempt.optimization = self.sync.optimize(**options)
        if not all(replacement["segment_id"] in setters for replacement in attempt.optimization.replacements):
            attempt.optimization, attempt.reason = None, "invalid_replacement_plan"
            return messages, attempt
        return _apply(messages, attempt.optimization, setters), attempt

    async def prepare_async(self, messages, config=None, binding=None, model=None, overhead=None, prefix=()):
        state = self.state(messages, config, self.async_runtime, binding, model, overhead, prefix)
        if state is None:
            return messages, None
        attempt, setters, options = state
        if options is None:

View on GitHub (pinned to 3ee70a1026)