From 341055469d3beb9f5d3aade57dcc7a4c49c9e593 Mon Sep 17 00:00:00 2001 From: Akshat malik Date: Sun, 13 Sep 2026 12:33:27 +0530 Subject: [PATCH] fix: correctly bind async local plugins as coroutines --- .../engine/langgraph/graph/graph_builder.py | 5 ++- tests/test_review_regression.py | 39 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 tests/test_review_regression.py diff --git a/src/agent_engine/engine/langgraph/graph/graph_builder.py b/src/agent_engine/engine/langgraph/graph/graph_builder.py index 81187057..f1f34b24 100644 --- a/src/agent_engine/engine/langgraph/graph/graph_builder.py +++ b/src/agent_engine/engine/langgraph/graph/graph_builder.py @@ -255,7 +255,10 @@ def _build_agent_tools( server_by_tool: dict[str, str] = {} for tool_spec in spec.tools: function = self._tool_loader.load(tool_spec.id) - tools.append(StructuredTool.from_function(function, description=tool_spec.description)) + if inspect.iscoroutinefunction(function): + tools.append(StructuredTool.from_function(coroutine=function, description=tool_spec.description)) + else: + tools.append(StructuredTool.from_function(func=function, description=tool_spec.description)) for mcp in spec.mcps: server_tools = self._mcp_tools.get(mcp.id, []) tools.extend(server_tools) diff --git a/tests/test_review_regression.py b/tests/test_review_regression.py new file mode 100644 index 00000000..7261467a --- /dev/null +++ b/tests/test_review_regression.py @@ -0,0 +1,39 @@ +import pytest +from langchain_core.messages import AIMessage, ToolMessage + +from agent_engine.engine.langgraph.engine import LangGraphEngine +from tests.approvals.test_engine_hitl import _spec as approval_spec + + +class SingleToolCallModel: + def __init__(self): + self.calls = 0 + + def bind_tools(self, tools): + return self + + async def ainvoke(self, messages): + if any(isinstance(m, ToolMessage) for m in messages): + return AIMessage(content='done') + self.calls += 1 + return AIMessage(content='', tool_calls=[{ + 'name': 'record_value', 'args': {'message': 'reviewed-value'}, + 'id': f'call-{self.calls}', 'type': 'tool_call', + }]) + + +@pytest.mark.asyncio +async def test_async_local_tool_executes(tmp_path): + output = tmp_path / 'async-executed.txt' + tool = tmp_path / 'plugins/tools/record_value.py' + tool.parent.mkdir(parents=True) + tool.write_text('from pathlib import Path\n' + 'async def record_value(message: str) -> str:\n' + f' Path({str(output)!r}).write_text(message)\n' + ' return message\n') + model = SingleToolCallModel() + async with LangGraphEngine(tmp_path, model_factory=lambda *a, **kw: model) as engine: + await engine.build(approval_spec('record_value', auto_mode=True)) + result = await engine.run('record a value') + print(f'async tool executed={output.exists()}, used_tools={result.used_tools}') + assert output.exists()