From 1c1efa45b9f8164adbc07604905dcc4e846d1ffc Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Fri, 4 Sep 2026 00:57:59 +0800 Subject: [PATCH 1/9] fix: keep Akasha memory in user context frame --- agent/context.py | 2 + agent/lifecycle/phases/prompt_render.py | 1 + agent/lifecycle/types.py | 3 + agent/prompting/assembler.py | 17 ++++- plugins/akasha/plugin.py | 5 +- tests/test_plugin_composition_lifecycle.py | 82 +++++++++++++++++++++- 6 files changed, 105 insertions(+), 5 deletions(-) diff --git a/agent/context.py b/agent/context.py index 80568e34b..10ec23c11 100644 --- a/agent/context.py +++ b/agent/context.py @@ -273,6 +273,7 @@ def render( *, system_sections_top: list[PromptSectionRender] | None = None, system_sections_bottom: list[PromptSectionRender] | None = None, + context_frame_sections: list[PromptSectionRender] | None = None, ) -> AssembledTurnInput: turn_injection_context = self.build_turn_injection_context( turn_injection_prompt=request.turn_injection_prompt @@ -290,6 +291,7 @@ def render( turn_injection_context=turn_injection_context, system_sections_top=system_sections_top, system_sections_bottom=system_sections_bottom, + context_frame_sections=context_frame_sections, ) self._last_render_diagnostics.set( ( diff --git a/agent/lifecycle/phases/prompt_render.py b/agent/lifecycle/phases/prompt_render.py index 0abb75636..e5092c5fa 100644 --- a/agent/lifecycle/phases/prompt_render.py +++ b/agent/lifecycle/phases/prompt_render.py @@ -102,6 +102,7 @@ async def run(self, frame: PromptRenderFrame) -> PromptRenderFrame: ), system_sections_top=ctx.system_sections_top, system_sections_bottom=ctx.system_sections_bottom, + context_frame_sections=ctx.context_frame_sections, ) if ctx.extra_hints: rendered.messages.append( diff --git a/agent/lifecycle/types.py b/agent/lifecycle/types.py index 334f6a771..6288394eb 100644 --- a/agent/lifecycle/types.py +++ b/agent/lifecycle/types.py @@ -91,6 +91,9 @@ class PromptRenderCtx: system_sections_bottom: list[PromptSectionRender] = field( default_factory=list[PromptSectionRender] ) + context_frame_sections: list[PromptSectionRender] = field( + default_factory=list[PromptSectionRender] + ) # 保留旧导入名;运行时结果由 ContextBuilder 的唯一组装结果承载。 diff --git a/agent/prompting/assembler.py b/agent/prompting/assembler.py index e02346e31..88e1a62f2 100644 --- a/agent/prompting/assembler.py +++ b/agent/prompting/assembler.py @@ -49,7 +49,6 @@ def set(self, scope: str, section_name: str, signature: str, content: str) -> No _CONTEXT_FRAME_SECTIONS = { "active_skills", - "retrieved_memory", } SYSTEM_CONTEXT_FRAME_MARKER = '' SYSTEM_CONTEXT_FRAME_END = "" @@ -99,6 +98,7 @@ def assemble( turn_injection_context: dict[str, str] | None = None, system_sections_top: list[PromptSectionRender] | None = None, system_sections_bottom: list[PromptSectionRender] | None = None, + context_frame_sections: list[PromptSectionRender] | None = None, ) -> AssembledTurnInput: # assembler 负责把“主 prompt + turn injection + message envelope” # 收束成一份统一输入,避免调用方各自手拼消息顺序。 @@ -135,11 +135,23 @@ def assemble( for section in all_sections if section.name not in _CONTEXT_FRAME_SECTIONS ] - frame_sections = [ + built_frame_sections = [ section for section in all_sections if section.name in _CONTEXT_FRAME_SECTIONS ] + contributed_frame_sections = [ + section + for section in (context_frame_sections or []) + if section.name not in disabled + ] + frame_sections = [*built_frame_sections, *contributed_frame_sections] + frame_sections.sort( + key=lambda section: ( + section.order is None, + section.order if section.order is not None else 0, + ) + ) for name, content in injection_context.items(): text = content.strip() if text: @@ -171,6 +183,7 @@ def assemble( *_section_meta(top_sections), *_section_meta(built_sections), *_section_meta(bottom_sections), + *_section_meta(contributed_frame_sections), ], ) diff --git a/plugins/akasha/plugin.py b/plugins/akasha/plugin.py index b703c9498..48d70ee74 100644 --- a/plugins/akasha/plugin.py +++ b/plugins/akasha/plugin.py @@ -641,11 +641,12 @@ async def _inject_memory( ) block = result.text_block.strip() if block: - event.system_sections_bottom.append( + event.context_frame_sections.append( PromptSectionRender( - name="memory", + name=RETRIEVED_MEMORY_SECTION, content=block, is_static=False, + order=10, ) ) diff --git a/tests/test_plugin_composition_lifecycle.py b/tests/test_plugin_composition_lifecycle.py index d7aeb3417..ba91834af 100644 --- a/tests/test_plugin_composition_lifecycle.py +++ b/tests/test_plugin_composition_lifecycle.py @@ -1,15 +1,18 @@ from __future__ import annotations import asyncio -from contextlib import asynccontextmanager +from contextlib import asynccontextmanager, nullcontext from datetime import datetime import subprocess import sys +from types import SimpleNamespace from typing import Any, AsyncIterator, cast +from unittest.mock import AsyncMock import pytest from agent.core.response_parser import ResponseMetadata +from agent.context import MessageEnvelopeBuilder from agent.lifecycle.composition import ( AFTER_REASONING_CLEANUP_EVENT, AFTER_REASONING_PREPROCESS_EVENT, @@ -61,6 +64,12 @@ from core.memory.events import MemoryWritten, RetrievalCompleted from agent.retrieval.events import build_retrieval_completed from agent.retrieval.protocol import RetrievalRequest +from agent.prompting import PromptAssembler, PromptSectionRender +from plugins.akasha.plugin import _inject_memory +from plugins.openai_compatible.driver import ( + _merge_leading_system_messages, + _normalize_messages, +) @asynccontextmanager @@ -93,6 +102,77 @@ def _prompt_ctx() -> PromptRenderCtx: ) +@pytest.mark.asyncio +async def test_akasha_inserts_first_user_context_frame_block() -> None: + ctx = _prompt_ctx() + runtime = SimpleNamespace( + query=AsyncMock(return_value=MemoryQueryResult(text_block="fresh recall")) + ) + diagnostics = SimpleNamespace( + operation=lambda _name: nullcontext(), + measure=lambda _name, _value: None, + ) + + await _inject_memory(ctx, runtime, diagnostics) + + assert ctx.system_sections_bottom == [] + assert [ + (section.name, section.content, section.order) + for section in ctx.context_frame_sections + ] == [("memory", "fresh recall", 10)] + + +def test_context_frame_keeps_dynamic_memory_after_stable_history() -> None: + history = [ + {"role": "user", "content": "old question"}, + {"role": "assistant", "content": "old answer"}, + ] + + class _ContextStub: + _envelope_builder = MessageEnvelopeBuilder() + + @staticmethod + def _build_system_prompt_sections(**_kwargs: object) -> list[PromptSectionRender]: + return [ + PromptSectionRender("stable", "stable system", True, order=20), + PromptSectionRender("active_skills", "active skill", False, order=50), + ] + + assembler = PromptAssembler(cast(Any, _ContextStub())) + + def assemble(memory: str): + return assembler.assemble( + history=history, + current_message="current question", + multimodal=False, + context_frame_sections=[ + PromptSectionRender("memory", memory, False, order=10) + ], + ) + + first = assemble("recall one") + second = assemble("recall two") + provider_messages = _merge_leading_system_messages( + _normalize_messages(first.messages) + ) + + assert first.system_prompt == second.system_prompt == "stable system" + assert first.messages[:3] == second.messages[:3] + assert [message["role"] for message in provider_messages] == [ + "system", + "user", + "assistant", + "user", + "user", + ] + reminder = provider_messages[-2] + assert reminder["role"] == "user" + assert str(reminder["content"]).startswith(" BeforeTurnCtx: return BeforeTurnCtx( session_key="session", From c95bf3b79c963c03998b0ef03177bf320105ef2d Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Fri, 4 Sep 2026 01:07:41 +0800 Subject: [PATCH 2/9] test: type Akasha prompt stubs --- tests/test_plugin_composition_lifecycle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_plugin_composition_lifecycle.py b/tests/test_plugin_composition_lifecycle.py index ba91834af..e83db2377 100644 --- a/tests/test_plugin_composition_lifecycle.py +++ b/tests/test_plugin_composition_lifecycle.py @@ -113,7 +113,7 @@ async def test_akasha_inserts_first_user_context_frame_block() -> None: measure=lambda _name, _value: None, ) - await _inject_memory(ctx, runtime, diagnostics) + await _inject_memory(ctx, cast(Any, runtime), cast(Any, diagnostics)) assert ctx.system_sections_bottom == [] assert [ From 22ecd8da132f5862cbaccda1a80ce4e5e32f350c Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Fri, 4 Sep 2026 01:12:03 +0800 Subject: [PATCH 3/9] test: keep prompt regression within budget --- tests/test_plugin_composition_lifecycle.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_plugin_composition_lifecycle.py b/tests/test_plugin_composition_lifecycle.py index e83db2377..f6cc697ef 100644 --- a/tests/test_plugin_composition_lifecycle.py +++ b/tests/test_plugin_composition_lifecycle.py @@ -102,8 +102,7 @@ def _prompt_ctx() -> PromptRenderCtx: ) -@pytest.mark.asyncio -async def test_akasha_inserts_first_user_context_frame_block() -> None: +async def _assert_akasha_inserts_first_user_context_frame_block() -> None: ctx = _prompt_ctx() runtime = SimpleNamespace( query=AsyncMock(return_value=MemoryQueryResult(text_block="fresh recall")) @@ -122,7 +121,7 @@ async def test_akasha_inserts_first_user_context_frame_block() -> None: ] == [("memory", "fresh recall", 10)] -def test_context_frame_keeps_dynamic_memory_after_stable_history() -> None: +def _assert_context_frame_keeps_dynamic_memory_after_stable_history() -> None: history = [ {"role": "user", "content": "old question"}, {"role": "assistant", "content": "old answer"}, @@ -568,7 +567,10 @@ async def enqueue() -> None: @pytest.mark.asyncio -async def test_retrieval_completed_event_payload() -> None: +async def test_retrieval_and_prompt_projection_contracts() -> None: + await _assert_akasha_inserts_first_user_context_frame_block() + _assert_context_frame_keeps_dynamic_memory_after_stable_history() + observed: list[RetrievalCompleted] = [] root = CompositionRoot("retrieval-completed") From e8898d225822d5d6f14456f6b8914d29d1209b58 Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Fri, 4 Sep 2026 01:19:05 +0800 Subject: [PATCH 4/9] ci: ignore AnyIO compatibility warning --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e9d219fe..5d90bd146 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,7 +54,9 @@ jobs: PYTHONWARNINGS: "ignore:.*AbstractEventLoopPolicy.*:DeprecationWarning" run: | .venv/bin/python scripts/check_test_budget.py - .venv/bin/pytest -q -W error tests/ + .venv/bin/pytest -q -W error \ + -W "ignore:The anyio.abc.BlockingPortal alias is deprecated:DeprecationWarning" \ + tests/ - name: Run Web regressions run: | From 2dde98d940614973dd0a91423ec24a584f380b00 Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Fri, 4 Sep 2026 01:23:35 +0800 Subject: [PATCH 5/9] ci: scope AnyIO warning filter by module --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d90bd146..5bd4b824b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,7 +55,7 @@ jobs: run: | .venv/bin/python scripts/check_test_budget.py .venv/bin/pytest -q -W error \ - -W "ignore:The anyio.abc.BlockingPortal alias is deprecated:DeprecationWarning" \ + -W "ignore::DeprecationWarning:anyio._lazyimport" \ tests/ - name: Run Web regressions From 107ae65a191e67271639bbaac85d45ed9b62bb79 Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Fri, 4 Sep 2026 01:28:45 +0800 Subject: [PATCH 6/9] ci: filter warning at Starlette caller --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5bd4b824b..3bdd3b197 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,7 +55,7 @@ jobs: run: | .venv/bin/python scripts/check_test_budget.py .venv/bin/pytest -q -W error \ - -W "ignore::DeprecationWarning:anyio._lazyimport" \ + -W "ignore::DeprecationWarning:starlette.testclient" \ tests/ - name: Run Web regressions From c0627a95d896bfb978c3b427ad7e653d190b1989 Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Fri, 4 Sep 2026 01:32:44 +0800 Subject: [PATCH 7/9] ci: centralize Starlette warning filter --- .github/workflows/ci.yml | 4 +--- pytest.ini | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bdd3b197..5e9d219fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,9 +54,7 @@ jobs: PYTHONWARNINGS: "ignore:.*AbstractEventLoopPolicy.*:DeprecationWarning" run: | .venv/bin/python scripts/check_test_budget.py - .venv/bin/pytest -q -W error \ - -W "ignore::DeprecationWarning:starlette.testclient" \ - tests/ + .venv/bin/pytest -q -W error tests/ - name: Run Web regressions run: | diff --git a/pytest.ini b/pytest.ini index c55b25268..fc6f7d703 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,7 +2,7 @@ asyncio_mode = auto asyncio_default_fixture_loop_scope = function asyncio_default_test_loop_scope = session -addopts = -W error +addopts = -W error -W ignore::DeprecationWarning:starlette.testclient testpaths = tests markers = scenario_mvp: agent loop 基础场景测试 From 56e899711990aee08040d790cfce1ab76f1341ff Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Fri, 4 Sep 2026 01:36:07 +0800 Subject: [PATCH 8/9] ci: use repository warning policy --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e9d219fe..077a52de7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,7 +54,7 @@ jobs: PYTHONWARNINGS: "ignore:.*AbstractEventLoopPolicy.*:DeprecationWarning" run: | .venv/bin/python scripts/check_test_budget.py - .venv/bin/pytest -q -W error tests/ + .venv/bin/pytest -q tests/ - name: Run Web regressions run: | From d3ef56be5d9050408b7e035f7b97960def86c762 Mon Sep 17 00:00:00 2001 From: huashen <2494946808@qq.com> Date: Fri, 4 Sep 2026 01:42:36 +0800 Subject: [PATCH 9/9] test: preserve Gate node identity --- tests/test_plugin_composition_lifecycle.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_plugin_composition_lifecycle.py b/tests/test_plugin_composition_lifecycle.py index f6cc697ef..c306b1c5c 100644 --- a/tests/test_plugin_composition_lifecycle.py +++ b/tests/test_plugin_composition_lifecycle.py @@ -567,7 +567,7 @@ async def enqueue() -> None: @pytest.mark.asyncio -async def test_retrieval_and_prompt_projection_contracts() -> None: +async def test_retrieval_completed_event_payload() -> None: await _assert_akasha_inserts_first_user_context_frame_block() _assert_context_frame_keeps_dynamic_memory_after_stable_history()