Skip to content
Closed
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.

51 changes: 51 additions & 0 deletions .github/workflows/plugin-api-v3.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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: 435bbaf1e6f777951b4622702d7a7796665ed67a
path: .akashic-core
- 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 Citation receipts
env:
AKASHIC_AGENT_ROOT: .akashic-core
PYTHONPATH: .akashic-core
run: python -m pytest -q tests/
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@

| 接入方式 | 阶段 |
|---|---|
| v3 `PROMPT_RENDER_EVENT` | 注入引用协议文本 |
| v3 `AFTER_REASONING_PREPROCESS_EVENT` | 提取 cited ID 到 `persist_assistant_metadata` |
| v3 `AFTER_REASONING_CLEANUP_EVENT` | 清理残留协议标签 |
| v3 `PROMPT_RENDER_AFTER_EVENT_BUS` | legacy Prompt 事件后注入引用协议文本 |
| v3 `AFTER_REASONING_BEFORE_EVENT_BUS` | legacy AfterReasoning 事件前提取 cited ID |
| v3 `AFTER_REASONING_BEFORE_PERSIST` | 持久化前清理残留协议标签 |

插件通过模块命名导出 `api_version = 3` 与 `apply(ctx, config)` 注册这些 listener,并提供 `citation.protocol` Service 给依赖引用协议顺序的插件。旧 `CitationPlugin` 与 phase module 暂时保留,只用于迁移期行为等价验证;新 Core 不再从固定 PluginManager 列表装配 Citation。

Expand Down
16 changes: 8 additions & 8 deletions plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
from dataclasses import dataclass
from typing import Any, cast

