fix(agent): close reasoning on stream timeout
This commit is contained in:
@@ -187,7 +187,7 @@ These variables are process-level switches. Set them in the same terminal, servi
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `NANOBOT_MAX_CONCURRENT_REQUESTS` | `3` | Maximum concurrently running inbound agent requests. Must be an integer; set `0` or a negative value for unlimited. |
|
||||
| `NANOBOT_LLM_TIMEOUT_S` | `300` | Wall-clock timeout, in seconds, around ordinary LLM requests. Set `0` to disable. Sustained-goal turns bypass this wall-clock cap. |
|
||||
| `NANOBOT_LLM_TIMEOUT_S` | `300` | Wall-clock timeout, in seconds. Ordinary requests use this value; streaming requests use the greater of 300 seconds or twice this value. Set `0` to disable. Sustained-goal turns bypass this wall-clock cap. |
|
||||
| `NANOBOT_STREAM_IDLE_TIMEOUT_S` | `90` | Streaming idle timeout, in seconds, used by streaming providers. Invalid or non-positive values are ignored; values above `3600` are clamped. |
|
||||
| `NANOBOT_OPENAI_COMPAT_TIMEOUT_S` | `120` | HTTP request timeout, in seconds, for OpenAI-compatible providers. Invalid or non-positive values are ignored. |
|
||||
| `NANOBOT_WORKSPACE_SANDBOX_ENFORCED` | unset | Marks that an external workspace sandbox is already enforced. Truthy values (`1`, `true`, `yes`, `on`, `enabled`) use `NANOBOT_WORKSPACE_SANDBOX_PROVIDER` as the label; any other non-false value is treated as the provider name. |
|
||||
|
||||
@@ -824,16 +824,17 @@ class AgentRunner:
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
if outer_timeout_s is None:
|
||||
return LLMResponse(
|
||||
response = LLMResponse(
|
||||
content="Error calling LLM: stream stalled",
|
||||
finish_reason="error",
|
||||
error_kind="timeout",
|
||||
)
|
||||
return LLMResponse(
|
||||
content=f"Error calling LLM: timed out after {outer_timeout_s:g}s",
|
||||
finish_reason="error",
|
||||
error_kind="timeout",
|
||||
)
|
||||
else:
|
||||
response = LLMResponse(
|
||||
content=f"Error calling LLM: timed out after {outer_timeout_s:g}s",
|
||||
finish_reason="error",
|
||||
error_kind="timeout",
|
||||
)
|
||||
if progress_state and progress_state.get("reasoning_open"):
|
||||
await hook.emit_reasoning_end()
|
||||
dropped, all_dropped, original_finish_reason = (
|
||||
|
||||
@@ -280,6 +280,65 @@ async def test_runner_times_out_never_ending_streaming_request():
|
||||
provider.chat_with_retry.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_runner_closes_progress_reasoning_on_streaming_wall_timeout():
|
||||
from nanobot.agent.hook import AgentHook
|
||||
from nanobot.agent.runner import AgentRunner
|
||||
|
||||
provider = MagicMock(spec=LLMProvider)
|
||||
provider.supports_progress_deltas = True
|
||||
events: list[tuple[str, str | None]] = []
|
||||
|
||||
async def chat_stream_with_retry(*, on_content_delta, **kwargs):
|
||||
try:
|
||||
await on_content_delta("<think>working...</think>")
|
||||
await asyncio.sleep(3600)
|
||||
finally:
|
||||
events.append(("provider_cancelled", None))
|
||||
|
||||
provider.chat_stream_with_retry = chat_stream_with_retry
|
||||
provider.chat_with_retry = AsyncMock()
|
||||
tools = MagicMock()
|
||||
tools.get_definitions.return_value = []
|
||||
|
||||
class ProgressReasoningHook(AgentHook):
|
||||
async def emit_reasoning(self, reasoning_content: str | None) -> None:
|
||||
if reasoning_content:
|
||||
events.append(("reasoning", reasoning_content))
|
||||
|
||||
async def emit_reasoning_end(self) -> None:
|
||||
events.append(("reasoning_end", None))
|
||||
|
||||
real_wait_for = asyncio.wait_for
|
||||
|
||||
async def fake_wait_for(coro, *, timeout):
|
||||
assert timeout == 300.0
|
||||
return await real_wait_for(coro, timeout=0.01)
|
||||
|
||||
runner = AgentRunner()
|
||||
with patch("nanobot.agent.runner.asyncio.wait_for", fake_wait_for):
|
||||
result = await runner.run(make_run_spec(provider,
|
||||
initial_messages=[{"role": "user", "content": "think forever"}],
|
||||
tools=tools,
|
||||
model="test-model",
|
||||
max_iterations=1,
|
||||
max_tool_result_chars=_MAX_TOOL_RESULT_CHARS,
|
||||
hook=ProgressReasoningHook(),
|
||||
progress_callback=AsyncMock(),
|
||||
stream_progress_deltas=True,
|
||||
llm_timeout_s=1,
|
||||
))
|
||||
|
||||
assert result.stop_reason == "error"
|
||||
assert result.final_content == "Error calling LLM: timed out after 300s"
|
||||
assert events == [
|
||||
("reasoning", "working..."),
|
||||
("provider_cancelled", None),
|
||||
("reasoning_end", None),
|
||||
]
|
||||
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 AgentRunner
|
||||
|
||||
Reference in New Issue
Block a user