Skip to content

Commit 76167ff

Browse files
xumapleclaude
andcommitted
Use LangSmith's official runtime override API instead of monkey-patching aio_to_thread
LangSmith 0.7.34 added `set_runtime_overrides(aio_to_thread=...)` which provides a supported hook for frameworks with non-standard event loops. This replaces the process-wide monkey-patch of `langsmith._internal._aiter.aio_to_thread` with a call to the official API, making the integration less fragile against LangSmith internal refactors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent aa95444 commit 76167ff

3 files changed

Lines changed: 37 additions & 39 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ pydantic = ["pydantic>=2.0.0,<3"]
3131
openai-agents = ["openai-agents>=0.14.0", "mcp>=1.9.4, <2"]
3232
google-adk = ["google-adk>=1.27.0,<2"]
3333
langgraph = ["langgraph>=1.1.0"]
34-
langsmith = ["langsmith>=0.7.0,<0.8"]
34+
langsmith = ["langsmith>=0.7.34,<0.7.35"]
3535
lambda-worker-otel = [
3636
"opentelemetry-api>=1.11.1,<2",
3737
"opentelemetry-sdk>=1.11.1,<2",
@@ -81,7 +81,7 @@ dev = [
8181
"pytest-xdist>=3.6,<4",
8282
"moto[s3,server]>=5",
8383
"langgraph>=1.1.0",
84-
"langsmith>=0.7.0,<0.7.34",
84+
"langsmith>=0.7.34,<0.7.35",
8585
"setuptools<82",
8686
"opentelemetry-exporter-otlp-proto-grpc>=1.11.1,<2",
8787
"opentelemetry-semantic-conventions>=0.40b0,<1",

temporalio/contrib/langsmith/_interceptor.py

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -152,48 +152,46 @@ def _get_current_run_for_propagation() -> RunTree | None:
152152

153153

154154
# ---------------------------------------------------------------------------
155-
# Workflow event loop safety: patch @traceable's aio_to_thread
155+
# Workflow event loop safety: override @traceable's aio_to_thread
156156
# ---------------------------------------------------------------------------
157157

158-
_aio_to_thread_patched = False
158+
_aio_to_thread_override_installed = False
159159

160160

161-
def _patch_aio_to_thread() -> None:
162-
"""Patch langsmith's ``aio_to_thread`` to run synchronously in workflows.
161+
async def _temporal_aio_to_thread(
162+
default_aio_to_thread: Callable[..., Any],
163+
ctx: Any,
164+
func: Callable[..., Any],
165+
/,
166+
*args: Any,
167+
**kwargs: Any,
168+
) -> Any:
169+
"""Run LangSmith's ``aio_to_thread`` synchronously inside Temporal workflows.
163170
164171
The ``@traceable`` decorator on async functions uses ``aio_to_thread()`` →
165172
``loop.run_in_executor()`` for run setup/teardown. The Temporal workflow
166-
event loop does not support ``run_in_executor``. This patch runs those
167-
functions synchronously on the workflow thread when inside a workflow.
168-
Functions passed here must not perform blocking I/O.
173+
event loop does not support ``run_in_executor``. This override runs those
174+
functions synchronously on the workflow thread when inside a workflow,
175+
and delegates to the default implementation outside workflows.
169176
177+
Registered via ``langsmith.set_runtime_overrides(aio_to_thread=...)``.
170178
"""
171-
global _aio_to_thread_patched # noqa: PLW0603
172-
if _aio_to_thread_patched:
173-
return
174-
175-
import langsmith._internal._aiter as _aiter
179+
if not temporalio.workflow.in_workflow():
180+
return await default_aio_to_thread(ctx, func, *args, **kwargs)
181+
with temporalio.workflow.unsafe.sandbox_unrestricted():
182+
return ctx.run(func, *args, **kwargs)
176183

177-
_original = _aiter.aio_to_thread
178184

179-
import contextvars
185+
def _install_aio_to_thread_override() -> None:
186+
"""Install the ``aio_to_thread`` override via LangSmith's official API.
180187
181-
async def _safe_aio_to_thread(
182-
func: Callable[..., Any],
183-
/,
184-
*args: Any,
185-
__ctx: contextvars.Context | None = None,
186-
**kwargs: Any,
187-
) -> Any:
188-
if not temporalio.workflow.in_workflow():
189-
return await _original(func, *args, __ctx=__ctx, **kwargs)
190-
with temporalio.workflow.unsafe.sandbox_unrestricted():
191-
# Run without ctx.run() so context var changes propagate
192-
# to the caller. Safe because workflows are single-threaded.
193-
return func(*args, **kwargs)
194-
195-
_aiter.aio_to_thread = _safe_aio_to_thread # type: ignore[assignment]
196-
_aio_to_thread_patched = True
188+
Safe to call multiple times; the override is only installed once.
189+
"""
190+
global _aio_to_thread_override_installed # noqa: PLW0603
191+
if _aio_to_thread_override_installed:
192+
return
193+
langsmith.set_runtime_overrides(aio_to_thread=_temporal_aio_to_thread)
194+
_aio_to_thread_override_installed = True
197195

198196

199197
# ---------------------------------------------------------------------------
@@ -599,7 +597,7 @@ def workflow_interceptor_class(
599597
self, input: temporalio.worker.WorkflowInterceptorClassInput
600598
) -> type[_LangSmithWorkflowInboundInterceptor]:
601599
"""Return the workflow interceptor class with config bound."""
602-
_patch_aio_to_thread()
600+
_install_aio_to_thread_override()
603601
config = self
604602

605603
class InterceptorWithConfig(_LangSmithWorkflowInboundInterceptor):

uv.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)