feat: add name field to protocol status and message tracking

- Add _message_count and _last_activity_timestamp to orchestrator
- Add record_activity() method to orchestrator
- Add name field to get_protocol_status returns
- Add total_messages and last_activity to get_stats
- Update tests to verify new fields
This commit is contained in:
LIghtJUNction
2026-03-24 17:42:40 +08:00
parent dbeb104600
commit f7ec5ea1c1
10 changed files with 388 additions and 5 deletions

1
.gitignore vendored
View File

@@ -69,3 +69,4 @@ GenieData/
.env
dashboard/warker.js
dashboard/bun.lock
.pua/

View File

@@ -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.

View File

@@ -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

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-24

View File

@@ -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`

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -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/`

View File

@@ -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."""