"""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 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, profile: AgentProfile | None = None, loader: EngineLoader | None = None, ) -> None: self.profile = profile or AgentProfile() self.loader = loader or EngineLoader(workspace=workspace) 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) 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))