diff --git a/nanobot/agent/subagent.py b/nanobot/agent/subagent.py index 571bcc792..f464e51a1 100644 --- a/nanobot/agent/subagent.py +++ b/nanobot/agent/subagent.py @@ -262,3 +262,11 @@ class SubagentManager: def get_running_count(self) -> int: """Return the number of currently running subagents.""" return len(self._running_tasks) + + def get_running_count_by_session(self, session_key: str) -> int: + """Return the number of currently running subagents for a session.""" + tids = self._session_tasks.get(session_key, set()) + return sum( + 1 for tid in tids + if tid in self._running_tasks and not self._running_tasks[tid].done() + ) diff --git a/nanobot/command/builtin.py b/nanobot/command/builtin.py index 94e46320b..f60e7e87c 100644 --- a/nanobot/command/builtin.py +++ b/nanobot/command/builtin.py @@ -74,6 +74,12 @@ async def cmd_status(ctx: CommandContext) -> OutboundMessage: search_usage_text = usage.format() except Exception: pass # Never let usage fetch break /status + active_tasks = loop._active_tasks.get(ctx.key, []) + task_count = sum(1 for t in active_tasks if not t.done()) + try: + task_count += loop.subagents.get_running_count_by_session(ctx.key) + except Exception: + pass return OutboundMessage( channel=ctx.msg.channel, chat_id=ctx.msg.chat_id, @@ -84,6 +90,7 @@ async def cmd_status(ctx: CommandContext) -> OutboundMessage: session_msg_count=len(session.get_history(max_messages=0)), context_tokens_estimate=ctx_est, search_usage_text=search_usage_text, + active_task_count=task_count, ), metadata={**dict(ctx.msg.metadata or {}), "render_as": "text"}, ) diff --git a/nanobot/utils/helpers.py b/nanobot/utils/helpers.py index 1bfd9f18b..535048855 100644 --- a/nanobot/utils/helpers.py +++ b/nanobot/utils/helpers.py @@ -400,6 +400,7 @@ def build_status_content( session_msg_count: int, context_tokens_estimate: int, search_usage_text: str | None = None, + active_task_count: int = 0, ) -> str: """Build a human-readable runtime status snapshot. @@ -431,6 +432,7 @@ def build_status_content( f"\U0001f4da Context: {ctx_used_str}/{ctx_total_str} ({ctx_pct}%)", f"\U0001f4ac Session: {session_msg_count} messages", f"\u23f1 Uptime: {uptime}", + f"\u26a1 Tasks: {active_task_count} active", ] if search_usage_text: lines.append(search_usage_text) diff --git a/tests/cli/test_restart_command.py b/tests/cli/test_restart_command.py index 697d5fc17..03ca152e2 100644 --- a/tests/cli/test_restart_command.py +++ b/tests/cli/test_restart_command.py @@ -140,6 +140,7 @@ class TestRestartCommand: loop.consolidator.estimate_session_prompt_tokens = MagicMock( return_value=(20500, "tiktoken") ) + loop.subagents.get_running_count_by_session.return_value = 0 msg = InboundMessage(channel="telegram", sender_id="u1", chat_id="c1", content="/status") @@ -151,6 +152,7 @@ class TestRestartCommand: assert "Context: 20k/65k (31%)" in response.content assert "Session: 3 messages" in response.content assert "Uptime: 2m 5s" in response.content + assert "Tasks: 0 active" in response.content assert response.metadata == {"render_as": "text"} @pytest.mark.asyncio @@ -179,6 +181,7 @@ class TestRestartCommand: loop.consolidator.estimate_session_prompt_tokens = MagicMock( return_value=(0, "none") ) + loop.subagents.get_running_count_by_session.return_value = 0 response = await loop._process_message( InboundMessage(channel="telegram", sender_id="u1", chat_id="c1", content="/status") @@ -187,6 +190,7 @@ class TestRestartCommand: assert response is not None assert "Tokens: 1200 in / 34 out" in response.content assert "Context: 1k/65k (1%)" in response.content + assert "Tasks: 0 active" in response.content @pytest.mark.asyncio async def test_process_direct_preserves_render_metadata(self): @@ -195,6 +199,7 @@ class TestRestartCommand: session.get_history.return_value = [] loop.sessions.get_or_create.return_value = session loop.subagents.get_running_count.return_value = 0 + loop.subagents.get_running_count_by_session.return_value = 0 response = await loop.process_direct("/status", session_key="cli:test") diff --git a/tests/test_build_status.py b/tests/test_build_status.py index d98301cf7..acbef416f 100644 --- a/tests/test_build_status.py +++ b/tests/test_build_status.py @@ -15,6 +15,7 @@ def test_status_shows_cache_hit_rate(): ) assert "60% cached" in content assert "2000 in / 300 out" in content + assert "Tasks: 0 active" in content def test_status_no_cache_info(): @@ -30,6 +31,7 @@ def test_status_no_cache_info(): ) assert "cached" not in content.lower() assert "2000 in / 300 out" in content + assert "Tasks: 0 active" in content def test_status_zero_cached_tokens():