"""Application service for agent entry. 这层的职责是把“接口层如何调用 AgentLoop”统一收口。 接口层以后不应该各自做这些事情: 1. 自己 new `AgentLoop` 2. 自己决定何时 `boot()` 3. 自己处理 direct run 的同步/异步包装 统一放在 `AgentService` 后,CLI / Web / Gateway 才能共享同一条运行主链。 """ from __future__ import annotations import asyncio from pathlib import Path from typing import Any from beaver.engine import AgentLoop, AgentProfile, AgentRunResult, EngineLoader from beaver.foundation.events import InboundMessage, OutboundMessage class AgentService: """面向 interfaces 的统一 agent 运行入口。 这里明确区分两种调用模式: 1. direct mode - 不启动后台运行循环 - 直接调用 `process_direct()` / `run_direct()` 2. running mode - 先 `await start()` - 之后所有外部任务都必须走 `submit_direct()` - 不允许再直接调用 `process_direct()` """ def __init__( self, *, workspace: str | Path | None = None, config_path: str | Path | None = None, profile: AgentProfile | None = None, loader: EngineLoader | None = None, ) -> None: self.profile = profile or AgentProfile() self.loader = loader or EngineLoader(workspace=workspace, config_path=config_path) self._loop: AgentLoop | None = None self._run_task: asyncio.Task[None] | None = None def create_loop(self) -> AgentLoop: """创建并缓存当前 service 使用的 AgentLoop。""" if self._loop is None: self._loop = AgentLoop(profile=self.profile, loader=self.loader) self._loop.boot() return self._loop @property def has_loop(self) -> bool: """当前 service 是否已经创建过 loop。""" return self._loop is not None @property def is_running(self) -> bool: """当前 service 是否处于 running mode。""" return self._run_task is not None and not self._run_task.done() def close(self) -> None: """关闭当前 service 持有的 runtime。""" if self._run_task is not None and not self._run_task.done(): raise RuntimeError("AgentService.close() requires stop() before closing a running loop") self._run_task = None if self._loop is None: return try: self._loop.close() finally: self._loop = None async def start(self) -> None: """启动后台运行循环,进入 running mode。 进入 running mode 后: - 外部任务必须通过 `submit_direct()` 提交 - `process_direct()` 不再允许直接调用 """ if self._run_task is not None and not self._run_task.done(): return loop = self.create_loop() self._run_task = asyncio.create_task(loop.run()) while not loop.is_running: if self._run_task.done(): await self._run_task break await asyncio.sleep(0) async def _stop_impl( self, *, timeout_seconds: float | None = None, force: bool = False, ) -> None: """内部停止实现,支持 graceful timeout 和可选 force cancel。""" if self._run_task is None: return run_task = self._run_task loop = self.create_loop() try: await loop.stop() if timeout_seconds is None: await run_task else: try: await asyncio.wait_for(asyncio.shield(run_task), timeout=timeout_seconds) except asyncio.TimeoutError as exc: if force: run_task.cancel() try: await run_task except asyncio.CancelledError: pass else: raise TimeoutError( f"AgentService.stop() timed out after {timeout_seconds} seconds while draining queued tasks" ) from exc finally: if run_task.done(): self._run_task = None async def stop( self, *, timeout_seconds: float | None = None, force: bool = False, ) -> None: """停止后台运行循环并等待退出。 参数: - `timeout_seconds`: graceful drain 的最长等待时间;`None` 表示一直等 - `force`: 超时后是否 cancel 掉运行循环 task """ await self._stop_impl(timeout_seconds=timeout_seconds, force=force) async def shutdown( self, *, timeout_seconds: float | None = None, force: bool = False, ) -> None: """先停运行循环,再释放 runtime。""" await self._stop_impl(timeout_seconds=timeout_seconds, force=force) self.close() async def process_direct( self, message: str, **kwargs: Any, ) -> AgentRunResult: """异步 direct run 入口。 仅在 direct mode 下可用。 如果 service 已经 `start()` 进入 running mode, 调用方必须改用 `submit_direct()`,不能绕过运行队列直接执行。 """ if self._run_task is not None and not self._run_task.done(): raise RuntimeError( "AgentService.process_direct() is unavailable while the service is running; " "use 'await AgentService.submit_direct(...)' after start()." ) loop = self.create_loop() return await loop.process_direct(message, **kwargs) async def submit_direct( self, message: str, **kwargs: Any, ) -> AgentRunResult: """向 running mode 下的 loop 提交 direct task。 这是 `start()` 之后唯一合法的外部任务入口。 """ loop = self.create_loop() return await loop.submit_direct(message, **kwargs) async def handle_inbound_message(self, inbound: InboundMessage) -> OutboundMessage: """把 bus inbound 映射成标准 runtime 调用,并返回结构化 outbound。""" try: result = await self.submit_direct( inbound.content, session_id=inbound.session_id, source=f"gateway:{inbound.channel}", user_id=inbound.user_id, title=inbound.title, execution_context=inbound.execution_context, model=inbound.model, provider_name=inbound.provider_name, embedding_model=inbound.embedding_model, ) except Exception as exc: return self.build_outbound_error(inbound, detail=str(exc)) return self.build_outbound_message(inbound, result) @staticmethod def build_outbound_message(inbound: InboundMessage, result: AgentRunResult) -> OutboundMessage: """把一次 runtime 正常结果转成 bus outbound。""" return OutboundMessage( message_id=inbound.message_id, channel=inbound.channel, session_id=result.session_id, run_id=result.run_id, content=result.output_text, finish_reason=result.finish_reason, provider_name=result.provider_name, model=result.model, usage=dict(result.usage), metadata={"inbound_metadata": dict(inbound.metadata)}, ) @staticmethod def build_outbound_error( inbound: InboundMessage, *, detail: str, finish_reason: str = "error", ) -> OutboundMessage: """把 inbound 处理失败转换成结构化 outbound 错误消息。""" return OutboundMessage( message_id=inbound.message_id, channel=inbound.channel, session_id=inbound.session_id, content=detail, finish_reason=finish_reason, metadata={"error": detail, "inbound_metadata": dict(inbound.metadata)}, ) def run_direct( self, message: str, **kwargs: Any, ) -> AgentRunResult: """同步 direct run 包装。 主要给当前 CLI 或简单脚本使用。真正的长期方向仍然是让 interfaces 在 direct mode 下直接走 `await process_direct(...)`。 """ try: asyncio.get_running_loop() except RuntimeError: pass else: raise RuntimeError( "AgentService.run_direct() cannot be used inside an active event loop; " "use 'await AgentService.process_direct(...)' instead." ) return asyncio.run(self.process_direct(message, **kwargs))