From 29025f5a8bfaeed8a8c0daf22c770afd9d023dd0 Mon Sep 17 00:00:00 2001 From: chengyongru <61816729+chengyongru@users.noreply.github.com> Date: Thu, 27 Aug 2026 23:31:38 +0800 Subject: [PATCH] fix(agent): default request concurrency to unlimited (#5572) * fix(agent): default request concurrency to unlimited * test(agent): clarify session serialization coverage --- docs/configuration.md | 2 +- nanobot/agent/loop.py | 4 +-- tests/agent/test_loop_concurrency.py | 47 ++++++++++++++++++++++++++++ tests/agent/test_task_cancel.py | 2 +- 4 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 tests/agent/test_loop_concurrency.py diff --git a/docs/configuration.md b/docs/configuration.md index f687db940..b6a6986ae 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -188,7 +188,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_MAX_CONCURRENT_REQUESTS` | Unlimited | Maximum concurrently running inbound agent requests. Set a positive integer to apply a cap; unset, `0`, or a negative value means unlimited. | | `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. | diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index a2b967a42..f5859abe7 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -430,8 +430,8 @@ class AgentLoop: ("cron", self._cron_turns), ("local trigger", self._local_trigger_turns), ) - # NANOBOT_MAX_CONCURRENT_REQUESTS: <=0 means unlimited; default 3. - _max = int(os.environ.get("NANOBOT_MAX_CONCURRENT_REQUESTS", "3")) + # NANOBOT_MAX_CONCURRENT_REQUESTS: unset or <=0 means unlimited. + _max = int(os.environ.get("NANOBOT_MAX_CONCURRENT_REQUESTS", "0")) self._concurrency_gate: asyncio.Semaphore | None = ( asyncio.Semaphore(_max) if _max > 0 else None ) diff --git a/tests/agent/test_loop_concurrency.py b/tests/agent/test_loop_concurrency.py new file mode 100644 index 000000000..9a6d5f63d --- /dev/null +++ b/tests/agent/test_loop_concurrency.py @@ -0,0 +1,47 @@ +from __future__ import annotations + +from types import SimpleNamespace +from unittest.mock import MagicMock + +import pytest + + +def _provider() -> MagicMock: + provider = MagicMock() + provider.get_default_model.return_value = "test-model" + provider.generation = SimpleNamespace( + max_tokens=4096, + temperature=0.1, + reasoning_effort=None, + ) + return provider + + +def test_request_concurrency_is_unlimited_by_default( + monkeypatch: pytest.MonkeyPatch, + loop_factory, +) -> None: + monkeypatch.delenv("NANOBOT_MAX_CONCURRENT_REQUESTS", raising=False) + + loop = loop_factory(provider=_provider(), patch_deps=True) + + assert loop._concurrency_gate is None + + +@pytest.mark.asyncio +async def test_positive_request_concurrency_keeps_explicit_cap( + monkeypatch: pytest.MonkeyPatch, + loop_factory, +) -> None: + monkeypatch.setenv("NANOBOT_MAX_CONCURRENT_REQUESTS", "2") + loop = loop_factory(provider=_provider(), patch_deps=True) + gate = loop._concurrency_gate + + assert gate is not None + for _ in range(2): + await gate.acquire() + try: + assert gate.locked() + finally: + for _ in range(2): + gate.release() diff --git a/tests/agent/test_task_cancel.py b/tests/agent/test_task_cancel.py index f44516b7b..642772a4b 100644 --- a/tests/agent/test_task_cancel.py +++ b/tests/agent/test_task_cancel.py @@ -254,7 +254,7 @@ class TestDispatch: assert isinstance(second.event, StreamEndEvent) @pytest.mark.asyncio - async def test_processing_lock_serializes(self): + async def test_same_session_dispatches_serialize(self): from nanobot.bus.events import InboundMessage, OutboundMessage loop, bus = _make_loop()