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.

71 changes: 71 additions & 0 deletions .github/workflows/plugin-api-v3.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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

plugin-tests:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
repository: kachofugetsu09/akashic-agent
# 5624 lacks the v3 domain-effect lookup export. Keep this exact
# Core commit until it is published; checkout failure is intentional
# release blocking, not permission to weaken the plugin oracle.
ref: 3005f838bcd96e2cbc58616aede46e4f39df4523
path: .akashic-core
- uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: pip
cache-dependency-path: |
.akashic-core/requirements.txt
.akashic-core/requirements-dev.txt
- name: Install exact Core runtime
run: python -m pip install -r .akashic-core/requirements.txt -r .akashic-core/requirements-dev.txt
- name: Run focused plugin tests
env:
AKASHIC_AGENT_ROOT: .akashic-core
PYTHONPATH: .akashic-core
run: python -m pytest -q tests
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Verify mobile panel
run: node --test tests/test_mobile_panel.mjs
- name: Check v3 source types
env:
AKASHIC_AGENT_ROOT: .akashic-core
PYTHONPATH: .akashic-core
run: pyright --level error plugin.py dashboard.py db.py tests
- name: Compile Python sources
run: python -m compileall -q plugin.py dashboard.py db.py drift tests
- name: Check diff formatting
run: git diff --check
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,31 @@

Akashic emotion and proactive tuning plugin.

## v3 接入

入口是 module-level `api_version = 3` 与 `apply(ctx, config)`。Emotion 通过 Core
声明以下能力:

- `PROACTIVE_COMPONENTS`:在 exact generation 中形成 VAD prompt projection;formal
运行由 `emotion.state` domain effect 提交 SQLite,candidate 不打开数据库。
- `BACKGROUND_JOBS`:`feedback-preference-context` Drift 完成后,使用 Core 的 LLM
lease 和窄 documents port 合并 `PROACTIVE_CONTEXT.md` / `proactive_pending.md`。
- `AFTER_TURN_COMMITTED`:消费 Core 已提交的 typed Turn。上游若提供
`extra.proactive_feedback`,按其稳定 identity 幂等写入;显式引用消息则按 Turn
自带标记写入 gold feedback。
- `UI_SLOTS` 与 C09 Dashboard:移动端和桌面端只读 Emotion 自有投影,不读取
`sessions.db`,不取得任意 workspace 句柄。

插件不再声明 v2 `Plugin`、EventBus listener、固定 `proactive_modules()` / `jobs()`
或旧 mobile/dashboard ABI。旧数据库不会在 import/apply 时自动迁移;切换前应先
停用旧 runtime 并使用独立迁移脚本(尚未将旧源删除)。

CI 的 Core pin 是 20062a715d2c5822228b327863b51c8d036119b3,因为旧 pin
5624a059348406c1f97993612adfec886b158158 没有 domain_effect_lookup_export。
该 commit 尚未发布到 Core 的公共默认分支前,CI checkout 失败属于明确的发布阻塞;
本插件必须继续在 integration Core exact worktree 上验证,不得删除 lookup seam 或放宽
candidate/formal oracle。

## 移动端看板

插件通过通用移动 UI 生命周期注册“主动状态”入口,说明用户反馈如何改变 Agent 的语气
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 = "emotion"
version = "3.0.0"
api_version = 3
entrypoint = "plugin.py"
43 changes: 11 additions & 32 deletions dashboard.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from __future__ import annotations

import json
import sqlite3
import threading
Expand All @@ -9,13 +8,14 @@

from fastapi import FastAPI

from agent.plugin_composition import DashboardContext

from .db import EmotionState, describe_behavior


class EmotionDashboardReader:
def __init__(self, workspace: Path) -> None:
self.db_path = workspace / "emotion" / "emotion.db"
self.sessions_db_path = workspace / "sessions.db"
def __init__(self, emotion_root: Path) -> None:
self.db_path = emotion_root / "emotion.db"
self._lock = threading.RLock()

def get_overview(self) -> dict[str, Any]:
Expand All @@ -36,11 +36,10 @@ def list_influences(self, *, limit: int = 30) -> list[dict[str, Any]]:
with _connect(self.db_path) as db:
rows = _influence_rows(db, safe_limit)

# 2. 用事件已持有的消息 ID 补齐可读预览
# 2. 事件 payload 已经是插件自己的完整投影;不越过 Core workspace 边界读 sessions.db。
decoded = [_decode_influence(row) for row in rows]
previews = self._load_user_previews(decoded)
for item in decoded:
item["user_preview"] = _preview(previews.get(str(item["user_message_id"])))
item["user_preview"] = ""
return decoded

def get_mobile_bootstrap(self, *, limit: int = 30) -> dict[str, Any]:
Expand All @@ -56,10 +55,9 @@ def get_mobile_bootstrap(self, *, limit: int = 30) -> dict[str, Any]:
overview = _overview_from_db(db)
decoded = [_decode_influence(row) for row in _influence_rows(db, safe_limit)]

# 2. 会话预览是独立数据源,只补充文案,不参与 emotion 状态一致性
previews = self._load_user_previews(decoded)
# 2. Mobile 只返回 Emotion 自有投影,不取得 Session 持久化 owner。
for item in decoded:
item["user_preview"] = _preview(previews.get(str(item["user_message_id"])))
item["user_preview"] = ""
return {"overview": overview, "items": decoded}

def list_effects(
Expand Down Expand Up @@ -98,22 +96,10 @@ def get_effect(self, effect_id: int) -> dict[str, Any] | None:
).fetchone()
return _decode_effect(row) if row is not None else None

def _load_user_previews(self, items: list[dict[str, Any]]) -> dict[str, str]:
if not items or not self.sessions_db_path.exists():
return {}
ids = list(dict.fromkeys(str(item["user_message_id"]) for item in items))
placeholders = ",".join("?" for _ in ids)
with _connect(self.sessions_db_path) as db:
rows = db.execute(
f"SELECT id, content FROM messages WHERE id IN ({placeholders})",
ids,
).fetchall()
return {str(row["id"]): str(row["content"] or "") for row in rows}

def register(app: FastAPI, context: DashboardContext) -> None:
"""Register Emotion read-only routes against the exact v3 workspace root."""

def register(app: FastAPI, plugin_dir: Path, workspace: Path) -> None:
_ = plugin_dir
reader = EmotionDashboardReader(workspace)
reader = EmotionDashboardReader(context.workspace_root("emotion"))

@app.get("/api/dashboard/emotion/overview")
def get_emotion_overview() -> dict[str, Any]:
Expand Down Expand Up @@ -246,10 +232,3 @@ def _decode_influence(row: sqlite3.Row) -> dict[str, Any]:
metadata = json.loads(str(payload.pop("payload_json")))
payload["user_message_id"] = str(metadata["user_message_id"])
return payload


def _preview(value: str | None, limit: int = 180) -> str:
text = str(value or "").replace("\n", " ").strip()
if len(text) <= limit:
return text
return text[:limit].rstrip() + "..."
Loading
Loading