Skip to content

Commit 82a676b

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 f721b8a commit 82a676b

4 files changed

Lines changed: 38 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.17.1", "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.8"]
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.8",
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
@@ -154,48 +154,46 @@ def _get_current_run_for_propagation() -> RunTree | None:
154154

155155

156156
# ---------------------------------------------------------------------------
157-
# Workflow event loop safety: patch @traceable's aio_to_thread
157+
# Workflow event loop safety: override @traceable's aio_to_thread
158158
# ---------------------------------------------------------------------------
159159

160-
_aio_to_thread_patched = False
160+
_aio_to_thread_override_installed = False
161161

162162

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

179-
_original = _aiter.aio_to_thread
180186

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

200198

201199
# ---------------------------------------------------------------------------
@@ -611,7 +609,7 @@ def workflow_interceptor_class(
611609
self, input: temporalio.worker.WorkflowInterceptorClassInput
612610
) -> type[_LangSmithWorkflowInboundInterceptor]:
613611
"""Return the workflow interceptor class with config bound."""
614-
_patch_aio_to_thread()
612+
_install_aio_to_thread_override()
615613
config = self
616614

617615
class InterceptorWithConfig(_LangSmithWorkflowInboundInterceptor):

temporalio/contrib/langsmith/_plugin.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ def workflow_runner(runner: WorkflowRunner | None) -> WorkflowRunner:
7373
restrictions=runner.restrictions.with_passthrough_modules(
7474
"langsmith",
7575
"langchain_core",
76+
"opentelemetry",
7677
),
7778
)
7879
return runner

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)