Conversation
Signed-off-by: Mohamed Ali <mohamed.ali@openinnovation.ai>
|
Thank you for your pull request and welcome to our community! To contribute, please sign the Oracle Contributor Agreement (OCA).
To sign the OCA, please create an Oracle account and sign the OCA in Oracle's Contributor Agreement Application. When signing the OCA, please provide your GitHub username. After signing the OCA and getting an OCA approval from Oracle, this PR will be automatically updated. If you are an Oracle employee, please make sure that you are a member of the main Oracle GitHub organization, and your membership in this organization is public. |
|
@fashkl Thank you for the contribution! To start, could you please sign the Oracle Contributor Agreement (OCA)? You can find more info in the comment at #229 (comment). Please make sure to use the same email associated to your Github account, that should be the one reported in the comment. |
Hello @cesarebernardis |
|
Reviewed and verified this locally against current
The only thing holding this back is the OCA check, and @fashkl reported on Aug 31 that they signed more than two weeks earlier. @cesarebernardis, could someone check with the OSS office why the approval is still pending? Once the bot flips this looks ready to me. |
fede-kamel
left a comment
There was a problem hiding this comment.
Follow-up verification focused on what changes when the sync callable moves to a worker thread (PR head 2f6f016, LangGraph 1.2.4, langchain-core 1.4.9):
Verified
tests/adapters/langgraph+tests/adapters/test_template_rendering.pyon Python 3.12: 151 passed, 89 skipped (LLM tests). The new test passes 10/10 consecutive runs.- Nested interrupts through the thread hop: a
ClientToolwithrequires_confirmation=Truerun withainvokeyields the confirmation interrupt (action_requests/review_configs), resuming withapproveyields theclient_tool_requestinterrupt with the right kwargs, resuming the value completes the flow. Resuming withrejectpropagates the adapter'sRuntimeError("Tool ... was denied by the user")out ofto_threadas before. Sointerrupt()inside_confirm_thenand insideclient_toolboth work from the worker thread (context is copied byasyncio.to_thread). - Tracing context:
get_current_span()inside a remote-tool callable duringainvokereturns theToolNodeExecution[...]span both onmainand on this branch, so the change does not alter which span the callable sees. (That theToolExecutionspan opened byAgentSpecToolCallbackHandler.on_tool_startis not the current span inside the callable in async runs is pre-existing and unrelated to this PR.) - Equivalence with the default:
asyncio.to_threaddoes the same thing LangChain's own_arunfallback does for func-only tools (run_in_executor(None, ...)wrapping the call incopy_context().runon the loop's default executor), which is also what LangGraph uses for sync nodes in async runs. OnlyRemoteTool(_langgraphconverter.py:807) andClientTool(:943) go through_as_structured_tool_coroutine; syncServerToolcallables already used the LangChain fallback, so this brings the two paths in line. - Python 3.10 (LangGraph extra installed, which the public CI does not do):
mainfailstests/adapters/langgraph/flows/test_toolnode.py::test_toolnode_can_be_executed_async_with_interrupt_resumewithRuntimeError: Called get_config outside of a runnable context(3/3 runs), this branch passes it (3/3 runs). That is exactly the situation the load-time warning at_langgraphconverter.py:899describes, so the thread hop also fixes async client-tool interrupts on 3.10 in my runs.
Suggestions (non-blocking)
test_remote_tool_coroutine_does_not_block_event_loopassertstime.monotonic() - started_at < 0.1after a 0.01 s sleep while the mock blocks 0.2 s. It is stable locally, but on a slow runner a wider margin (e.g. block 0.5 s, assert < 0.25 s, keepnot task.done()) would make the intent survive scheduling jitter.- Given the 3.10 result above, it may be worth checking whether the
< (3, 11)warning for client tools can be narrowed once this lands; happy to do that as a follow-up if useful.
Replace the wall-clock threshold in test_remote_tool_coroutine_does_not_block_event_loop with threading.Event gating, so the test no longer depends on scheduling jitter on slow runners. The mock signals when the blocking request is entered and waits on a bounded release event. The test observes that signal from the event loop and asserts the task has not completed, which is the actual property under test. A regression still fails (on `not task.done()`) instead of hanging.
|
Applied the test suggestion in 2759730, though I changed it a bit more than you proposed. Instead of widening the window I dropped the wall-clock assert entirely: the mock sets a It still catches the regression: with My runs, Python 3.13.13 / langgraph 1.2.11 / langchain-core 1.4.9:
On the OCA is still the only red check. @cesarebernardis, anything from the OSS office? I signed in mid-August. |
Problem
Fixes #228.
LangGraph
RemoteToolconversion exposes a coroutine for sync remote-tool callables, but the coroutine currently calls the synchronous function directly. ForRemoteTool, that function executeshttpx.request(...), so async LangGraph execution can block the event loop while waiting on network I/O.This is worse than leaving
StructuredTool.coroutineunset. LangChain's ownStructuredTool._arunchecksself.coroutinefirst; when it isNone, it falls back to the base async implementation, whose source comment says it is expected to delegate_runto a separate thread. By supplying a fake async wrapper, pyagentspec bypasses that safe default and runs blocking sync I/O on the event-loop thread.This also makes
RemoteToolinconsistent with the existingApiNodeexecutor split in this codebase:ApiNodeExecutor._execute()uses synchttpx.request(...), whileApiNodeExecutor._aexecute()useshttpx.AsyncClient. RemoteTool is the outlier because its async entrypoint is only async-shaped, not async-safe.In services with event-loop based liveness/readiness handling, a slow or stuck remote tool request can therefore freeze unrelated async work long enough to trip health checks and restart the service.
Fix
Offload synchronous structured-tool coroutine execution with
asyncio.to_thread(...)while preserving native async callables unchanged.This is a small general safety net for sync callables that pyagentspec intentionally exposes through a coroutine, including the current
RemoteToolandClientToolconversion paths. A future native asyncRemoteToolimplementation usinghttpx.AsyncClientwould still be compatible with this change and could further reduce thread usage for RemoteTool specifically.Testing
test_remote_tool_coroutine_does_not_block_event_loopSKIP_LLM_TESTS=1 uv run python -m pytest -q tests/adapters/langgraph/test_tools.pyCommit is signed off per the contribution guide.