diff --git a/.gitignore b/.gitignore index 6efde63b3..fe3f30dd2 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,4 @@ GenieData/ .env dashboard/warker.js dashboard/bun.lock +.pua/ diff --git a/astrbot/_internal/runtime/orchestrator.py b/astrbot/_internal/runtime/orchestrator.py index 2b5fd9ded..268807a05 100644 --- a/astrbot/_internal/runtime/orchestrator.py +++ b/astrbot/_internal/runtime/orchestrator.py @@ -42,6 +42,8 @@ class AstrbotOrchestrator(BaseAstrbotOrchestrator): self._running = False self._stars: dict[str, Any] = {} + self._message_count: int = 0 + self._last_activity_timestamp: float | None = None # Auto-register RuntimeStatusStar self._runtime_status_star = RuntimeStatusStar() @@ -137,6 +139,12 @@ class AstrbotOrchestrator(BaseAstrbotOrchestrator): """List all registered star names.""" return list(self._stars.keys()) + def record_activity(self) -> None: + """Record a message activity for stats tracking.""" + self._message_count += 1 + import time + self._last_activity_timestamp = time.time() + async def shutdown(self) -> None: """ Shutdown the orchestrator and all protocol clients. diff --git a/astrbot/_internal/stars/runtime_status_star.py b/astrbot/_internal/stars/runtime_status_star.py index a692f054e..642849719 100644 --- a/astrbot/_internal/stars/runtime_status_star.py +++ b/astrbot/_internal/stars/runtime_status_star.py @@ -12,6 +12,7 @@ from __future__ import annotations import time from dataclasses import dataclass, field +from datetime import datetime, timezone from typing import Any @@ -76,24 +77,28 @@ class RuntimeStatusStar: """Get status of each protocol client.""" if not self._orchestrator: return { - "lsp": {"connected": False}, - "mcp": {"connected": False}, - "acp": {"connected": False}, - "abp": {"connected": False}, + "lsp": {"connected": False, "name": "lsp-client"}, + "mcp": {"connected": False, "name": "mcp-client"}, + "acp": {"connected": False, "name": "acp-client"}, + "abp": {"connected": False, "name": "abp-client"}, } return { "lsp": { "connected": getattr(self._orchestrator.lsp, "connected", False), + "name": "lsp-client", }, "mcp": { "connected": getattr(self._orchestrator.mcp, "connected", False), + "name": "mcp-client", }, "acp": { "connected": getattr(self._orchestrator.acp, "connected", False), + "name": "acp-client", }, "abp": { "connected": getattr(self._orchestrator.abp, "connected", False), + "name": "abp-client", }, } @@ -107,6 +112,14 @@ class RuntimeStatusStar: def _get_stats(self) -> dict[str, Any]: """Get message counts and metrics.""" - return { + result = { "uptime_seconds": time.time() - self._start_time, } + if self._orchestrator: + result["total_messages"] = getattr(self._orchestrator, "_message_count", 0) + last_ts = getattr(self._orchestrator, "_last_activity_timestamp", None) + if last_ts is not None: + result["last_activity"] = datetime.fromtimestamp(last_ts, tz=timezone.utc).isoformat() + else: + result["last_activity"] = None + return result diff --git a/openspec/changes/fix-runtime-status-star-spec-compliance/.openspec.yaml b/openspec/changes/fix-runtime-status-star-spec-compliance/.openspec.yaml new file mode 100644 index 000000000..2ca4bc851 --- /dev/null +++ b/openspec/changes/fix-runtime-status-star-spec-compliance/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-03-24 diff --git a/openspec/changes/fix-runtime-status-star-spec-compliance/design.md b/openspec/changes/fix-runtime-status-star-spec-compliance/design.md new file mode 100644 index 000000000..b47c13bd0 --- /dev/null +++ b/openspec/changes/fix-runtime-status-star-spec-compliance/design.md @@ -0,0 +1,82 @@ +# Design: Fix RuntimeStatusStar Spec Compliance + +## Problem + +The RuntimeStatusStar implementation does not match its specification: + +1. `get_protocol_status` should return protocol name along with connected state +2. `get_stats` should return `total_messages` and `last_activity` not just `uptime_seconds` + +## Solution + +### 1. Update `get_protocol_status` + +**Current implementation:** +```python +return { + "lsp": {"connected": getattr(self._orchestrator.lsp, "connected", False)}, + "mcp": {"connected": getattr(self._orchestrator.mcp, "connected", False)}, + ... +} +``` + +**Fixed implementation:** +```python +return { + "lsp": { + "connected": getattr(self._orchestrator.lsp, "connected", False), + "name": "lsp-client" + }, + "mcp": { + "connected": getattr(self._orchestrator.mcp, "connected", False), + "name": "mcp-client" + }, + ... +} +``` + +### 2. Update `get_stats` and add message tracking + +**Orchestrator changes:** +```python +def __init__(self) -> None: + ... + self._message_count = 0 + self._last_activity_timestamp: float | None = None + +def record_activity(self) -> None: + """Record a message activity for stats tracking.""" + self._message_count += 1 + self._last_activity_timestamp = time.time() +``` + +**RuntimeStatusStar changes:** +```python +def _get_stats(self) -> dict[str, Any]: + last_activity = None + if self._orchestrator and self._orchestrator._last_activity_timestamp: + last_activity = datetime.fromtimestamp( + self._orchestrator._last_activity_timestamp + ).isoformat() + + return { + "total_messages": getattr(self._orchestrator, "_message_count", 0), + "last_activity": last_activity, + "uptime_seconds": time.time() - self._start_time, + } +``` + +## Files to Modify + +1. `astrbot/_internal/runtime/orchestrator.py`: + - Add `_message_count: int = 0` + - Add `_last_activity_timestamp: float | None = None` + - Add `record_activity()` method + +2. `astrbot/_internal/stars/runtime_status_star.py`: + - Update `_get_protocol_status()` to include `name` field + - Update `_get_stats()` to return `total_messages` and `last_activity` + +3. `tests/unit/test_runtime_status_star.py`: + - Update `test_get_protocol_status` to verify `name` field + - Update `test_get_stats` to verify `total_messages` and `last_activity` diff --git a/openspec/changes/fix-runtime-status-star-spec-compliance/proposal.md b/openspec/changes/fix-runtime-status-star-spec-compliance/proposal.md new file mode 100644 index 000000000..aecbfaa01 --- /dev/null +++ b/openspec/changes/fix-runtime-status-star-spec-compliance/proposal.md @@ -0,0 +1,30 @@ +## Why + +The RuntimeStatusStar was implemented based on a spec, but the implementation does not match the specification: + +1. `get_protocol_status` spec requires a `name` field per protocol, but implementation only returns `connected` +2. `get_stats` spec requires `total_messages` and `last_activity` fields, but implementation only returns `uptime_seconds` + +This spec-implementation mismatch means external consumers cannot rely on the documented interface. + +## What Changes + +- Update `get_protocol_status` to include `name` field for each protocol client (lsp, mcp, acp, abp) +- Update `get_stats` to return `total_messages` and `last_activity` as specified +- Add message tracking infrastructure to the orchestrator to support `total_messages` and `last_activity` +- Update tests to verify spec compliance + +## Capabilities + +### Modified Capabilities +- `RuntimeStatusStar.get_protocol_status`: Now returns `{"lsp": {"connected": bool, "name": "lsp-client"}, ...}` +- `RuntimeStatusStar.get_stats`: Now returns `{"total_messages": int, "last_activity": str, "uptime_seconds": float}` + +### New Infrastructure +- Orchestrator message tracking: `_message_count` and `_last_activity_timestamp` + +## Impact + +- Modifies: `astrbot/_internal/stars/runtime_status_star.py` - add name fields and message stats +- Modifies: `astrbot/_internal/runtime/orchestrator.py` - add message tracking counters +- Modifies: `tests/unit/test_runtime_status_star.py` - update tests to verify spec compliance diff --git a/openspec/changes/fix-runtime-status-star-spec-compliance/spec.md b/openspec/changes/fix-runtime-status-star-spec-compliance/spec.md new file mode 100644 index 000000000..60fb36040 --- /dev/null +++ b/openspec/changes/fix-runtime-status-star-spec-compliance/spec.md @@ -0,0 +1,126 @@ +# RuntimeStatusStar Specification (Corrected) + +## Overview + +RuntimeStatusStar is an internal ABP (AstrBot Protocol) star plugin that exposes runtime state information via callable tools. It provides diagnostic capabilities for monitoring the AstrBot core runtime health. + +## Tool Interface + +### Tools Exposed + +All tools follow the ABP tool calling convention via `call_tool(tool_name, arguments)`. + +#### 1. get_runtime_status + +Returns the current runtime state. + +**Arguments:** None + +**Returns:** +```json +{ + "running": true, + "uptime_seconds": 12345.67 +} +``` + +#### 2. get_protocol_status + +Returns the connection state of each protocol client. + +**Arguments:** None + +**Returns:** +```json +{ + "lsp": {"connected": true, "name": "lsp-client"}, + "mcp": {"connected": false, "name": "mcp-client"}, + "acp": {"connected": true, "name": "acp-client"}, + "abp": {"connected": true, "name": "abp-client"} +} +``` + +#### 3. get_star_registry + +Returns the list of registered star names. + +**Arguments:** None + +**Returns:** +```json +{ + "stars": ["runtime-status-star", "star-1", "star-2"] +} +``` + +#### 4. get_stats + +Returns runtime statistics and metrics. + +**Arguments:** None + +**Returns:** +```json +{ + "total_messages": 100, + "last_activity": "2024-01-01T00:00:00Z", + "uptime_seconds": 12345.67 +} +``` + +## Architecture + +### Component Hierarchy + +``` +AstrbotOrchestrator + └── RuntimeStatusStar (auto-registered) + └── Tools: get_runtime_status, get_protocol_status, get_star_registry, get_stats +``` + +### Message Tracking + +The orchestrator tracks: +- `_message_count`: Total number of messages processed +- `_last_activity_timestamp`: ISO timestamp of last activity + +### Auto-Registration + +RuntimeStatusStar is instantiated and registered in `AstrbotOrchestrator.__init__()`: + +```python +from astrbot._internal.stars.runtime_status_star import RuntimeStatusStar + +class AstrbotOrchestrator: + def __init__(self): + self._stars: dict[str, Star] = {} + self._message_count = 0 + self._last_activity_timestamp: float | None = None + # Auto-register RuntimeStatusStar + self._runtime_status_star = RuntimeStatusStar() + self._runtime_status_star.set_orchestrator(self) + self._stars["runtime-status-star"] = self._runtime_status_star +``` + +### Orchestrator Reference + +RuntimeStatusStar holds a reference to the orchestrator to query state: + +```python +class RuntimeStatusStar: + def __init__(self): + self._orchestrator = None + + def set_orchestrator(self, orchestrator): + self._orchestrator = orchestrator +``` + +## Implementation Notes + +1. **Thread Safety:** The orchestrator may be accessed from multiple contexts. All state queries should be read-only. + +2. **Latency:** Tool calls should return immediately without blocking. No polling or long-running operations. + +3. **Error Handling:** If any status query fails, return an error message rather than raising an exception. + +4. **Future Extensibility:** Additional stats can be added by extending the `get_stats` tool response structure. diff --git a/openspec/changes/fix-runtime-status-star-spec-compliance/specs/runtime-status-star/spec.md b/openspec/changes/fix-runtime-status-star-spec-compliance/specs/runtime-status-star/spec.md new file mode 100644 index 000000000..e5b3eade5 --- /dev/null +++ b/openspec/changes/fix-runtime-status-star-spec-compliance/specs/runtime-status-star/spec.md @@ -0,0 +1,56 @@ +# Runtime Status Star Specification (Spec Compliance Fix) + +## MODIFIED Requirements + +### Requirement: RuntimeStatusStar provides diagnostic tools + +The RuntimeStatusStar SHALL provide callable tools that expose core runtime internal state for diagnostic purposes. + +#### Scenario: Get runtime status +- **WHEN** ABP client calls `get_runtime_status` tool +- **THEN** returns `{"running": bool, "uptime_seconds": float}` + +#### Scenario: Get protocol status +- **WHEN** ABP client calls `get_protocol_status` tool +- **THEN** returns status of each protocol client (lsp, mcp, acp, abp) +- **AND** each protocol includes `connected: bool` AND `name: string` fields +- **EXAMPLE**: `{"lsp": {"connected": true, "name": "lsp-client"}, ...}` + +#### Scenario: Get star registry +- **WHEN** ABP client calls `get_star_registry` tool +- **THEN** returns list of registered star names + +#### Scenario: Get stats +- **WHEN** ABP client calls `get_stats` tool +- **THEN** returns `{"total_messages": int, "last_activity": string, "uptime_seconds": float}` +- **AND** `last_activity` is ISO8601 formatted timestamp string + +### Requirement: Auto-registration with orchestrator + +The RuntimeStatusStar SHALL be automatically registered with the orchestrator on initialization. + +#### Scenario: Orchestrator initialization +- **WHEN** AstrbotOrchestrator is created +- **THEN** RuntimeStatusStar instance is created and registered with name "runtime-status-star" + +### Requirement: Orchestrator message tracking + +The orchestrator SHALL track message counts and last activity timestamp for stats reporting. + +#### Scenario: Message tracking +- **WHEN** orchestrator processes a message +- **THEN** `_message_count` is incremented +- **AND** `_last_activity_timestamp` is updated to current time + +#### Scenario: Stats retrieval +- **WHEN** RuntimeStatusStar.get_stats is called +- **THEN** returns `total_messages` from orchestrator's `_message_count` +- **AND** returns `last_activity` as ISO8601 string from `_last_activity_timestamp` + +### Requirement: Error handling + +The RuntimeStatusStar tools SHALL handle errors gracefully without exposing internal exceptions. + +#### Scenario: Orchestrator unavailable +- **WHEN** orchestrator reference is None +- **THEN** returns appropriate error message instead of raising exception diff --git a/openspec/changes/fix-runtime-status-star-spec-compliance/tasks.md b/openspec/changes/fix-runtime-status-star-spec-compliance/tasks.md new file mode 100644 index 000000000..171f42fa2 --- /dev/null +++ b/openspec/changes/fix-runtime-status-star-spec-compliance/tasks.md @@ -0,0 +1,34 @@ +# Tasks: Fix RuntimeStatusStar Spec Compliance + +## Implementation Tasks + +### 1. Update Orchestrator for message tracking + +- [x] Add `_message_count: int = 0` to `__init__` +- [x] Add `_last_activity_timestamp: float | None = None` to `__init__` +- [x] Add `record_activity()` method that increments count and updates timestamp +- [ ] Call `record_activity()` from appropriate message processing points + +### 2. Update RuntimeStatusStar.get_protocol_status + +- [x] Add `name` field to each protocol's return dict +- [x] Verify format matches: `{"connected": bool, "name": "xxx-client"}` + +### 3. Update RuntimeStatusStar.get_stats + +- [x] Import `datetime` for timestamp formatting +- [x] Return `total_messages` from orchestrator's `_message_count` +- [x] Return `last_activity` as ISO formatted timestamp from `_last_activity_timestamp` +- [x] Keep `uptime_seconds` for backward compatibility + +### 4. Update Tests + +- [x] Update `test_get_protocol_status` to verify `name` field exists and has correct value +- [x] Update `test_get_stats` to verify `total_messages` and `last_activity` fields exist +- [x] Add test for `record_activity()` method + +### 5. Verification + +- [x] Run `uv run pytest tests/unit/test_runtime_status_star.py -v` +- [x] Run `uv run pytest tests/unit/test_internal_runtime.py -v` +- [x] Run full test suite: `uv run pytest --cov=astrbot tests/` diff --git a/tests/unit/test_runtime_status_star.py b/tests/unit/test_runtime_status_star.py index e297f2fe4..8baaa01e6 100644 --- a/tests/unit/test_runtime_status_star.py +++ b/tests/unit/test_runtime_status_star.py @@ -24,6 +24,8 @@ class TestRuntimeStatusStar: orchestrator.acp.connected = False orchestrator.abp.connected = True orchestrator.list_stars = AsyncMock(return_value=["star-a", "star-b"]) + orchestrator._message_count = 42 + orchestrator._last_activity_timestamp = 1710000000.0 return orchestrator @pytest.fixture @@ -73,6 +75,12 @@ class TestRuntimeStatusStar: assert result["acp"]["connected"] is False assert result["abp"]["connected"] is True + # Verify name field + assert result["lsp"]["name"] == "lsp-client" + assert result["mcp"]["name"] == "mcp-client" + assert result["acp"]["name"] == "acp-client" + assert result["abp"]["name"] == "abp-client" + @pytest.mark.anyio async def test_get_protocol_status_no_orchestrator(self) -> None: """Test get_protocol_status when no orchestrator is set.""" @@ -111,6 +119,10 @@ class TestRuntimeStatusStar: assert isinstance(result, dict) assert "uptime_seconds" in result assert result["uptime_seconds"] >= 0 + assert "total_messages" in result + assert result["total_messages"] == 42 + assert "last_activity" in result + assert result["last_activity"] is not None @pytest.mark.anyio async def test_unknown_tool(self, star: RuntimeStatusStar) -> None: @@ -118,6 +130,25 @@ class TestRuntimeStatusStar: with pytest.raises(ValueError, match="Unknown tool"): await star.call_tool("unknown_tool", {}) + @pytest.mark.anyio + async def test_record_activity(self) -> None: + """Test orchestrator record_activity() increments message count.""" + from astrbot._internal.runtime.orchestrator import AstrbotOrchestrator + + orchestrator = AstrbotOrchestrator() + assert orchestrator._message_count == 0 + + orchestrator.record_activity() + assert orchestrator._message_count == 1 + + orchestrator.record_activity() + orchestrator.record_activity() + assert orchestrator._message_count == 3 + + assert orchestrator._last_activity_timestamp is not None + + await orchestrator.shutdown() + @pytest.mark.anyio async def test_star_name(self) -> None: """Test star has correct name and description."""