修改了nanobot,往Hermes agent的风格走,进度1/3
This commit is contained in:
6
app-instance/backend/beaver/services/__init__.py
Normal file
6
app-instance/backend/beaver/services/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
"""Application services for Beaver."""
|
||||
|
||||
from .agent_service import AgentService
|
||||
from .memory_service import MemoryService
|
||||
|
||||
__all__ = ["AgentService", "MemoryService"]
|
||||
2
app-instance/backend/beaver/services/admin_service.py
Normal file
2
app-instance/backend/beaver/services/admin_service.py
Normal file
@ -0,0 +1,2 @@
|
||||
"""Administrative application service."""
|
||||
|
||||
212
app-instance/backend/beaver/services/agent_service.py
Normal file
212
app-instance/backend/beaver/services/agent_service.py
Normal file
@ -0,0 +1,212 @@
|
||||
"""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))
|
||||
65
app-instance/backend/beaver/services/memory_service.py
Normal file
65
app-instance/backend/beaver/services/memory_service.py
Normal file
@ -0,0 +1,65 @@
|
||||
"""Beaver memory 应用服务。
|
||||
|
||||
这层不是新的 memory 实现,而是对现有 `MemoryStore + MemorySnapshot` 的应用层包装。
|
||||
|
||||
目标只有三个:
|
||||
1. 把“本轮运行前需要 refresh live state”这件事集中到一个地方
|
||||
2. 把“给 context builder 的只能是 frozen snapshot”这条规则写死
|
||||
3. 让 `AgentLoop` 不再直接操作 `MemoryStore` 细节
|
||||
|
||||
设计边界:
|
||||
1. 记忆实际读写逻辑仍然在 `beaver.memory.curated.store.MemoryStore`
|
||||
2. memory tool 仍然直接写 store
|
||||
3. 本服务只负责 runtime 接入策略,不负责 CRUD 业务本身
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from beaver.memory.curated.snapshot import MemorySnapshot, capture_memory_snapshot
|
||||
from beaver.memory.curated.store import MemoryStore
|
||||
|
||||
|
||||
class MemoryService:
|
||||
"""统一封装 runtime 对 curated memory 的访问方式。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
root: str | Path,
|
||||
*,
|
||||
store: MemoryStore | None = None,
|
||||
) -> None:
|
||||
self.root = Path(root)
|
||||
self.store = store or MemoryStore(self.root)
|
||||
self._snapshot: MemorySnapshot | None = None
|
||||
|
||||
def initialize(self) -> None:
|
||||
"""启动时加载一次磁盘内容,建立首份 frozen snapshot 基线。"""
|
||||
|
||||
self.store.load_from_disk()
|
||||
self._snapshot = capture_memory_snapshot(self.store)
|
||||
|
||||
def reload_for_new_run(self) -> None:
|
||||
"""每次新 run 开始前刷新 live state。
|
||||
|
||||
这是 Hermes 风格 memory policy 的关键点:
|
||||
- 上一次会话中通过 tool 写入的持久记忆,下一次运行应该能看到
|
||||
- 但同一次 run 中途写入的新记忆,不应反向修改当前 frozen snapshot
|
||||
"""
|
||||
|
||||
self.store.load_from_disk()
|
||||
self._snapshot = capture_memory_snapshot(self.store)
|
||||
|
||||
def get_snapshot(self) -> MemorySnapshot:
|
||||
"""获取当前 run 应注入 system prompt 的 frozen snapshot。"""
|
||||
|
||||
if self._snapshot is None:
|
||||
# 兜底场景:如果调用方绕过 initialize/reload,首次读取时仍建立一份快照。
|
||||
self._snapshot = capture_memory_snapshot(self.store)
|
||||
return self._snapshot
|
||||
|
||||
def get_store(self) -> MemoryStore:
|
||||
"""暴露底层 store 给需要直接调用 CRUD 的工具层。"""
|
||||
|
||||
return self.store
|
||||
2
app-instance/backend/beaver/services/skill_service.py
Normal file
2
app-instance/backend/beaver/services/skill_service.py
Normal file
@ -0,0 +1,2 @@
|
||||
"""Application service for skills."""
|
||||
|
||||
10
app-instance/backend/beaver/services/team_service.py
Normal file
10
app-instance/backend/beaver/services/team_service.py
Normal file
@ -0,0 +1,10 @@
|
||||
"""Application service for coordinated team runs."""
|
||||
|
||||
|
||||
class TeamService:
|
||||
"""Placeholder service for multi-agent execution."""
|
||||
|
||||
def run(self, task: str) -> str:
|
||||
"""Return a placeholder summary until real backends are migrated."""
|
||||
return f"team run placeholder: {task}"
|
||||
|
||||
Reference in New Issue
Block a user