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 |
|
| 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_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_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_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. |
|
| `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,12 +824,13 @@ class AgentRunner:
|
|||||||
)
|
)
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
if outer_timeout_s is None:
|
if outer_timeout_s is None:
|
||||||
return LLMResponse(
|
response = LLMResponse(
|
||||||
content="Error calling LLM: stream stalled",
|
content="Error calling LLM: stream stalled",
|
||||||
finish_reason="error",
|
finish_reason="error",
|
||||||
error_kind="timeout",
|
error_kind="timeout",
|
||||||
)
|
)
|
||||||
return LLMResponse(
|
else:
|
||||||
|
response = LLMResponse(
|
||||||
content=f"Error calling LLM: timed out after {outer_timeout_s:g}s",
|
content=f"Error calling LLM: timed out after {outer_timeout_s:g}s",
|
||||||
finish_reason="error",
|
finish_reason="error",
|
||||||
error_kind="timeout",
|
error_kind="timeout",
|
||||||
|
|||||||
@@ -280,6 +280,65 @@ async def test_runner_times_out_never_ending_streaming_request():
|
|||||||
provider.chat_with_retry.assert_not_awaited()
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_runner_replaces_empty_tool_result_with_marker():
|
async def test_runner_replaces_empty_tool_result_with_marker():
|
||||||
from nanobot.agent.runner import AgentRunner
|
from nanobot.agent.runner import AgentRunner
|
||||||
|
|||||||
Reference in New Issue
Block a user