diff --git a/pyagentspec/src/pyagentspec/adapters/langgraph/_langgraphconverter.py b/pyagentspec/src/pyagentspec/adapters/langgraph/_langgraphconverter.py index 069ea273..eb3c0616 100644 --- a/pyagentspec/src/pyagentspec/adapters/langgraph/_langgraphconverter.py +++ b/pyagentspec/src/pyagentspec/adapters/langgraph/_langgraphconverter.py @@ -5,6 +5,7 @@ # (UPL) 1.0 (LICENSE-UPL or https://oss.oracle.com/licenses/upl), at your option. +import asyncio import inspect import logging import os @@ -1893,7 +1894,7 @@ def _as_structured_tool_coroutine( return func async def _wrapped_async(*args: Any, **kwargs: Any) -> Any: - return func(*args, **kwargs) + return await asyncio.to_thread(func, *args, **kwargs) return _wrapped_async diff --git a/pyagentspec/tests/adapters/langgraph/test_tools.py b/pyagentspec/tests/adapters/langgraph/test_tools.py index 9808ca2a..edcad2ce 100644 --- a/pyagentspec/tests/adapters/langgraph/test_tools.py +++ b/pyagentspec/tests/adapters/langgraph/test_tools.py @@ -4,7 +4,9 @@ # (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) or Universal Permissive License # (UPL) 1.0 (LICENSE-UPL or https://oss.oracle.com/licenses/upl), at your option. +import asyncio import threading +import time from typing import Any from unittest.mock import patch @@ -1132,6 +1134,39 @@ def test_server_tool_missing_from_registry_raises() -> None: AgentSpecLoader(tool_registry={}, checkpointer=MemorySaver()).load_component(flow) +@pytest.mark.anyio +async def test_remote_tool_coroutine_does_not_block_event_loop() -> None: + from pyagentspec.adapters.langgraph import AgentSpecLoader + + def mock_request(*args: Any, **kwargs: Any) -> DummyResponse: + time.sleep(0.2) + return DummyResponse({"ok": True, "body": kwargs["json"]}) + + remote_tool = RemoteTool( + name="remote_echo", + description="Echoes the input value", + url="https://example.com/echo", + http_method="POST", + data={"x": "{{x}}"}, + inputs=[IntegerProperty(title="x")], + outputs=[Property(title="result", json_schema={})], + ) + + lang_tool = AgentSpecLoader().load_component(remote_tool) + + assert lang_tool.coroutine is not None + + with patch("httpx.request", side_effect=mock_request): + started_at = time.monotonic() + task = asyncio.create_task(lang_tool.coroutine(x=5)) + + await asyncio.sleep(0.01) + + assert time.monotonic() - started_at < 0.1 + assert not task.done() + assert await task == {"ok": True, "body": {"x": "5"}} + + @pytest.mark.anyio async def test_async_server_tool_callable_converts_to_structured_tool_coroutine() -> None: from pyagentspec.adapters.langgraph import AgentSpecLoader