mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-18 18:10:37 +08:00
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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -69,3 +69,4 @@ GenieData/
|
||||
.env
|
||||
dashboard/warker.js
|
||||
dashboard/bun.lock
|
||||
.pua/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-03-24
|
||||
@@ -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`
|
||||
@@ -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
|
||||
126
openspec/changes/fix-runtime-status-star-spec-compliance/spec.md
Normal file
126
openspec/changes/fix-runtime-status-star-spec-compliance/spec.md
Normal 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.
|
||||
@@ -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
|
||||
@@ -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/`
|
||||
@@ -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."""
|
||||
|
||||
Reference in New Issue
Block a user