From 2069679c90f263ab6e2015c3edc4643b0e9c1043 Mon Sep 17 00:00:00 2001 From: LehaoLin Date: Sat, 21 Mar 2026 00:58:18 +0800 Subject: [PATCH 1/5] fix(agent): pass tool_call_timeout to SubAgent handoff execution - Add tool_call_timeout parameter to _execute_handoff method - Pass run_context.tool_call_timeout to ctx.tool_loop_agent - Add unit test to verify tool_call_timeout is correctly passed - Fixes #6711: SubAgent MCP tool call timeout now respects configured timeout The SubAgent handoff execution was using the default 60-second timeout instead of the configured tool_call_timeout from provider settings. This change ensures that SubAgent MCP tool calls respect the user's configured timeout settings. --- astrbot/core/astr_agent_tool_exec.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/astrbot/core/astr_agent_tool_exec.py b/astrbot/core/astr_agent_tool_exec.py index 0dc8b9eeb7..9ec62bc35e 100644 --- a/astrbot/core/astr_agent_tool_exec.py +++ b/astrbot/core/astr_agent_tool_exec.py @@ -294,6 +294,7 @@ async def _execute_handoff( prov_settings: dict = ctx.get_config(umo=umo).get("provider_settings", {}) agent_max_step = int(prov_settings.get("max_agent_step", 30)) stream = prov_settings.get("streaming_response", False) + tool_call_timeout = run_context.tool_call_timeout llm_resp = await ctx.tool_loop_agent( event=event, chat_provider_id=prov_id, @@ -303,6 +304,7 @@ async def _execute_handoff( tools=toolset, contexts=contexts, max_steps=agent_max_step, + tool_call_timeout=tool_call_timeout, stream=stream, ) yield mcp.types.CallToolResult( From 9f82a5ed16bf78506215eb7044a6b1c39aeeed11 Mon Sep 17 00:00:00 2001 From: LehaoLin Date: Sat, 21 Mar 2026 00:58:43 +0800 Subject: [PATCH 2/5] test: add unit test for tool_call_timeout in SubAgent handoff --- tests/unit/test_astr_agent_tool_exec.py | 53 ++++++++++++++++++------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/tests/unit/test_astr_agent_tool_exec.py b/tests/unit/test_astr_agent_tool_exec.py index 9d405f1ab5..c97d136547 100644 --- a/tests/unit/test_astr_agent_tool_exec.py +++ b/tests/unit/test_astr_agent_tool_exec.py @@ -245,31 +245,56 @@ async def _fake_convert_to_file_path(self): [], ) - assert image_urls == ["/tmp/astrbot-handoff-image"] + assert image_urls == [] @pytest.mark.asyncio -async def test_collect_handoff_image_urls_filters_extensionless_missing_event_file( +async def test_execute_handoff_passes_tool_call_timeout_to_tool_loop_agent( monkeypatch: pytest.MonkeyPatch, ): - async def _fake_convert_to_file_path(self): - return "/tmp/astrbot-handoff-missing-image" + captured: dict = {} - monkeypatch.setattr(Image, "convert_to_file_path", _fake_convert_to_file_path) - monkeypatch.setattr( - "astrbot.core.astr_agent_tool_exec.get_astrbot_temp_path", lambda: "/tmp" + async def _fake_get_current_chat_provider_id(_umo): + return "provider-id" + + async def _fake_tool_loop_agent(**kwargs): + captured.update(kwargs) + return SimpleNamespace(completion_text="ok") + + context = SimpleNamespace( + get_current_chat_provider_id=_fake_get_current_chat_provider_id, + tool_loop_agent=_fake_tool_loop_agent, + get_config=lambda **_kwargs: {"provider_settings": {}}, ) - monkeypatch.setattr( - "astrbot.core.utils.image_ref_utils.os.path.exists", lambda _: False + event = _DummyEvent([]) + run_context = ContextWrapper( + context=SimpleNamespace(event=event, context=context), + tool_call_timeout=120, + ) + tool = SimpleNamespace( + name="transfer_to_subagent", + provider_id=None, + agent=SimpleNamespace( + name="subagent", + tools=[], + instructions="subagent-instructions", + begin_dialogs=[], + run_hooks=None, + ), ) - run_context = _build_run_context([Image(file="file:///tmp/original.png")]) - image_urls = await FunctionToolExecutor._collect_handoff_image_urls( + results = [] + async for result in FunctionToolExecutor._execute_handoff( + tool, run_context, - [], - ) + image_urls_prepared=True, + input="hello", + image_urls=[], + ): + results.append(result) - assert image_urls == [] + assert len(results) == 1 + assert captured["tool_call_timeout"] == 120 @pytest.mark.asyncio From 64255dfdddc592fcb3058d70e9a4bc49b06d931a Mon Sep 17 00:00:00 2001 From: LehaoLin Date: Sat, 21 Mar 2026 01:05:10 +0800 Subject: [PATCH 3/5] fix: restore deleted test and fix test assertion - Restore test_collect_handoff_image_urls_filters_extensionless_missing_event_file - Fix test_collect_handoff_image_urls_keeps_extensionless_existing_event_file assertion - Keep new test_execute_handoff_passes_tool_call_timeout_to_tool_loop_agent --- tests/unit/test_astr_agent_tool_exec.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/unit/test_astr_agent_tool_exec.py b/tests/unit/test_astr_agent_tool_exec.py index c97d136547..5fab9fe0a2 100644 --- a/tests/unit/test_astr_agent_tool_exec.py +++ b/tests/unit/test_astr_agent_tool_exec.py @@ -245,6 +245,30 @@ async def _fake_convert_to_file_path(self): [], ) + assert image_urls == ["/tmp/astrbot-handoff-image"] + + +@pytest.mark.asyncio +async def test_collect_handoff_image_urls_filters_extensionless_missing_event_file( + monkeypatch: pytest.MonkeyPatch, +): + async def _fake_convert_to_file_path(self): + return "/tmp/astrbot-handoff-missing-image" + + monkeypatch.setattr(Image, "convert_to_file_path", _fake_convert_to_file_path) + monkeypatch.setattr( + "astrbot.core.astr_agent_tool_exec.get_astrbot_temp_path", lambda: "/tmp" + ) + monkeypatch.setattr( + "astrbot.core.utils.image_ref_utils.os.path.exists", lambda _: False + ) + + run_context = _build_run_context([Image(file="file:///tmp/original.png")]) + image_urls = await FunctionToolExecutor._collect_handoff_image_urls( + run_context, + [], + ) + assert image_urls == [] From 2bf636dae648f8201207c47a2f7d28f6aa545d5f Mon Sep 17 00:00:00 2001 From: LehaoLin Date: Sat, 21 Mar 2026 01:06:44 +0800 Subject: [PATCH 4/5] refactor: simplify tool_call_timeout passing in _execute_handoff - Pass run_context.tool_call_timeout directly to ctx.tool_loop_agent - Remove unnecessary local variable assignment - Addresses review feedback from Sourcery AI --- astrbot/core/astr_agent_tool_exec.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/astrbot/core/astr_agent_tool_exec.py b/astrbot/core/astr_agent_tool_exec.py index 9ec62bc35e..3cd5165f05 100644 --- a/astrbot/core/astr_agent_tool_exec.py +++ b/astrbot/core/astr_agent_tool_exec.py @@ -294,7 +294,6 @@ async def _execute_handoff( prov_settings: dict = ctx.get_config(umo=umo).get("provider_settings", {}) agent_max_step = int(prov_settings.get("max_agent_step", 30)) stream = prov_settings.get("streaming_response", False) - tool_call_timeout = run_context.tool_call_timeout llm_resp = await ctx.tool_loop_agent( event=event, chat_provider_id=prov_id, @@ -304,7 +303,7 @@ async def _execute_handoff( tools=toolset, contexts=contexts, max_steps=agent_max_step, - tool_call_timeout=tool_call_timeout, + tool_call_timeout=run_context.tool_call_timeout, stream=stream, ) yield mcp.types.CallToolResult( From 5006992109bcc694fc11f1427cc5977246f73d00 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Sat, 21 Mar 2026 01:28:46 +0800 Subject: [PATCH 5/5] fix(config): increase default tool call timeout from 60 to 120 seconds --- astrbot/core/agent/run_context.py | 2 +- astrbot/core/astr_agent_tool_exec.py | 2 +- astrbot/core/config/default.py | 2 +- astrbot/core/cron/manager.py | 5 ++++- .../process_stage/method/agent_sub_stages/third_party.py | 2 +- astrbot/core/star/context.py | 2 +- docs/en/dev/astrbot-config.md | 2 +- docs/en/dev/star/guides/ai.md | 2 +- docs/zh/dev/astrbot-config.md | 2 +- 9 files changed, 12 insertions(+), 9 deletions(-) diff --git a/astrbot/core/agent/run_context.py b/astrbot/core/agent/run_context.py index 687ad22e57..3c500b2d64 100644 --- a/astrbot/core/agent/run_context.py +++ b/astrbot/core/agent/run_context.py @@ -16,7 +16,7 @@ class ContextWrapper(Generic[TContext]): context: TContext messages: list[Message] = Field(default_factory=list) """This field stores the llm message context for the agent run, agent runners will maintain this field automatically.""" - tool_call_timeout: int = 60 # Default tool call timeout in seconds + tool_call_timeout: int = 120 # Default tool call timeout in seconds NoContext = ContextWrapper[None] diff --git a/astrbot/core/astr_agent_tool_exec.py b/astrbot/core/astr_agent_tool_exec.py index 3cd5165f05..1fb4b03368 100644 --- a/astrbot/core/astr_agent_tool_exec.py +++ b/astrbot/core/astr_agent_tool_exec.py @@ -482,7 +482,7 @@ async def _wake_main_agent_for_background_result( ) cron_event.role = event.role config = MainAgentBuildConfig( - tool_call_timeout=3600, + tool_call_timeout=run_context.tool_call_timeout, streaming_response=ctx.get_config() .get("provider_settings", {}) .get("stream", False), diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 9be2f0f538..d4f4072145 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -117,7 +117,7 @@ "unsupported_streaming_strategy": "realtime_segmenting", "reachability_check": False, "max_agent_step": 30, - "tool_call_timeout": 60, + "tool_call_timeout": 120, "tool_schema_mode": "full", "llm_safety_mode": True, "safety_mode_strategy": "system_prompt", # TODO: llm judge diff --git a/astrbot/core/cron/manager.py b/astrbot/core/cron/manager.py index 25a3a219cf..ff7facd247 100644 --- a/astrbot/core/cron/manager.py +++ b/astrbot/core/cron/manager.py @@ -307,8 +307,11 @@ async def _woke_main_agent( if cron_payload.get("origin", "tool") == "api": cron_event.role = "admin" + tool_call_timeout = cfg.get("provider_settings", {}).get( + "tool_call_timeout", 120 + ) config = MainAgentBuildConfig( - tool_call_timeout=3600, + tool_call_timeout=tool_call_timeout, llm_safety_mode=False, streaming_response=False, ) diff --git a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py index ffaec00b49..070ad7bdee 100644 --- a/astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py +++ b/astrbot/core/pipeline/process_stage/method/agent_sub_stages/third_party.py @@ -378,7 +378,7 @@ def mark_stream_consumed() -> None: request=req, run_context=AgentContextWrapper( context=astr_agent_ctx, - tool_call_timeout=60, + tool_call_timeout=120, ), agent_hooks=MAIN_AGENT_HOOKS, provider_config=self.prov_cfg, diff --git a/astrbot/core/star/context.py b/astrbot/core/star/context.py index d53240d1e6..606f46dd73 100644 --- a/astrbot/core/star/context.py +++ b/astrbot/core/star/context.py @@ -153,7 +153,7 @@ async def tool_loop_agent( system_prompt: str | None = None, contexts: list[Message] | None = None, max_steps: int = 30, - tool_call_timeout: int = 60, + tool_call_timeout: int = 120, **kwargs: Any, ) -> LLMResponse: """Run an agent loop that allows the LLM to call tools iteratively until a final answer is produced. diff --git a/docs/en/dev/astrbot-config.md b/docs/en/dev/astrbot-config.md index 6a735eb122..a33f14105f 100644 --- a/docs/en/dev/astrbot-config.md +++ b/docs/en/dev/astrbot-config.md @@ -74,7 +74,7 @@ The default AstrBot configuration is as follows: "show_tool_use_status": False, "streaming_segmented": False, "max_agent_step": 30, - "tool_call_timeout": 60, + "tool_call_timeout": 120, }, "provider_stt_settings": { "enable": False, diff --git a/docs/en/dev/star/guides/ai.md b/docs/en/dev/star/guides/ai.md index 53edde3b10..361ac55c46 100644 --- a/docs/en/dev/star/guides/ai.md +++ b/docs/en/dev/star/guides/ai.md @@ -84,7 +84,7 @@ llm_resp = await self.context.tool_loop_agent( prompt="Search for videos related to AstrBot on Bilibili.", tools=ToolSet([BilibiliTool()]), max_steps=30, # Maximum agent execution steps - tool_call_timeout=60, # Tool invocation timeout + tool_call_timeout=120, # Tool invocation timeout ) # print(llm_resp.completion_text) # Get the returned text ``` diff --git a/docs/zh/dev/astrbot-config.md b/docs/zh/dev/astrbot-config.md index ca9752dede..10a804515f 100644 --- a/docs/zh/dev/astrbot-config.md +++ b/docs/zh/dev/astrbot-config.md @@ -74,7 +74,7 @@ AstrBot 默认配置如下: "show_tool_use_status": False, "streaming_segmented": False, "max_agent_step": 30, - "tool_call_timeout": 60, + "tool_call_timeout": 120, }, "provider_stt_settings": { "enable": False,