from agent.lifecycle.composition import (
AFTER_REASONING_CLEANUP_EVENT,
AFTER_REASONING_PREPROCESS_EVENT,
PROMPT_RENDER_EVENT,
from agent.turn_events.after_reasoning import (
AFTER_REASONING_BEFORE_EVENT_BUS,
AFTER_REASONING_BEFORE_PERSIST,
)
from agent.turn_events.prompt_render import PROMPT_RENDER_AFTER_EVENT_BUS
from agent.lifecycle.types import AfterReasoningCtx, PromptRenderCtx
from agent.plugin_composition import Context, ServiceKey
from agent.plugins import Plugin
Expand Down Expand Up @@ -139,12 +139,12 @@ async def apply(ctx: Context, config: object) -> None:

# 1. Register the three behaviorally equivalent lifecycle listeners.
_ = config
await ctx.on(PROMPT_RENDER_EVENT, append_citation_protocol)
await ctx.on(AFTER_REASONING_PREPROCESS_EVENT, _persist_v3_citation)
await ctx.on(AFTER_REASONING_CLEANUP_EVENT, cleanup_protocol_tags)
_ = await ctx.on(PROMPT_RENDER_AFTER_EVENT_BUS, append_citation_protocol)
_ = await ctx.on(AFTER_REASONING_BEFORE_EVENT_BUS, _persist_v3_citation)
_ = await ctx.on(AFTER_REASONING_BEFORE_PERSIST, cleanup_protocol_tags)

# 2. Publish last so dependents unload before citation listeners disappear.
await ctx.provide(CITATION_PROTOCOL_SERVICE, CitationProtocol())
_ = await ctx.provide(CITATION_PROTOCOL_SERVICE, CitationProtocol())


class CitationPlugin(Plugin):
Expand Down
47 changes: 31 additions & 16 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@

import plugin as citation_module
from agent.core.response_parser import ResponseMetadata
from agent.lifecycle.composition import (
AFTER_REASONING_CLEANUP_EVENT,
AFTER_REASONING_PREPROCESS_EVENT,
PROMPT_RENDER_EVENT,
from agent.turn_events.after_reasoning import (
AFTER_REASONING_BEFORE_EVENT_BUS,
AFTER_REASONING_BEFORE_PERSIST,
)
from agent.turn_events.prompt_render import PROMPT_RENDER_AFTER_EVENT_BUS
from agent.lifecycle.types import AfterReasoningCtx, PromptRenderCtx
from agent.plugin_composition import CompositionRoot, PluginRuntime
from agent.plugin_composition import CompositionRoot, Context, PluginRuntime
from agent.plugins.composable import ComposablePlugin
from agent.plugins.manager import PluginManager
from bus.event_bus import EventBus
Expand Down Expand Up @@ -107,10 +107,10 @@ async def test_after_reasoning_modules_strip_and_persist() -> None:
async def test_v3_named_exports_match_legacy_lifecycle_behavior(
tmp_path: Path,
) -> None:
ComposablePlugin.from_module(citation_module)
_ = ComposablePlugin.from_module(citation_module)
root = CompositionRoot("citation-parity")

async def mount(ctx) -> None:
async def mount(ctx: Context) -> None:
await apply(ctx, object())

_ = await root.mount(
Expand All @@ -125,15 +125,18 @@ async def mount(ctx) -> None:
config=object(),
),
)
assert root.receipt().ready is True
receipt = root.receipt()
assert receipt.ready is True
assert receipt.writes == ()
assert receipt.external_effects == ()
assert root.context.require(CITATION_PROTOCOL_SERVICE).version == 1

legacy_prompt = _prompt_ctx()
await CitationPromptModule().run(
SimpleNamespace(slots={"prompt:ctx": legacy_prompt})
)
v3_prompt = _prompt_ctx()
await root.context.serial(PROMPT_RENDER_EVENT, v3_prompt)
_ = await root.context.serial(PROMPT_RENDER_AFTER_EVENT_BUS, v3_prompt)
assert v3_prompt.system_sections_bottom == legacy_prompt.system_sections_bottom

reply = "答复正文\n§cited:[mem_1]§ <meme:shy>"
Expand All @@ -142,14 +145,16 @@ async def mount(ctx) -> None:
await CitationAfterReasoningModule().run(legacy_frame)
await ProtocolTagCleanupModule().run(legacy_frame)
v3_answer = _answer_ctx(reply)
await root.context.serial(AFTER_REASONING_PREPROCESS_EVENT, v3_answer)
await root.context.serial(AFTER_REASONING_CLEANUP_EVENT, v3_answer)
_ = await root.context.serial(AFTER_REASONING_BEFORE_EVENT_BUS, v3_answer)
_ = await root.context.serial(AFTER_REASONING_BEFORE_PERSIST, v3_answer)

assert v3_answer.reply == legacy_answer.reply
assert v3_answer.persist_assistant_metadata["cited_memory_ids"] == (
legacy_frame.slots["persist:assistant:cited_memory_ids"]
)
await root.dispose()
assert root.receipt().services == ()
assert root.receipt().effects == ()


@pytest.mark.asyncio
Expand All @@ -158,10 +163,16 @@ async def test_v3_plugin_loads_through_real_generation_manager(
) -> None:
plugin_home = tmp_path / "plugins"
plugin_home.mkdir()
shutil.copytree(
_ = shutil.copytree(
Path(__file__).parents[1],
plugin_home / "citation",
ignore=shutil.ignore_patterns(".git", ".pytest_cache", "__pycache__"),
ignore=shutil.ignore_patterns(
".akashic-core",
".git",
".plugin-contracts",
".pytest_cache",
"__pycache__",
),
)
manager = PluginManager(
plugin_dirs=[plugin_home],
Expand All @@ -180,8 +191,12 @@ async def test_v3_plugin_loads_through_real_generation_manager(
assert snapshot.composition_topology is not None
assert "citation.protocol" in snapshot.composition_topology.services
assert snapshot.composition_topology.listeners == (
"serial:turn.prompt_render:citation",
"serial:turn.after_reasoning.preprocess:citation",
"serial:turn.after_reasoning.cleanup:citation",
"serial:turn.prompt_render.after_event_bus:citation",
"serial:turn.after_reasoning.before_event_bus:citation",
"serial:turn.after_reasoning.before_persist:citation",
)
root = snapshot.composition_root
assert root is not None
await manager.terminate_all()
assert root.receipt().services == ()
assert root.receipt().effects == ()
Loading