fix(agent): prevent outer wall-clock timeout for streaming requests

This commit is contained in:
Xubin Ren 2026-05-16 10:12:57 +00:00
parent 06a1bef9fe
commit e87c07c368
2 changed files with 58 additions and 3 deletions

View File

@ -669,14 +669,25 @@ class AgentRunner:
else:
coro = self.provider.chat_with_retry(**kwargs)
# Streaming requests already have provider-level idle timeouts
# (NANOBOT_STREAM_IDLE_TIMEOUT_S). Do not also apply the outer wall-clock
# LLM timeout here, or healthy long reasoning streams can be killed just
# because total elapsed time exceeded NANOBOT_LLM_TIMEOUT_S.
outer_timeout_s = None if (wants_streaming or wants_progress_streaming) else timeout_s
try:
response = (
await coro if timeout_s is None
else await asyncio.wait_for(coro, timeout=timeout_s)
await coro if outer_timeout_s is None
else await asyncio.wait_for(coro, timeout=outer_timeout_s)
)
except asyncio.TimeoutError:
if outer_timeout_s is None:
return LLMResponse(
content="Error calling LLM: stream stalled",
finish_reason="error",
error_kind="timeout",
)
return LLMResponse(
content=f"Error calling LLM: timed out after {timeout_s:g}s",
content=f"Error calling LLM: timed out after {outer_timeout_s:g}s",
finish_reason="error",
error_kind="timeout",
)

View File

@ -133,6 +133,50 @@ async def test_runner_times_out_hung_llm_request():
assert "timed out" in (result.final_content or "").lower()
@pytest.mark.asyncio
async def test_runner_does_not_apply_outer_wall_timeout_to_streaming_requests():
from nanobot.agent.hook import AgentHook, AgentHookContext
from nanobot.agent.runner import AgentRunSpec, AgentRunner
provider = MagicMock(spec=LLMProvider)
streamed: list[str] = []
async def chat_stream_with_retry(*, on_content_delta, **kwargs):
await asyncio.sleep(0.08)
await on_content_delta("still ")
await asyncio.sleep(0.08)
await on_content_delta("alive")
return LLMResponse(content="still alive", tool_calls=[])
provider.chat_stream_with_retry = chat_stream_with_retry
provider.chat_with_retry = AsyncMock()
tools = MagicMock()
tools.get_definitions.return_value = []
class StreamingHook(AgentHook):
def wants_streaming(self) -> bool:
return True
async def on_stream(self, context: AgentHookContext, delta: str) -> None:
streamed.append(delta)
runner = AgentRunner(provider)
result = await runner.run(AgentRunSpec(
initial_messages=[{"role": "user", "content": "think for a while"}],
tools=tools,
model="test-model",
max_iterations=1,
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
hook=StreamingHook(),
llm_timeout_s=0.01,
))
assert result.stop_reason == "completed"
assert result.final_content == "still alive"
assert streamed == ["still ", "alive"]
provider.chat_with_retry.assert_not_awaited()
@pytest.mark.asyncio
async def test_runner_replaces_empty_tool_result_with_marker():
from nanobot.agent.runner import AgentRunSpec, AgentRunner