Skip to content

fix(agents): accept sync tool handlers on invoke and native graphs - #134

Open
andrewklatzke wants to merge 2 commits into
mainfrom
aklatzke/AIC-3507/fix-lc-handoff
Open

andrewklatzke wants to merge 2 commits into
mainfrom
aklatzke/AIC-3507/fix-lc-handoff

Conversation

@andrewklatzke

@andrewklatzke andrewklatzke commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • LangChain agent tools now run both synchronous and asynchronous handlers. Graph __handoff_* tools stay synchronous so routing records the selected edge instead of raising when the result is awaited.
  • Invoke and stream share _build_agent_tools(), so both routes pick up the same behavior.
  • The same inspect.isawaitable rule is applied on both native-graph paths (langchain-agents and openai-agents), which read tool_handlers without wrap_tool_handlers.
  • READMEs document that sync and async both work, and that sync handlers run on the event-loop thread (blocking I/O stalls the agent).

Fixes AIC-3507.

Test plan

  • A synchronous handler returning a string works
  • An asynchronous handler is still awaited
  • A synthetic __handoff_* handler records and returns its destination
  • Exceptions from either handler type propagate
  • Invoke and streaming routes both execute a sync handoff
  • LangChain _build_node_tools accepts a sync lambda (Jeff's repro)
  • OpenAI _build_node_tools accepts sync / async / sync-returning-awaitable
  • Targeted unit suite: 173 passed (test_handler + both test_native_graph)
  • Integration (Python): langchain-agents, native-graph-langchain, graph, openai-agents — success exit 0 + valid output JSON; failure exit 1 + clean error

Graph handoff tools stay synchronous so routing can record the selected edge. Awaiting every tool result raised before that record happened.

Co-authored-by: Cursor <cursoragent@cursor.com>
@andrewklatzke

andrewklatzke commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Human message:

Recreated the error on a minimal setup (three nodes, one handoff to each). Made the update then re-ran the graph that was previously failing on the error successfully (it even returned the structured output correctly as configured):

Screenshot 2026-10-02 at 12 58 34 PM Screenshot 2026-10-02 at 12 58 26 PM Screenshot 2026-10-02 at 12 58 29 PM

@jeffdupont jeffdupont left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GA 1.0 review of 5fb2560 against main. pytest tests/test_handler.py in langchain-agents: 122 passed. With handler.py put back to main, three of the new tests fail with TypeError: object str can't be used in 'await' expression (test_sync_handler_returns_string, test_handoff_handler_records_destination, test_invoke_and_stream_run_sync_handoff), so they catch the bug. The fix matches the rule in tracking.wrap_tool_handlers (tracking.py:102). JS doesn't need a counterpart: langchain-agents/src/handler.ts:301 awaits the result, and awaiting a plain value is fine in JS.

Two things to settle before merge. Both are the same bug on the native-graph path, which this PR doesn't touch:

  1. langchain-agents/native_graph.py:82 still does res = await fn(kwargs). That path reads _opts["tool_handlers"] directly (:161), without wrap_tool_handlers, so a sync user handler reaches it unwrapped. Reproduced: _build_node_tools(node, {"weather": lambda a: "sunny"}), then calling the built tool, raises TypeError: object str can't be used in 'await' expression. It needs the same three lines and a test.
  2. openai-agents/native_graph.py:70 has the same code (res = await handler(args), with tool_handlers also read straight from _opts). I haven't run a probe there; that one is from reading the code. AIC-3507 should cover both, either here or in a linked PR.

Smaller notes are inline: one shared helper for calling a handler, documenting the sync/async contract for 1.0, and the scope of the description.

Comment thread packages/langchain-agents/tests/test_handler.py
LangChain and OpenAI native graphs read tool_handlers without wrap_tool_handlers,
so sync handlers raised TypeError on await. Match the handler/tracking isawaitable
rule and document the sync/async contract.

Co-authored-by: Cursor <cursoragent@cursor.com>
@andrewklatzke

andrewklatzke commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor Author

Lol AI wrote this, not sure why it tried to sound like this

Thanks for the review — addressed in 100ca1f:

  1. Native graphs (blockers): same inspect.isawaitable rule in langchain-agents/native_graph.py and openai-agents/native_graph.py, with tests that call _build_node_tools directly (including the sync lambda a: "sunny" repro).
  2. Docs: langchain-agents and openai-agents READMEs now state sync + async are supported, sync runs on the event-loop thread, and __handoff_* must stay sync.
  3. Test description: test_sync_handler_returns_string notes that _build_agent_tools skips wrap_tool_handlers, so the production failure was primarily __handoff_* (and native-graph handlers that also bypass wrapping).
  4. Shared helper: deferred to AIC-3439 as you suggested — happy to consolidate isawaitable vs iscoroutinefunction there.

@andrewklatzke andrewklatzke changed the title fix(langchain-agents): accept sync tool handlers fix(agents): accept sync tool handlers on invoke and native graphs Oct 5, 2026
@andrewklatzke

Copy link
Copy Markdown
Contributor Author

Integration tests run locally as welll

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants