From d79d67b1d3a1c35604829ba43ec3a715b6acadac Mon Sep 17 00:00:00 2001 From: iomgaa Date: Mon, 6 Jul 2026 22:36:31 -0400 Subject: [PATCH] =?UTF-8?q?feat(core/agent):=20=E6=B7=BB=E5=8A=A0=20ToolDi?= =?UTF-8?q?spatcher=20Protocol=20=E5=92=8C=20AgentLoopSpec=20hookspec?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ToolDispatcher async + context 参数。AgentLoopSpec 四个 async 生命周期 hook。 Co-Authored-By: Claude Opus 4.6 (1M context) --- core/agent/protocols.py | 45 ++++++++++++++++++++++++++++++ tests/unit/test_agent_protocols.py | 30 ++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 core/agent/protocols.py create mode 100644 tests/unit/test_agent_protocols.py diff --git a/core/agent/protocols.py b/core/agent/protocols.py new file mode 100644 index 0000000..258feaf --- /dev/null +++ b/core/agent/protocols.py @@ -0,0 +1,45 @@ +"""Agent 专属 Protocol 端口。""" +from __future__ import annotations + +from typing import Any, Protocol, runtime_checkable + +import pluggy + +from core.agent.types import LoopResult, Step + +hookspec = pluggy.HookspecMarker("agent_loop") +hookimpl = pluggy.HookimplMarker("agent_loop") + + +@runtime_checkable +class ToolDispatcher(Protocol): + """Agent 工具调度端口。无效工具名抛 ValueError。""" + + async def dispatch( + self, tool_name: str, args: dict[str, Any], *, context: dict[str, Any] + ) -> str: ... + + +class AgentLoopSpec: + """AgentLoop 生命周期扩展点。 + + 每个 hookimpl 可选择观察(返回 None)或变换(返回值)。 + """ + + @hookspec + async def before_step( + self, iteration: int, messages: list[dict[str, Any]] + ) -> None: ... + + @hookspec + async def after_tool( + self, iteration: int, step: Step + ) -> str | None: ... + + @hookspec + async def after_step( + self, iteration: int, messages: list[dict[str, Any]] + ) -> None: ... + + @hookspec + async def on_finish(self, result: LoopResult) -> None: ... diff --git a/tests/unit/test_agent_protocols.py b/tests/unit/test_agent_protocols.py new file mode 100644 index 0000000..306a878 --- /dev/null +++ b/tests/unit/test_agent_protocols.py @@ -0,0 +1,30 @@ +"""core/agent/protocols.py 单元测试。""" +from __future__ import annotations + +from typing import Any + +import pluggy + +from core.agent.protocols import AgentLoopSpec, ToolDispatcher + + +class _FakeDispatcher: + async def dispatch( + self, tool_name: str, args: dict[str, Any], *, context: dict[str, Any] + ) -> str: + return f"executed {tool_name}" + + +def test_fake_dispatcher_satisfies_protocol() -> None: + assert isinstance(_FakeDispatcher(), ToolDispatcher) + +def test_plain_object_not_dispatcher() -> None: + assert not isinstance(object(), ToolDispatcher) + +def test_hookspec_can_register() -> None: + pm = pluggy.PluginManager("agent_loop") + pm.add_hookspecs(AgentLoopSpec) + assert pm.hook.before_step is not None + assert pm.hook.after_tool is not None + assert pm.hook.after_step is not None + assert pm.hook.on_finish is not None