Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 0 additions & 28 deletions .github/workflows/plugin-api-v2.yml

This file was deleted.

67 changes: 67 additions & 0 deletions .github/workflows/plugin-api-v3.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: plugin-api-v3

on:
pull_request:
push:
branches:
- main
workflow_dispatch:

permissions:
contents: read

jobs:
contract:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: akashic-plugins/plugin-contracts
ref: 4dd69dd621e029e51e99aa428443fa3a4ec1f6cf
path: .plugin-contracts
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Check Plugin API v3
env:
PYTHONPATH: .plugin-contracts
run: python -m akashic_plugin_contracts check plugin.py

composition-parity:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: kachofugetsu09/akashic-agent
ref: 3005f838bcd96e2cbc58616aede46e4f39df4523
path: .akashic-core
- uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: pip
cache-dependency-path: .akashic-core/requirements.txt
- name: Install exact Core runtime
run: |
python -m venv .venv
.venv/bin/python -m pip install \
-r .akashic-core/requirements.txt \
-r .akashic-core/requirements-dev.txt \
pytest pytest-asyncio
- name: Verify Observe v3 composition
env:
AKASHIC_AGENT_ROOT: ${{ github.workspace }}/.akashic-core
PYTHONPATH: ${{ github.workspace }}/.akashic-core
run: cd tests && ../.venv/bin/python -m pytest -q .
- name: Verify Mobile panel behavior
run: node --test tests/test_mobile_panel.mjs
- name: Check v3 source types
env:
PYTHONPATH: .akashic-core
run: .venv/bin/pyright --pythonpath .venv/bin/python --level error plugin.py collector.py dashboard.py
- name: Compile Python sources
run: python -m compileall -q .
- name: Check diff formatting
run: git diff --check
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
# observe

Akashic 可观测性插件,负责采集 Turn、检索、记忆写入和全局错误遥测。
Akashic 可观测性插件(Plugin API v3),负责采集已提交 Turn 和全局错误遥测,
并提供 Dashboard 与移动端只读投影。

插件通过模块级 `api_version = 3` / `apply(ctx, config)` 接入 Core:

- `turn.after_turn.committed`:在 Core 提交 `TurnCommitted` 后写入 Observe 数据库;
- `core.ui_slots`:发布移动端静态资源和只读 query;
- `workspace_roots = ("observe",)`:数据库、retention marker 和候选副本都由 Core 分配。

Observe 不再导出 v2 `Plugin` class、`activate()`、`terminate()`、`mobile_ui()` 或
`mobile_ui_query()` ABI。Dashboard 使用 `register(app, DashboardContext)`,只读取当前
generation 的声明式 workspace root。

历史 `rag_queries` 与 `memory_writes` 表保留供兼容读取;Core
`5f2c8fb5c64496897475bd3226812b2e17fcf37e` 提供的
`proactive.finished`、`memory.retrieval.completed` 和 `memory.written` typed
Observe seam 都直接转换为既有 Observe 表结构,不复制或伪造领域 DTO。

插件测试与 CI 固定使用上述 Core commit;candidate 验证只写 Core 分配的临时
workspace,formal publish 后才继续写正式 `observe` workspace。

## 移动端

