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.

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

on:
pull_request:
push:
branches:
- main

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: a047470a39d4f7d2e6be1d2a8e2824916d52fad1
path: .akashic-core
- uses: actions/checkout@v4
with:
repository: akashic-plugins/citation
ref: a9abeb31c25458b8e799dc6aae25d3e83b912c83
path: .citation
- uses: actions/setup-python@v5
with:
python-version: "3.13"
cache: pip
cache-dependency-path: .akashic-core/requirements.txt
- name: Install pinned Core dependencies
run: python -m pip install -r .akashic-core/requirements.txt pytest pytest-asyncio
- name: Compare v2 and v3 Meme receipts
env:
AKASHIC_AGENT_ROOT: .akashic-core
AKASHIC_CITATION_ROOT: .citation
PYTHONPATH: .akashic-core
run: python -m pytest -q tests/
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@

| 接入方式 | 阶段 |
|---|---|
| `prompt_render_modules()` | `prompt_render.emit` 之后——注入表情包目录说明 |
| `@on_after_reasoning()` | AfterReasoning GATE——解析 meme 标签,附加媒体 |
| v3 `PROMPT_RENDER_EVENT` | 注入表情包目录说明 |
| v3 `AFTER_REASONING_PREPROCESS_EVENT` | 解析 meme 标签,附加媒体 |
| `skill_roots = ("skills",)` | 声明管理 Skill |
| `dashboard_module = "dashboard.py"` | 声明 v3 Dashboard |
| `workspace_roots = ("memes",)` | 取得 Core 分配的表情包资产根 |

插件通过模块命名导出声明静态贡献,通过 `apply(ctx, config)` 自行构建 `MemeCatalog`/`MemeDecorator` 并注册接入点。`citation.protocol` 是硬依赖:Citation 先剥离 cited metadata 并保留 meme tag,Meme 再完成媒体装饰,Citation cleanup 最后清除残留协议标签。Core 只分配生命周期、依赖顺序与 workspace root;Meme 的目录结构、随机选图和 Dashboard 仍由插件拥有。旧 `MemePlugin` 暂时保留,只用于迁移期差分验证。

---

## 运作逻辑

### 1. 初始化(initialize)
### 1. 初始化

从工作区路径(`workspace/memes/`)加载 `manifest.json`,构建 `MemeCatalog` 和 `MemeDecorator` 实例。`MemeCatalog` 按需检测 manifest 的 mtime,变动时自动热重载,不需要重启。

Expand Down
8 changes: 4 additions & 4 deletions dashboard.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
from __future__ import annotations

import os
from pathlib import Path
from typing import Any

from fastapi import FastAPI, HTTPException
from fastapi.responses import FileResponse

from .plugin import _workspace
from agent.plugin_composition import DashboardContext
from .runtime import MemeCatalog

def register(app: FastAPI, plugin_dir: Path, workspace: Path) -> None:
memes_dir = _workspace(plugin_dir, workspace) / "memes"

def register(app: FastAPI, context: DashboardContext) -> None:
memes_dir = context.workspace_root("memes")
catalog = MemeCatalog(memes_dir)

@app.get("/api/dashboard/meme/categories")
Expand Down
79 changes: 63 additions & 16 deletions plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
from pathlib import Path
from typing import Any, cast

from agent.lifecycle.composition import (
AFTER_REASONING_PREPROCESS_EVENT,
PROMPT_RENDER_EVENT,
)
from agent.lifecycle.types import AfterReasoningCtx, PromptRenderCtx
from agent.plugin_composition import Context, ServiceKey
from agent.plugins import Plugin, on_after_reasoning
from agent.prompting import PromptSectionRender
from .runtime import MemeCatalog, MemeDecorator
Expand All @@ -15,6 +20,29 @@
re.IGNORECASE,
)

CITATION_PROTOCOL_SERVICE = ServiceKey[object]("citation.protocol")


def append_meme_prompt(ctx: PromptRenderCtx, catalog: MemeCatalog) -> None:
block = catalog.build_prompt_block()
if not block:
return
ctx.system_sections_bottom.append(
PromptSectionRender(
name="memes",
content=f"# Memes\n\n{block}",
is_static=False,
)
)


def decorate_meme_ctx(ctx: AfterReasoningCtx, decorator: MemeDecorator) -> None:
cleaned, tag = _extract_meme_tag(ctx.reply)
decorated = decorator.decorate(cleaned, meme_tag=tag)
ctx.reply = decorated.content
ctx.media.extend(decorated.media)
ctx.meme_tag = decorated.tag


class MemePromptModule:
slot = "meme.prompt"
Expand All @@ -28,21 +56,41 @@ async def run(self, frame: Any) -> Any:
ctx = frame.slots.get(_CTX_SLOT)
if not isinstance(ctx, PromptRenderCtx):
return frame
block = self._plugin.catalog.build_prompt_block()
if not block:
return frame
ctx.system_sections_bottom.append(
PromptSectionRender(
name="memes",
content=f"# Memes\n\n{block}",
is_static=False,
)
)
append_meme_prompt(ctx, self._plugin.catalog)
return frame


api_version = 3
name = "meme"
version = "1.0.0"
inject: tuple[ServiceKey[object], ...] = (CITATION_PROTOCOL_SERVICE,)
skill_roots = ("skills",)
dashboard_module = "dashboard.py"
workspace_roots = ("memes",)


async def apply(ctx: Context, config: object) -> None:
"""Build Meme domain objects and register their Core-hosted adapters."""

# 1. Domain state remains plugin-owned and reads the assigned workspace.
_ = config
catalog = MemeCatalog(ctx.workspace_root("memes"))
decorator = MemeDecorator(catalog)

# 2. Lifecycle behavior is owned by reversible Fiber effects.
def prompt_listener(prompt: PromptRenderCtx) -> None:
append_meme_prompt(prompt, catalog)

def answer_listener(answer: AfterReasoningCtx) -> None:
decorate_meme_ctx(answer, decorator)

_ = await ctx.on(PROMPT_RENDER_EVENT, prompt_listener)
_ = await ctx.on(AFTER_REASONING_PREPROCESS_EVENT, answer_listener)


class MemePlugin(Plugin):
api_version = 2

@classmethod
def dashboard_module(cls) -> str | None:
return "dashboard.py"
Expand All @@ -53,11 +101,14 @@ def dashboard_module(cls) -> str | None:
@classmethod
def skill_roots(cls) -> tuple[str, ...]:
return ("skills",)

_catalog: Any = None
_decorator: Any = None

async def prepare(self) -> None:
memes_dir = _workspace(self.context.plugin_dir, self.context.workspace) / "memes"
memes_dir = (
_workspace(self.context.plugin_dir, self.context.workspace) / "memes"
)
self._catalog = MemeCatalog(memes_dir)
self._decorator = MemeDecorator(self._catalog)

Expand All @@ -66,11 +117,7 @@ def prompt_render_modules(self) -> list[object]:

@on_after_reasoning()
async def decorate_meme(self, ctx: AfterReasoningCtx) -> AfterReasoningCtx:
cleaned, tag = _extract_meme_tag(ctx.reply)
decorated = self.decorator.decorate(cleaned, meme_tag=tag)
ctx.reply = decorated.content
ctx.media.extend(decorated.media)
ctx.meme_tag = decorated.tag
decorate_meme_ctx(ctx, self.decorator)
return ctx

@property
Expand Down
Loading
Loading