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 0dc8b9eeb7..1fb4b03368 100644 --- a/astrbot/core/astr_agent_tool_exec.py +++ b/astrbot/core/astr_agent_tool_exec.py @@ -303,6 +303,7 @@ async def _execute_handoff( tools=toolset, contexts=contexts, max_steps=agent_max_step, + tool_call_timeout=run_context.tool_call_timeout, stream=stream, ) yield mcp.types.CallToolResult( @@ -481,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, diff --git a/tests/unit/test_astr_agent_tool_exec.py b/tests/unit/test_astr_agent_tool_exec.py index 9d405f1ab5..5fab9fe0a2 100644 --- a/tests/unit/test_astr_agent_tool_exec.py +++ b/tests/unit/test_astr_agent_tool_exec.py @@ -272,6 +272,55 @@ async def _fake_convert_to_file_path(self): assert image_urls == [] +@pytest.mark.asyncio +async def test_execute_handoff_passes_tool_call_timeout_to_tool_loop_agent( + monkeypatch: pytest.MonkeyPatch, +): + captured: dict = {} + + 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": {}}, + ) + 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, + ), + ) + + results = [] + async for result in FunctionToolExecutor._execute_handoff( + tool, + run_context, + image_urls_prepared=True, + input="hello", + image_urls=[], + ): + results.append(result) + + assert len(results) == 1 + assert captured["tool_call_timeout"] == 120 + + @pytest.mark.asyncio async def test_collect_handoff_image_urls_filters_extensionless_file_outside_temp_root( monkeypatch: pytest.MonkeyPatch,