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
- Construct the adapter with a MiddlewareRuntime so connection.sync is populated.
- Use the async API (ainvoke/arecover) when only an async runtime is available.
- 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
- Pair sync runtimes with sync APIs and async runtimes with ainvoke consistently.
- Build the adapter with both runtimes if the codebase mixes sync and async call sites.
- Annotate adapter constructors to reject async-only setups for sync wrappers.
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
- Expected a native LangChain BaseChatModel
- scope resolver must return a Caveman Scope
- Synchronous calls require MiddlewareRuntime
- Synchronous document compression requires MiddlewareRuntime
- Synchronous recovery requires MiddlewareRuntime
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)