Files
beaver_project/app-instance/backend/beaver/services/agent_service.py

213 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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))