From b2941ee472d73ac40547dde8f16c98f0dfbbcdba Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Fri, 14 Aug 2026 22:47:50 +0800 Subject: [PATCH] fix: bind citation to phase-owned turn events --- .github/workflows/plugin-api-v2.yml | 28 ---------------- .github/workflows/plugin-api-v3.yml | 51 +++++++++++++++++++++++++++++ README.md | 6 ++-- plugin.py | 16 ++++----- tests/test_plugin.py | 47 +++++++++++++++++--------- 5 files changed, 93 insertions(+), 55 deletions(-) delete mode 100644 .github/workflows/plugin-api-v2.yml create mode 100644 .github/workflows/plugin-api-v3.yml diff --git a/.github/workflows/plugin-api-v2.yml b/.github/workflows/plugin-api-v2.yml deleted file mode 100644 index 7c1876b..0000000 --- a/.github/workflows/plugin-api-v2.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: plugin-api-v2 - -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: 24543445c7b99ca63fcd90b5828f754a148b184c - path: .plugin-contracts - - uses: actions/setup-python@v5 - with: - python-version: "3.13" - - name: Check Plugin API v2 - env: - PYTHONPATH: .plugin-contracts - run: python -m akashic_plugin_contracts check plugin.py diff --git a/.github/workflows/plugin-api-v3.yml b/.github/workflows/plugin-api-v3.yml new file mode 100644 index 0000000..242873b --- /dev/null +++ b/.github/workflows/plugin-api-v3.yml @@ -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/ diff --git a/README.md b/README.md index eea19f2..8c55265 100644 --- a/README.md +++ b/README.md @@ -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。 diff --git a/plugin.py b/plugin.py index 981a307..6d85b5b 100644 --- a/plugin.py +++ b/plugin.py @@ -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 @@ -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): diff --git a/tests/test_plugin.py b/tests/test_plugin.py index c2f76f9..963b192 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -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 @@ -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( @@ -125,7 +125,10 @@ 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() @@ -133,7 +136,7 @@ async def mount(ctx) -> None: 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]§ " @@ -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 @@ -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], @@ -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 == ()