fix(agent): pass tool_call_timeout to subagent handsoff, cron and background task execution, and increase default timeout from 60 to 120 (#6713)

* 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.

* test: add unit test for tool_call_timeout in SubAgent handoff

* 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

* 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

* fix(config): increase default tool call timeout from 60 to 120 seconds

---------

Co-authored-by: LehaoLin <linlehao@cuhk.edu.cn>
Co-authored-by: Soulter <905617992@qq.com>
This commit is contained in:
LIU Yaohua
2026-03-21 01:30:53 +08:00
committed by GitHub
co-authored by LehaoLin Soulter
parent b273ba2a19
commit 8d9838a293
10 changed files with 62 additions and 9 deletions
+1 -1
View File
@@ -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]
+2 -1
View File
@@ -303,6 +303,7 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
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 @@ class FunctionToolExecutor(BaseFunctionToolExecutor[AstrAgentContext]):
)
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),
+1 -1
View File
@@ -117,7 +117,7 @@ DEFAULT_CONFIG = {
"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
+4 -1
View File
@@ -307,8 +307,11 @@ class CronJobManager:
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,
)
@@ -378,7 +378,7 @@ class ThirdPartyAgentSubStage(Stage):
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,
+1 -1
View File
@@ -153,7 +153,7 @@ class Context:
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.
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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
```
+1 -1
View File
@@ -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,
+49
View File
@@ -272,6 +272,55 @@ async def test_collect_handoff_image_urls_filters_extensionless_missing_event_fi
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,