Expand Down
5 changes: 5 additions & 0 deletions akashic.plugin.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
schema_version = 1
name = "observe"
version = "1.3.0"
api_version = 3
entrypoint = "plugin.py"
69 changes: 41 additions & 28 deletions collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import threading
import traceback
import types
from collections.abc import Callable
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from datetime import datetime, timezone
from pathlib import Path
Expand Down Expand Up @@ -84,11 +84,8 @@ class GlobalErrorCollector:
def __init__(
self,
writer: _Emitter,
*,
create_task: Callable[..., asyncio.Task[Any]] = asyncio.create_task,
) -> None:
self._writer = writer
self._create_task = create_task
self._lock = threading.Lock()
self._buckets: dict[tuple[str, str], _BucketAgg] = {}
self._flush_task: asyncio.Task[None] | None = None
Expand All @@ -102,34 +99,50 @@ def __init__(

# ── 生命周期 ─────────────────────────────────

def install(self) -> None:
async def install(
self,
*,
spawn_task: Callable[..., Awaitable[asyncio.Task[Any]]] | None = None,
) -> None:
if self._installed:
return
self._installed = True
# 1. root logging handler(level >= ERROR)
handler = _CollectorLogHandler(self)
logging.getLogger().addHandler(handler)
self._log_handler = handler
# 2. 同步未捕获异常
self._prev_excepthook = sys.excepthook
sys.excepthook = self._on_sys_except
# 3. 线程崩溃
self._prev_threadhook = threading.excepthook
threading.excepthook = self._on_thread_except
# 4. asyncio 任务异常 + flush task
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None
if loop is not None:
self._loop = loop
self._prev_loop_handler = loop.get_exception_handler()
loop.set_exception_handler(self._on_loop_except)
self._flush_task = self._create_task(
self._flush_loop(),
name="observe_error_flush",
)
logger.info("global error collector installed")
# 1. root logging handler(level >= ERROR)
handler = _CollectorLogHandler(self)
logging.getLogger().addHandler(handler)
self._log_handler = handler
# 2. 同步未捕获异常
self._prev_excepthook = sys.excepthook
sys.excepthook = self._on_sys_except
# 3. 线程崩溃
self._prev_threadhook = threading.excepthook
threading.excepthook = self._on_thread_except
# 4. asyncio 任务异常 + flush task
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None
if loop is not None:
self._loop = loop
self._prev_loop_handler = loop.get_exception_handler()
loop.set_exception_handler(self._on_loop_except)
flush = self._flush_loop()
if spawn_task is None:
self._flush_task = asyncio.create_task(
flush,
name="observe_error_flush",
)
else:
self._flush_task = await spawn_task(
flush,
name="observe_error_flush",
)
logger.info("global error collector installed")
except BaseException:
# 安装是事务性的:任务准入失败时不得遗留进程级 hook。
await self.uninstall()
raise

async def uninstall(self) -> None:
if not self._installed:
Expand Down
14 changes: 10 additions & 4 deletions dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

from fastapi import FastAPI

from agent.plugin_composition import DashboardContext

from .db import open_db

# Observe monitoring dashboard: aggregates the agent-loop telemetry written to
Expand Down Expand Up @@ -38,8 +40,8 @@ def _resolve_range(range_token: str) -> tuple[str | None, int]:


class ObserveDashboardReader:
def __init__(self, workspace: Path) -> None:
self.db_path = workspace / "observe" / "observe.db"
def __init__(self, observe_root: Path) -> None:
self.db_path = observe_root / "observe.db"
self._lock = threading.RLock()

# Aggregate the metric-card figures over the selected window.
Expand Down Expand Up @@ -356,8 +358,12 @@ def _global_occurrences(
return out


def register(app: FastAPI, plugin_dir: Path, workspace: Path) -> None:
reader = ObserveDashboardReader(workspace)
def register(app: FastAPI, context: DashboardContext) -> None:
"""Register read-only Observe routes against the generation-owned workspace."""

# 1. Dashboard only receives the declared Observe root, never the full workspace.
observe_root = context.workspace_root("observe")
reader = ObserveDashboardReader(observe_root)

@app.get("/api/dashboard/observe/overview")
def observe_overview(range: str = "24h") -> dict[str, Any]:
Expand Down
4 changes: 2 additions & 2 deletions mobile_kvcache.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
class KVCacheDashboardReader:
"""从 observe 数据库读取桌面与移动端共用的 KV Cache 投影。"""

def __init__(self, workspace: Path) -> None:
self.db_path = workspace / "observe" / "observe.db"
def __init__(self, observe_root: Path) -> None:
self.db_path = observe_root / "observe.db"
self._lock = threading.RLock()

def get_summary(self) -> dict[str, Any]:
Expand Down
Loading
Loading