feat: 移除backend-old目录中的废弃文件

移除以下文件:
- .dockerignore 和 .gitignore 配置文件
- A2A_Multiagent_change.md 设计文档
- COMMUNICATION.md 通讯信息文档
- Dockerfile 构建配置
- LICENSE 许可证文件

这些文件属于旧版本后端代码,不再需要维护。
This commit is contained in:
2026-05-14 17:19:25 +08:00
parent ebfa242862
commit b59968167e
312 changed files with 0 additions and 53485 deletions

View File

@ -1,2 +0,0 @@
"""Plugin system for Beaver."""

View File

@ -1,2 +0,0 @@
"""Plugin extension hooks."""

View File

@ -1,2 +0,0 @@
"""Plugin loading hooks."""

View File

@ -1,2 +0,0 @@
"""Plugin registry."""

View File

@ -1,262 +0,0 @@
"""Import no-credential Hermes Agent skills into Beaver."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
import json
import re
import shutil
from pathlib import Path
from typing import Any
from beaver.skills.catalog.utils import parse_frontmatter, strip_frontmatter
from beaver.skills.specs import SkillSpec, SkillSpecStore, SkillVersion
from beaver.skills.specs.serialization import canonical_hash, normalize_frontmatter, summarize_skill_content
HERMES_REPO_URL = "https://github.com/NousResearch/hermes-agent"
_CREDENTIAL_PATTERNS = [
re.compile(pattern, re.IGNORECASE)
for pattern in [
r"\bapi[_ -]?key\b",
r"\boauth\b",
r"\bbearer\s+token\b",
r"\baccess[_ -]?token\b",
r"\bclient[_ -]?secret\b",
r"\bsecret\b",
r"\bcredential",
r"\bspotify\b",
r"\bdiscord\b",
r"\bfeishu\b",
r"\bhome\s*assistant\b",
r"\bfal\b",
r"\bopenrouter\b",
r"\bwandb\b",
]
]
@dataclass(slots=True)
class HermesMigrationService:
store: SkillSpecStore
manifest_path: Path | None = None
included_tools: list[dict[str, Any]] = field(default_factory=list)
skipped_tools: list[dict[str, Any]] = field(default_factory=list)
def migrate(
self,
repo_path: str | Path,
*,
include_optional: bool = True,
dry_run: bool = False,
) -> dict[str, Any]:
repo = Path(repo_path)
if not repo.exists():
raise ValueError(f"Hermes repository not found: {repo}")
skill_files = self._discover_skill_files(repo, include_optional=include_optional)
included: list[dict[str, Any]] = []
skipped: list[dict[str, Any]] = []
for skill_file in skill_files:
result = self._migrate_skill(repo, skill_file, dry_run=dry_run)
if result["status"] in {"included", "unchanged"}:
included.append(result)
else:
skipped.append(result)
manifest = {
"source": "hermes-agent",
"repo_url": HERMES_REPO_URL,
"repo_path": str(repo),
"generated_at": datetime.now(timezone.utc).isoformat(),
"dry_run": dry_run,
"included": included,
"skipped": skipped,
"tools": self._tool_manifest(),
}
path = self.manifest_path or (self.store.workspace / "hermes_migration_manifest.json")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
return manifest
def _discover_skill_files(self, repo: Path, *, include_optional: bool) -> list[Path]:
roots = [repo / "skills"]
if include_optional:
roots.append(repo / "optional-skills")
files: list[Path] = []
for root in roots:
if root.exists():
files.extend(sorted(root.glob("**/SKILL.md")))
return files
def _migrate_skill(self, repo: Path, skill_file: Path, *, dry_run: bool) -> dict[str, Any]:
relative = skill_file.relative_to(repo)
content = skill_file.read_text(encoding="utf-8")
frontmatter, body = parse_frontmatter(content)
skill_name = _safe_skill_name(str(frontmatter.get("name") or skill_file.parent.name))
if not skill_name:
return _skip(relative, "unsafe_skill_name")
credential_reason = _credential_reason(content)
if credential_reason:
return _skip(relative, credential_reason, skill_name=skill_name)
normalized = normalize_frontmatter(
{
**frontmatter,
"name": skill_name,
"description": frontmatter.get("description") or skill_name,
}
)
rendered = _render_skill_content(normalized, body)
content_hash = canonical_hash(rendered)
existing = self.store.read_published_skill(skill_name)
existing_spec = self.store.get_skill_spec(skill_name)
if existing is not None and existing.version.content_hash == content_hash:
return {
"status": "unchanged",
"skill_name": skill_name,
"version": existing.version.version,
"path": str(relative),
"reason": "same_content_hash",
}
next_version = self._next_version(skill_name)
if dry_run:
return {
"status": "included",
"skill_name": skill_name,
"version": next_version,
"path": str(relative),
"dry_run": True,
}
now = datetime.now(timezone.utc).isoformat()
skill_version = SkillVersion(
skill_name=skill_name,
version=next_version,
content_hash=content_hash,
summary_hash=canonical_hash(strip_frontmatter(rendered).strip()),
created_at=now,
created_by="hermes_migration",
change_reason=f"Import Hermes skill {relative}",
parent_version=existing.version.version if existing is not None else None,
review_state="published",
frontmatter=normalized,
summary=summarize_skill_content(body),
tool_hints=self.store._extract_tool_hints(normalized),
provenance={
"source": "hermes-agent",
"repo_url": HERMES_REPO_URL,
"repo_path": str(repo),
"relative_path": str(relative),
},
)
self.store.write_skill_version(skill_version, rendered)
self._copy_supporting_files(skill_file.parent, skill_name, next_version)
spec = existing_spec or SkillSpec(
name=skill_name,
display_name=skill_name,
description=str(normalized.get("description") or skill_name),
created_at=now,
updated_at=now,
current_version=next_version,
status="active",
tags=[],
owners=["hermes-agent"],
source_kind="hermes-agent",
lineage=[],
)
spec.current_version = next_version
spec.updated_at = now
spec.status = "active"
spec.source_kind = "hermes-agent"
if "hermes-agent" not in spec.owners:
spec.owners.append("hermes-agent")
self.store.write_skill_spec(spec)
self.store.set_current_version(skill_name, next_version)
published = self.store.read_index("published")
if skill_name not in published:
published.append(skill_name)
self.store.update_index("published", published)
return {
"status": "included",
"skill_name": skill_name,
"version": next_version,
"path": str(relative),
}
def _copy_supporting_files(self, source_dir: Path, skill_name: str, version: str) -> None:
target_root = self.store.root / skill_name / "versions" / version
for source in sorted(source_dir.rglob("*")):
if not source.is_file() or source.name == "SKILL.md" or source.is_symlink():
continue
relative = source.relative_to(source_dir)
if any(part in {"", ".", ".."} for part in relative.parts):
continue
target = target_root / relative
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copyfile(source, target)
def _next_version(self, skill_name: str) -> str:
versions = [item for item in self.store.list_versions(skill_name) if item.startswith("v")]
numbers = [int(item[1:]) for item in versions if item[1:].isdigit()]
return f"v{(max(numbers) if numbers else 0) + 1:04d}"
def _tool_manifest(self) -> dict[str, list[dict[str, Any]]]:
included = self.included_tools or [
{"name": "todo", "reason": "implemented_builtin_no_api"},
{"name": "clarify", "reason": "implemented_builtin_no_api"},
{"name": "delegate", "reason": "implemented_builtin_no_api"},
{"name": "spawn", "reason": "implemented_builtin_no_api"},
{"name": "skills_list", "reason": "implemented_builtin_no_api"},
{"name": "skill_manage", "reason": "implemented_builtin_no_api"},
{"name": "terminal", "reason": "implemented_builtin_no_api"},
{"name": "process", "reason": "implemented_builtin_no_api"},
{"name": "patch", "reason": "implemented_builtin_no_api"},
{"name": "write_file", "reason": "implemented_builtin_no_api"},
{"name": "web_fetch", "reason": "implemented_builtin_no_api"},
{"name": "web_search", "reason": "implemented_builtin_no_api"},
{"name": "execute_code", "reason": "implemented_builtin_no_api"},
]
skipped = self.skipped_tools or [
{"name": "spotify", "reason": "requires_oauth"},
{"name": "discord", "reason": "requires_external_token"},
{"name": "feishu", "reason": "requires_external_token"},
{"name": "home_assistant", "reason": "requires_external_service_credentials"},
{"name": "fal_image_generation", "reason": "requires_api_key"},
{"name": "remote_web_providers", "reason": "requires_api_key_or_oauth"},
]
return {"included": included, "skipped": skipped}
def _credential_reason(content: str) -> str | None:
for pattern in _CREDENTIAL_PATTERNS:
if pattern.search(content):
return "requires_external_credentials"
return None
def _safe_skill_name(value: str) -> str:
cleaned = value.strip().replace(" ", "-")
if not cleaned or cleaned in {".", ".."} or "/" in cleaned or "\\" in cleaned:
return ""
if not re.fullmatch(r"[A-Za-z0-9_.-]+", cleaned):
return ""
return cleaned
def _skip(relative: Path, reason: str, *, skill_name: str | None = None) -> dict[str, Any]:
result = {"status": "skipped", "path": str(relative), "reason": reason}
if skill_name:
result["skill_name"] = skill_name
return result
def _render_skill_content(frontmatter: dict[str, Any], body: str) -> str:
lines = ["---"]
for key, value in normalize_frontmatter(frontmatter).items():
if isinstance(value, list):
lines.append(f"{key}:")
for item in value:
lines.append(f" - {item}")
else:
lines.append(f"{key}: {value}")
lines.extend(["---", "", body.strip()])
return "\n".join(lines).rstrip() + "\n"

View File

@ -1,208 +0,0 @@
"""Import legacy and staged skills into the Beaver SkillSpecStore."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timezone
import io
import json
import re
import zipfile
from pathlib import Path
from typing import Any
from beaver.skills.catalog.utils import parse_frontmatter, strip_frontmatter
from beaver.skills.specs import SkillSpec, SkillSpecStore, SkillVersion
from beaver.skills.specs.serialization import canonical_hash, normalize_frontmatter, summarize_skill_content
@dataclass(slots=True)
class SkillMigrationService:
store: SkillSpecStore
repo_root: Path | None = None
def migrate_all(self) -> dict[str, Any]:
included: list[dict[str, Any]] = []
skipped: list[dict[str, Any]] = []
for path in self._backend_old_skills():
self._migrate_skill_file(path, "backend-old", included, skipped)
for path in self._staged_skills():
self._migrate_skill_file(path, "stevenli-staged", included, skipped)
for path in self._skill_zips():
self._migrate_zip(path, included, skipped)
manifest = {
"generated_at": _now(),
"workspace": str(self.store.workspace),
"included": included,
"skipped": skipped,
}
manifest_path = self.store.workspace / "skill_migration_manifest.json"
manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
return manifest
def _backend_old_skills(self) -> list[Path]:
root = self._repo_root() / "app-instance" / "backend-old" / "nanobot" / "skills"
if not root.exists():
return []
return sorted(root.glob("*/SKILL.md"))
def _staged_skills(self) -> list[Path]:
root = self.store.workspace / "state" / "skill-reviews"
if not root.exists():
return []
return sorted(root.glob("*/staged/*/SKILL.md"))
def _skill_zips(self) -> list[Path]:
root = self.store.workspace / "skills"
if not root.exists():
return []
return sorted(root.glob("*.zip"))
def _repo_root(self) -> Path:
if self.repo_root is not None:
return self.repo_root
return Path(__file__).resolve().parents[4]
def _migrate_skill_file(self, path: Path, source: str, included: list[dict[str, Any]], skipped: list[dict[str, Any]]) -> None:
try:
content = path.read_text(encoding="utf-8")
result = self._publish_content(content, source=source, source_path=str(path))
included.append(result)
except Exception as exc:
skipped.append({"source": source, "source_path": str(path), "reason": str(exc)})
def _migrate_zip(self, path: Path, included: list[dict[str, Any]], skipped: list[dict[str, Any]]) -> None:
try:
with zipfile.ZipFile(io.BytesIO(path.read_bytes()), "r") as archive:
entries = [info for info in archive.infolist() if not info.is_dir()]
skill_entry = _find_skill_entry(entries)
content = archive.read(skill_entry).decode("utf-8", errors="replace")
result = self._publish_content(content, source="stevenli-zip", source_path=str(path))
skill_name = result["skill_name"]
version = result["version"]
top = Path(skill_entry).parts[0] if len(Path(skill_entry).parts) == 2 else ""
for info in entries:
raw = info.filename.replace("\\", "/")
if raw == skill_entry or raw.startswith("/") or "__MACOSX" in Path(raw).parts:
continue
parts = Path(raw).parts
rel_parts = parts[1:] if top and parts and parts[0] == top else parts
if not rel_parts or any(part in {"", ".", ".."} for part in rel_parts):
continue
target = self.store.root / skill_name / "versions" / version / "/".join(rel_parts)
target.parent.mkdir(parents=True, exist_ok=True)
target.write_bytes(archive.read(info))
included.append(result)
except Exception as exc:
skipped.append({"source": "stevenli-zip", "source_path": str(path), "reason": str(exc)})
def _publish_content(self, content: str, *, source: str, source_path: str) -> dict[str, Any]:
frontmatter, body = parse_frontmatter(content)
skill_name = _safe_name(str(frontmatter.get("name") or Path(source_path).parent.name))
if not skill_name:
raise ValueError("unsafe or missing skill name")
normalized = normalize_frontmatter(
{
**frontmatter,
"name": skill_name,
"description": frontmatter.get("description") or skill_name,
}
)
rendered = _render_skill_content(normalized, body)
content_hash = canonical_hash(rendered)
existing = self.store.read_published_skill(skill_name)
if existing is not None and existing.version.content_hash == content_hash:
return {
"status": "unchanged",
"skill_name": skill_name,
"version": existing.version.version,
"source": source,
"source_path": source_path,
}
version_id = self._next_version(skill_name)
now = _now()
skill_version = SkillVersion(
skill_name=skill_name,
version=version_id,
content_hash=content_hash,
summary_hash=canonical_hash(strip_frontmatter(rendered).strip()),
created_at=now,
created_by="migration",
change_reason=f"Import skill from {source}",
parent_version=existing.version.version if existing is not None else None,
review_state="published",
frontmatter=normalized,
summary=summarize_skill_content(body),
tool_hints=self.store._extract_tool_hints(normalized),
provenance={"source": source, "source_path": source_path, "imported_at": now},
)
self.store.write_skill_version(skill_version, rendered)
spec = self.store.get_skill_spec(skill_name) or SkillSpec(
name=skill_name,
display_name=skill_name,
description=str(normalized.get("description") or skill_name),
created_at=now,
updated_at=now,
current_version=version_id,
status="active",
tags=[],
owners=["migration"],
source_kind=source,
lineage=[],
)
spec.current_version = version_id
spec.updated_at = now
spec.status = "active"
spec.source_kind = source
if "migration" not in spec.owners:
spec.owners.append("migration")
self.store.write_skill_spec(spec)
self.store.set_current_version(skill_name, version_id)
published = self.store.read_index("published")
if skill_name not in published:
published.append(skill_name)
self.store.update_index("published", published)
return {"status": "included", "skill_name": skill_name, "version": version_id, "source": source, "source_path": source_path}
def _next_version(self, skill_name: str) -> str:
versions = [item for item in self.store.list_versions(skill_name) if item.startswith("v")]
numbers = [int(item[1:]) for item in versions if item[1:].isdigit()]
return f"v{(max(numbers) if numbers else 0) + 1:04d}"
def _find_skill_entry(entries: list[zipfile.ZipInfo]) -> str:
candidates = []
for info in entries:
raw = info.filename.replace("\\", "/")
parts = Path(raw).parts
if raw.startswith("/") or any(part in {"", ".", ".."} for part in parts):
raise ValueError(f"unsafe archive entry: {info.filename}")
if parts and parts[-1] == "SKILL.md" and len(parts) in (1, 2):
candidates.append(raw)
if not candidates:
raise ValueError("zip has no root SKILL.md")
return candidates[0]
def _safe_name(value: str) -> str:
cleaned = value.strip().replace(" ", "-")
if not cleaned or cleaned in {".", ".."} or "/" in cleaned or "\\" in cleaned:
return ""
return cleaned if re.fullmatch(r"[A-Za-z0-9_.-]+", cleaned) else ""
def _render_skill_content(frontmatter: dict[str, Any], body: str) -> str:
lines = ["---"]
for key, value in normalize_frontmatter(frontmatter).items():
if isinstance(value, list):
lines.append(f"{key}:")
for item in value:
lines.append(f" - {item}")
else:
lines.append(f"{key}: {value}")
lines.extend(["---", "", body.strip()])
return "\n".join(lines).rstrip() + "\n"
def _now() -> str:
return datetime.now(timezone.utc).isoformat()

View File

@ -1,936 +0,0 @@
# Beaver Backend 重构蓝图
## 命名说明
当前项目正式名称已经不是 `nanobot`,而是 `beaver`
这份文档里如果出现 `nanobot/...`,一律表示“当前仓库里还没迁走的历史代码路径 / 现状实现位置”,不代表目标命名。
后续重构目标应统一收敛到:
1. 产品名、项目名、运行时内核名统一按 `beaver` 表达。
2. `nanobot` 只作为迁移期遗留路径存在,最终应逐步退出目录、模块和文档命名。
3. 新增目录、新增模块、新增文档都应优先使用 `beaver` 命名,而不是继续扩散 `nanobot`
## 文档分工
三份核心文档从现在开始按下面的边界维护:
1. `flow.md`
- 只保留树形运行结构
- 只描述“运行时怎么连起来”
- 不再承载蓝图解释、阶段判断、参考项目分析
2. `施工指南.md`
- 保留施工顺序、阶段边界、完成标准、落地步骤
3. `change.md`
- 保留长期蓝图、设计动机、参考项目借鉴边界、架构取舍
这样做的目的很简单:
1. `flow.md` 必须像运行时接线图,而不是混合说明文
2. 施工时看 `施工指南.md`
3. 讨论为什么这样设计时看 `change.md`
## 1. 这次重构到底要解决什么
当前后端已经不是“功能不够”,而是“能力已经长出来了,但结构还停留在早期阶段”。
现在项目里同时存在这些事实:
1. `AgentLoop` 已经承担了太多职责,既管主 agent 对话又管工具、委派、MCP、会话、事件、memory。
2. `web/server.py` 已经变成超大文件FastAPI app factory、chat API、session、文件、skills、cron、A2A、Outlook 都放在一起。
3. `agent_team` 已经接上了 `swarms`,但目前更像“业务层直接借用第三方 runtime”不是“我们自己的多智能体平台”。
4. `skills` 已经有加载、安装、审核,但本质还是 Markdown 说明书,不是可学习、可演化、可评估的能力对象。
5. 项目里已经隐约出现了三个方向,但还没有被统一成一个完整架构:
- `swarms` 提供多智能体架构能力
- `hermes-agent` 提供 skill 生命周期与长期演进思路
- `OpenHarness` 提供模块化的 harness 设计方法
所以这次重构不是简单“整理目录”,而是把项目从“围绕一个 CLI 主 agent 生长出来的系统”升级成“所有 agent 共享同一内核的自有 agent harness 平台”。
### 1.1 当前落地状态2026-05-07
截至当前实现,新 `app-instance/backend/beaver` 已经把主链推进到:
1. Main Agent 自动 Task 化与反馈门控。
- 简单问题直接走 `AgentLoop` 单轮回答。
- 复杂任务自动进入内部 Task。
- 产品面仍只暴露聊天入口,不暴露显式 Task 创建/管理 API。
2. skill 生命周期与学习闭环第一层。
- runtime 记录 `SkillActivationReceipt / RunRecord / SkillEffectRecord`
- Task run 自动验证并失败重试一次。
- learning candidates 默认不在 run 完成时生成。
- 只有“自动验证通过 + 用户满意反馈”才生成成功学习候选。
- `abandon` 写 Failure Memory不生成成功 Skill draft。
3. Agent Team v1 轻量 coordinator。
- 已有 Beaver 自己的 `AgentDescriptor / DelegationEnvelope / ExecutionNode / ExecutionGraph / TeamRunResult`
- `TeamService.run_team(...)` 是内部服务入口,不新增产品级 Task API。
- `LocalAgentRunner` 让 sub-agent 复用主 `AgentLoop.process_direct()` / `submit_direct()`
- 已支持 `sequence / parallel / dag`
- `parallel` 和 DAG 同层节点保持真并发。
- 每个 run 使用独立 memory snapshot避免并发 prompt 串记忆。
- 支持 pinned skill 继承、open skill assembly、per-node provider factory。
- sub-agent run 归入父 Task失败节点归一成 `NodeRunResult`
4. Agent Team 已融入 Task mode 内部执行策略。
- `TaskExecutionPlanner` 先用 LLM JSON 规划 `single / team`
- team node 只声明 `skill_query / required_capabilities`,不声明固定 specialist 人设。
- `TaskSkillResolver` 为每个 generic sub-agent 选择 published skill未命中时生成 ephemeral guidance并作为本次 run 的 pinned guidance 使用。
- team 模式调用 `TeamService.run_team(...)` 产生 sub-agent runs。
- Team 输出只作为主 Agent synthesis run 的内部上下文。
- 用户可见最终回答仍由主 Agent 生成,并继续走验证、反馈和学习门控。
- planner 失败或 graph 非法时降级 `single`
当前仍未落地的部分:
1. Agent Team 不暴露产品级聊天路由或显式 Task API当前作为 Task 内部 sub-agent 执行策略。
2. `moa / hierarchy / heavy / group_chat / forest / maker / router` 仍是策略预留,不是 v1 完整行为。
3. 自动验证目前是 LLM validator不是 replay sandbox。
4. Skill draft synthesis / review / publish 安全链已有基础服务,但还没有做成完整后台学习 pipeline。
5. `/api/agents` 和 agent registry 可作为未来外部 agent/A2A 管理面保留,但不参与 Task sub-agent 选择。
6. 不允许在线直接改 published skill这条约束保持不变。
### 1.2 参考项目核对说明
这版蓝图不是只根据印象在写。`2026-05-06` 我们已经重新核对过下面三个参考项目的公开入口文档:
1. `OpenHarness`
- <https://github.com/HKUDS/OpenHarness>
2. `hermes-agent`
- <https://github.com/NousResearch/hermes-agent>
3. `swarms`
- <https://github.com/kyegomez/swarms>
这一步的目的不是“照着抄目录”,而是把“到底借什么、不借什么”明确写死,避免后续施工时又把第三方项目的实现细节直接揉回 Beaver。
## 2. 我是怎么想的
我的核心判断是我们不能继续把第三方库、业务流程、执行控制、UI/API 接口揉在一起,而是应该先定义我们自己的稳定边界,再让第三方能力挂进来。
换句话说,目标不是“把仓库改得更像 swarms / hermes / OpenHarness”而是
1.`swarms` 的强项来解决“团队编排”。
2.`hermes-agent` 的强项来解决“skills 怎么创建、维护、学习、沉淀”。
3.`OpenHarness` 的强项来解决“工程边界、模块职责、可维护性”。
4. 最终收口成我们自己的抽象和目录,而不是长期让第三方结构反向塑造我们。
这里把三者的借鉴边界再说得更具体一点:
1. `OpenHarness`
- 借它的 harness 分层方式:`engine / tools / skills / permissions / memory / coordinator / prompts / config`
- 借它“一条统一 loop + 明确 tool registry / permission / hook 边界”的工程组织方式
- 不直接照搬它的 CLI/TUI、commands、plugin 生态,也不要求 Beaver 长成它的目录镜像
2. `hermes-agent`
- 借它的 memory / session / session_search / skills 运行时关系
- 借它对 FTS5 transcript 搜索、长期记忆、显式 skill 注入、session lineage 的处理方向
- 不把“自动学习闭环、完整渠道网关、全部终端后端、Honcho 用户建模”当成当前阶段必须同步迁入的范围
3. `swarms`
- 借它已经验证过的多智能体执行形态,例如 sequential / hierarchy / rearrange / router 这类 orchestration 结构
- 借它作为 team execution backend 的角色,而不是借它来定义 Beaver 的主 runtime、session、tool、provider 契约
- 不再允许 Beaver 上层直接感知 `third_party/swarms``SwarmRouter` 参数细节或 import 副作用
这意味着后续所有设计都应遵守四条原则:
### 2.1 我们要有自己的抽象
不能让业务代码直接依赖:
- `third_party/swarms` 的导入路径
- `SwarmRouter` 的参数细节
- 某个第三方 skill 文件格式
- 某个第三方 runtime 的副作用
我们应该先定义自己的核心对象,例如:
- `AgentDescriptor`
- `SkillSpec`
- `SkillVersion`
- `TeamSpec`
- `ExecutionPlan`
- `ProcedureRecord`
- `RunRecord`
- `BridgeResult`
第三方库只能作为 adapter / backend 存在。
### 2.2 所有 agent 共享同一套运行内核
后面不应该再保留“CLI 单 agent”和“其他 agent 另一套执行方式”这种概念分叉。
正确做法应该是:
1. 所有 agent 都复用同一个 `AgentLoop` / engine。
2. 主 agent、subagent、team member、A2A local specialist 都只是不同的运行配置和上下文。
3. tools、skills、memory、permissions、MCP、delegation 都在同一套内核里装载。
4. CLI 只是一个 interface作用是把用户输入送进内核而不是代表一种单独的 agent 类型。
这样做的意义是:
1. 所有 agent 的能力边界一致。
2. 不会再出现“这个能力只在 CLI 主 agent 可用,子 agent 不一致”的问题。
3. agent 的差异只存在于 profile / policy / prompt / runtime context而不是存在于不同执行栈里。
### 2.3 Harness 和业务要分开
当前很多逻辑混在一起:既有“平台级能力”,也有“具体产品接入”。
后面应该分成两层:
1. Harness 层
- tool use
- skills
- memory
- delegation
- orchestration
- governance
2. Product / Interface 层
- web API
- gateway
- channel adapters
- Outlook / WhatsApp / 外部服务接入
这样平台能力才能稳定,接入层才能随产品变化而变化。
### 2.4 多智能体是平台能力,不是工具技巧
现在 `spawn_agent_team` 已经存在,但在结构上还像“一个高级工具”。
后面应该把 multi-agent 当成正式 runtime 能力:
- 有 plan 层
- 有 strategy 层
- 有 execution backend 层
- 有 result normalization 层
- 有 memory / procedure reuse 层
- 有 governance / safety / skill constraints
这里要特别说明 `2.4``2.5` 的关系:
1. multi-agent 不是独立于 skills 的第二套指导系统。
2. 我们仍然保留之前的群组讨论机制,也就是“探索式协作 + 流程化执行”两种能力都保留。
3. 但无论是探索式 group discussion还是流程化 sequential / rearrange / hierarchy都必须受 skills 指引和约束。
4. 也就是说skills 决定“应该如何思考、遵守什么边界、优先采用什么方法”,而 multi-agent 负责“由几个人、以什么结构去执行”。
所以后续正确关系应是:
`skills -> 约束与方法指导`
`multi-agent -> 在 skills 约束下进行探索、讨论、流程化执行`
### 2.5 skills 必须变成生命周期系统
现在的 skills 更像可读文档包,适合“手工维护”,不适合“自动学习”。
如果以后要做到自动创建、自动修订、自动推荐、自动淘汰skills 必须具备:
- 结构化元数据
- 版本号
- 来源与 lineage
- 审核状态
- 效果统计
- 与 procedure 的映射关系
- 可回滚、可禁用、可发布
并且这里的 `skills` 不应只服务于“工具使用技巧”,而应成为整个 agent 系统的统一指引层,包括:
1. 主 agent 如何规划和执行
2. subagent / team member 如何行动
3. memory 如何参与判断
4. procedure reuse 如何被触发和约束
5. multi-agent 讨论时允许采用哪些方法、角色分工和输出习惯
换句话说:
1. memory / procedure reuse 不是独立于 skills 的平行系统。
2. 但 memory 的实现标准要以 `hermes-agent` 为准,而不是继续沿用当前偏自由发挥的记忆模型。
3. skills 提供全局行为指引memory 只保存跨会话仍然有价值的稳定事实session_search 负责找回历史细节procedure 只作为可选优化层。
这里要明确四者分工:
1. `skills`
- 指导“怎么做”
- 约束工具使用、讨论方式、流程化执行方式
2. `memory`
- 保存 durable facts
- 例如用户偏好、环境事实、项目约定、工具 quirks
3. `session_search`
- 检索历史会话细节
- 不把大量过程细节直接塞进 memory
4. `procedure`
- 作为 coordinator 内部的复用优化
- 不是主 memory 契约,也不是主要 prompt 注入来源
## 3. 现有项目现在是咋样的
### 3.1 当前的主结构
从代码上看,`app-instance/backend` 当前大致是这几块。
注意:下面这些路径仍写作 `nanobot/...`,是因为这里描述的是“现状代码位置”,不是目标命名。
1. 启动与装配
- `nanobot/cli/commands.py`
- `nanobot/__main__.py`
2. agent 运行时
- `nanobot/agent/loop.py`
- `nanobot/agent/context.py`
- `nanobot/agent/tools/*`
- `nanobot/session/*`
- `nanobot/providers/*`
3. 多 agent / 委派
- `nanobot/agent/delegation.py`
- `nanobot/agent_team/*`
- `nanobot/a2a/*`
4. Web / Gateway / Channels
- `nanobot/web/server.py`
- `nanobot/channels/*`
- `bridge/`
5. 技能与插件
- `nanobot/skills/*`
- `nanobot/agent/skills.py`
- `nanobot/agent/plugins.py`
6. 外部运行时耦合点
- 当前主要是 vendored `swarms`
### 3.2 当前已经有的优点
这套代码不是没基础,相反已经有几个很有价值的雏形:
1. 已经有 `AgentRegistry``DelegationManager``agent_team`,说明“统一委派层”思路已经出现。
2. 已经有 `ProcedureMemory``RunMemory`,说明“从执行中学习”的基础数据层已经出现。
3. 已经有 `skills` 的加载、安装、审核,说明“受控扩展机制”已经存在。
4. 已经有 `SwarmsBridge``SwarmsPolicy``SwarmsRunPlanner`,说明多智能体桥接已经不是空白。
所以这次重构不是推倒重来,而是把这些散落的雏形收敛成一个完整架构。
### 3.3 当前最主要的问题
#### 问题一:装配逻辑散落
同一个后端能力,在 CLI、Web、Gateway 中经常重复装配,甚至行为已经开始漂移。
这会导致:
1. 同样的配置在不同入口行为不同。
2. 改一个入口容易漏另一个入口。
3. 测试覆盖变难。
#### 问题二:`AgentLoop` 太重,但又没有成为唯一内核
`AgentLoop` 已经不是纯 loop而是“半个 runtime 内核”。
这会导致:
1. 主 agent 与其他 agent 的边界不清。
2. tool、memory、delegation、session、events 相互缠绕。
3. 很多能力只能靠继续往 `AgentLoop` 里塞。
4. 同时又没有真正做到“所有 agent 都统一复用它”。
#### 问题三:`swarms` 接入边界不干净,而且 `third_party` 目录本身会持续恶化维护成本
当前 `agent_team` 虽然有 bridge但仍然直接依赖
1. `sys.path` 注入 vendored `swarms`
2. 顶层 `swarms` 包导入副作用
3. `SwarmRouter` 的参数细节
4. `AutoSwarmBuilder` 自己的 LLM 栈
这意味着现在不是“我们调度 swarms”而是“我们的平台有一部分被 swarms runtime 反向定义了”。
另外,`third_party/` 这种目录在这个项目里不应该长期存在。它会带来两个问题:
1. 仓库边界不清,到底哪些代码是我们的,哪些不是,很难维护。
2. 一旦改动第三方源码,升级、回滚、排障都会变得更脆弱。
#### 问题四skills 还是静态文档包
现在的 skill 系统适合:
- 展示
- 人工安装
- prompt 注入
但不适合:
- 自动学习
- 自动合并
- 自动评估
- 版本回滚
- 基于效果做选择
#### 问题五:接口层和核心层耦合过深
`web/server.py` 过大说明一个事实:
平台内核与外部 API、外部接入、外部服务没有完成分层。
## 4. 后面应该怎么改
## 4.1 先把系统改成 OpenHarness 风格的能力分组
这里我建议明确参考 OpenHarness 那种“按能力分组、核心目录更扁平”的结构,而不是继续按历史演化路径堆目录。
核心思路是:
1.`engine` 作为唯一运行内核。
2.`coordinator` 负责委派和多 agent 编排。
3.`tools``skills``memory``permissions` 作为独立能力层。
4.`interfaces` 只放 CLI / Web / Gateway / Channels 这类入口。
5.`integrations` 放外部协议和外部系统适配。
这样拆完之后,模块关系应变成:
`interfaces -> engine/coordinator/tools/skills/memory -> foundation`
而不是像现在这样互相横穿。
## 4.2 彻底去掉 `third_party/`,把 `swarms` 改造成可替换 backend
### 旧实现状态
`agent_team` 曾经接通:
- `GroupChat`
- `SequentialWorkflow`
- `ConcurrentWorkflow`
- `AgentRearrange`
- `MixtureOfAgents`
- `HierarchicalSwarm`
但这些能力还不是 Beaver 的正式能力集合,而是“旧 bridge 恰好能跑通的一部分 swarms 类型”。
更重要的是,当前它们依赖 `third_party/swarms` 这个 vendored 目录,这是后续必须去掉的。
### 当前 Beaver 状态
新后端已经先落地了不依赖 `third_party/swarms` 的 Agent Team v1
1. 自有核心模型:
- `AgentDescriptor`
- `DelegationEnvelope`
- `ExecutionNode`
- `ExecutionGraph`
- `NodeRunResult`
- `TeamRunResult`
2. 内部服务入口:
- `TeamService.run_team(...)`
3. 本地 delegated runner
- `LocalAgentRunner`
- sub-agent 复用主 `AgentLoop.process_direct()` / `submit_direct()`
4. 已实现策略:
- `sequence`
- `parallel`
- `dag`
5. 已固定的安全语义:
- parent Task 必须存在且 session 匹配
- sub-agent run_ids 回填父 Task
- team/sub-agent 默认只写 receipts/effects不生成 learning candidates
- learning candidates 仍只由 Task feedback gate 触发
- 节点级异常归一成 `NodeRunResult`
- summary 只聚合成功输出并列出失败节点
### 目标状态
后续应该继续沿用我们自己的团队执行抽象:
```text
TeamSpec
-> TeamPlanner
-> ExecutionPlan
-> StrategyBackend
-> NormalizedResult
```
然后:
1. `SwarmsBackend` 如果以后存在,也只能是 `StrategyBackend` 的一个实现。
2. 平台对外暴露的是自己的策略名和能力矩阵。
3. `swarms` 只提供可选执行或策略参考,不再负责定义平台边界。
4. 仓库内不再保留 `third_party/`
5. 高级策略可以先编译成 Beaver `ExecutionGraph` 或 step loop而不是直接暴露 swarms runtime。
### 具体改法
1. 保留当前 `coordinator/models.py / local.py / execution/scheduler.py` 作为 v1 core。
2. 在平台层继续扩展正式支持的 strategy。
- 已实现:`sequence / parallel / dag`
- 预留:`moa / hierarchy / heavy / group_chat / forest / maker / router`
3. 高级 strategy preset 先转成 `ExecutionGraph` 或 step loop。
4. 如果后续接外部 swarms单独放进 `coordinator/backends/swarms/`,并统一输入输出为 Beaver models。
### 结果
改完之后:
1. `third_party/` 目录消失。
2. 上层不再知道 `third_party/swarms` 这个路径。
3. 对上层透明的是 Beaver 自有 team model 和 `TeamService`,不是 vendored 源码目录。
## 4.3 把 `skills` 从静态文档升级成能力生命周期系统
### 当前状态
现在 skill 基本等于:
- 一个目录
- 一个 `SKILL.md`
- 一点 frontmatter
- 一点审核流程
### 目标状态
后续 skill 至少要分成三类对象:
1. `SkillDraft`
- 自动生成或人工创建
- 还没发布
2. `SkillVersion`
- 某个稳定版本
- 可启用/禁用/回滚
3. `SkillRuntimeView`
- 当前对模型暴露的生效版本
同时 skill 应该带这些元信息:
- `id`
- `name`
- `version`
- `summary`
- `usage_rules`
- `inputs`
- `outputs`
- `dependencies`
- `source`
- `derived_from_procedure`
- `review_status`
- `metrics`
### 自动学习建议
不要直接让 agent 在线改 live skills。
正确链路应该是:
`run result -> procedure candidate -> skill draft -> review -> publish -> runtime use`
这比“自动改 `SKILL.md`”安全得多,也更适合生产环境。
### 结果
改完之后skills 不再只是 prompt 资源,而是平台知识层的一等对象。
## 4.4 以 `hermes-agent` 的 memory 模型为基线重做 memory 层
这里要明确:新的 memory 设计不再以当前 `ProcedureMemory` 为中心,而是以 `hermes-agent` 的 memory 模型为准。
### 主 memory 契约
新的主 memory 契约应是:
1. 一个统一的 `memory` tool
2. 三个核心动作:
- `add`
- `replace`
- `remove`
3. 两个目标存储:
- `memory`agent 的环境事实、项目约定、工具经验
- `user`:用户画像、偏好、习惯、纠正记录
它的行为应对齐 Hermes
1. `add`
- 追加新条目
- 精确重复时跳过
- 超限时返回当前条目和占用情况
2. `replace`
-`old_text` 的短语义片段匹配条目并整体替换
- 多条匹配时要求更精确的 `old_text`
3. `remove`
- 也是通过 `old_text` 的语义片段删除
- 多条匹配时同样要求更精确匹配
这里要采用“子串匹配”而不是 UUID因为这更符合 LLM 的操作习惯。
### 写入安全与并发安全
新的 memory 层应保留 Hermes 这几个关键约束:
1. 写入前扫描注入/渗透模式
2. 在锁内重新从磁盘加载目标文件
3. 做重复检测和字符上限检测
4. 通过临时文件 + `os.replace()` 做原子写入
也就是说,并发安全的关键不是“先读后写”,而是:
`scan -> lock -> reload -> validate -> atomic write`
### 冻结快照模式
新的 memory 层必须采用 frozen snapshot而不是“每次 memory 写入都改 system prompt”。
规则是:
1. 会话开始时,从磁盘加载 `memory``user`
2. 立刻冻结成 system prompt snapshot
3. 会话中写入 memory 时,只更新磁盘上的 live state
4. 当前会话里的 system prompt 保持不变
5. 下一个会话开始时,再重新加载最新 memory
### session_search 取代“把所有过程细节塞进 memory”
大量过程细节不应继续塞进 `memory`
因此新后端应该明确区分:
1. `memory`
- 保存小而精的、跨会话稳定有效的事实
2. `session_search`
- 检索历史会话
- 支持“无 query 浏览最近会话”和“有 query 的全文搜索 + 摘要”
这个能力后续应在 Beaver 中落成:
- `beaver/memory/curated/*`
- `beaver/memory/search/*`
- `beaver/tools/builtins/memory.py`
- `beaver/tools/builtins/session_search.py`
### `ProcedureMemory` 的新定位
这不表示 `ProcedureMemory` 没价值,而是它的地位要下降:
1. `ProcedureMemory` 不再是主 memory 契约
2. 它不应该直接承担“跨会话记忆”职责
3. 它更适合作为 coordinator 内部的流程复用与路由优化层
新的优先级应是:
1. 用户偏好、纠正、环境事实 -> `memory`
2. 历史会话细节 -> `session_search`
3. 稳定方法论和工作法 -> `skills`
4. 团队/流程复用优化 -> `ProcedureMemory`
## 4.5 CLI 不再代表单 agent 模式,只保留为薄入口
当前入口层太厚,后续应该改成:
1. CLI 只做参数解析与 runtime 启动
2. Web 只做 API 与 request/response 映射
3. Gateway 只做渠道接入与消息转发
所有核心能力都由统一的 application services 提供,例如:
- `ChatApplicationService`
- `DelegationApplicationService`
- `TeamRunApplicationService`
- `SkillApplicationService`
- `MemoryApplicationService`
同时要明确一条原则:
CLI 不是“单 agent 专用模式”。
它只是这些 interface 之一:
- CLI
- Web
- Gateway
- Channel
无论从哪个入口进来,最终都进入同一套 `AgentLoop` / engine。
这样就不会再出现“CLI 一套 agent其他入口另一套 agent”的问题。
## 5. 具体改动后会是什么样
## 5.1 所有 agent 共用同一套 engine
### 现在
`CLI/Web/Gateway -> 各自装配一套 AgentLoop 或相关依赖`
### 之后
`CLI/Web/Gateway/Channel -> AgentEntryService -> AgentLoop(engine) -> tools/skills/memory/permissions/delegation`
结果是:
1. 主 agent、subagent、team member 复用同一套 engine。
2. 装载逻辑只在 engine 内统一处理一次。
3. 不再保留“CLI 单 agent 概念”。
4. 测试可以直接测 engine 和 service而不是分别测入口分支。
## 5.2 多 agent 场景
### 现在
`TeamService.run_team -> TeamGraphScheduler -> LocalAgentRunner -> AgentLoop.process_direct / submit_direct`
Task mode 内部已经变成:
`AgentService._run_task_mode -> TaskExecutionPlanner -> optional TeamService.run_team -> 主 Agent synthesis run -> ValidationService`
### 之后
`TeamService`
`-> strategy preset`
`-> ExecutionGraph`
`-> TeamGraphScheduler`
`-> LocalAgentRunner / optional StrategyBackend`
`-> NormalizedTeamResult`
结果是:
1. 团队能力不再绑定某个第三方 runtime 结构。
2. v1 已经支持 `sequence / parallel / dag`
3. 可以逐步增加高级 preset 或第二种 backend而不推翻平台层。
3. `swarms` 只是其中一个可插拔执行器。
## 5.3 skill 场景
### 现在
`SkillsLoader -> 读 SKILL.md -> 摘要注入 / 手动审核安装`
### 之后
`SkillCatalog`
`-> SkillDraftStore`
`-> SkillReviewService`
`-> SkillPublisher`
`-> SkillRuntimeResolver`
结果是:
1. skill 可以有版本。
2. skill 可以从 procedure 生成。
3. skill 可以审核和回滚。
4. skill 可以做效果分析和推荐。
## 5.4 运行学习场景
### 现在
`Run details 混在 session / memory / procedure 中`
### 之后
`Run transcript`
`-> session_search index`
`Durable fact`
`-> memory(add/replace/remove)`
`Stable method / workaround / reusable workflow`
`-> SkillCandidateGenerator`
`-> SkillDraft`
`-> Review`
`-> Publish`
`Repeated execution pattern`
`-> optional ProcedureMemory`
结果是:
1. durable facts、历史细节、稳定方法三类信息终于分层。
2. 自动学习不会把临时过程污染到主 memory。
3. skills 仍是最高层指导系统,而 memory 变成受控 CRUD 系统。
## 6. 分阶段落地建议
这次重构不应该一次性推翻,建议分四期做。
### 第一期:边界清理
目标:
1. 把入口装配统一掉
2.`web/server.py` 开始拆分
3. 先落地 Beaver 自有 Agent Team v1 core避免继续依赖 vendored swarms
交付物:
- 统一 app factory / service wiring
- 初步拆分 web routes
- `coordinator/models.py / local.py / execution/scheduler.py`
### 第二期:平台抽象固化
目标:
1. 定义 team / skill / memory / session_search 的正式模型
2. 让上层只依赖平台模型
交付物:
- `AgentDescriptor / ExecutionGraph / TeamRunResult`
- `SkillSpec`
- `ExecutionPlan`
- `MemoryEntry`
- `MemorySnapshot`
- `SessionSearchResult`
- `SkillDraft`
- `SkillVersion`
### 第三期skills 生命周期
目标:
1. 从“文档技能”升级到“版本化能力”
2. 打通“稳定方法 -> SkillDraft”
3. 按 Hermes 基线完成 memory CRUD、frozen snapshot、session_search
交付物:
- skill catalog
- review/publish flow
- runtime resolver
- memory tool
- session search tool
### 第四期:高级多智能体能力
目标:
1. 放开更多正式支持的 strategy
2. 评估 `GraphWorkflow``HeavySwarm`
3. 增加 fallback / retry / policy routing
交付物:
- 完整 strategy registry
- 多 backend 能力矩阵
- team execution fallback
## 7. 重构后的推荐目录
下面这个目录我已经按你说的方向收紧了:
1. 不保留 `third_party/`
2. 不保留“CLI 单 agent”这类结构暗示
3. 尽量参考 OpenHarness 那种按能力分组、观感更规整的布局
4. 每个目录后面都加中文说明
```text
app-instance/backend/
├── change.md # 这份重构蓝图
├── README.md # 后端总说明
├── workflow.md # 运行链路说明
├── docs/ # 架构文档和迁移文档
│ ├── architecture/ # 核心架构说明
│ └── migration/ # 分阶段迁移计划
├── beaver/
│ ├── foundation/ # 最底层公共设施:配置、模型、事件、错误、工具函数
│ │ ├── config/ # 配置定义与加载
│ │ ├── models/ # 全局共享数据模型
│ │ ├── events/ # 统一事件模型与事件派发
│ │ ├── errors/ # 统一错误类型
│ │ └── utils/ # 通用工具函数
│ ├── engine/ # 统一 agent 内核,所有 agent 都复用这里
│ │ ├── loop.py # AgentLoop 主循环与执行入口
│ │ ├── loader.py # tools、skills、memory、permissions 的统一装载
│ │ ├── context/ # 上下文拼装
│ │ ├── session/ # 会话状态与持久化
│ │ ├── providers/ # LLM provider 适配
│ │ └── runtime/ # 运行时辅助对象与执行上下文
│ ├── tools/ # 工具系统
│ │ ├── registry/ # 工具注册与发现
│ │ ├── builtins/ # 内置工具
│ │ ├── mcp/ # MCP 工具适配
│ │ └── policies/ # 工具权限与调用约束
│ ├── skills/ # 技能系统
│ │ ├── builtin/ # 内置技能内容
│ │ ├── catalog/ # 技能目录、索引与查询
│ │ ├── drafts/ # 自动生成或待审核的 skill draft
│ │ ├── reviews/ # 技能审核流
│ │ ├── publisher/ # 技能发布与版本切换
│ │ └── resolver/ # 运行时技能解析与注入
│ ├── memory/ # 记忆与经验沉淀系统
│ │ ├── curated/ # Hermes 风格的 MEMORY / USER 持久记忆
│ │ ├── search/ # session_search 与历史会话检索
│ │ ├── runs/ # 单次执行记录
│ │ ├── procedures/ # 可选的流程复用优化层
│ │ └── stores/ # 底层存储与原子写实现
│ ├── permissions/ # 权限、沙箱、治理规则
│ │ ├── policies/ # 权限策略
│ │ ├── guards/ # 执行前检查
│ │ └── profiles/ # 不同 agent 运行权限画像
│ ├── coordinator/ # 多 agent 协调层,参考 OpenHarness 的 coordinator 风格
│ │ ├── models.py # AgentDescriptor / ExecutionGraph / TeamRunResult
│ │ ├── local.py # LocalAgentRunner复用主 AgentLoop
│ │ ├── execution/ # sequence / parallel / dag 调度与聚合
│ │ ├── backends/ # 后续可替换多 agent backend
│ │ └── team/ # team 级模型 re-export / 后续高级编排对象
│ ├── services/ # application services对外提供统一能力入口
│ │ ├── agent_service.py # 统一 agent 运行入口
│ │ ├── team_service.py # 多 agent 执行入口
│ │ ├── skill_service.py # 技能管理入口
│ │ ├── memory_service.py # memory 查询与写入入口
│ │ └── admin_service.py # 平台管理入口
│ ├── interfaces/ # 薄入口层,不承载核心业务
│ │ ├── cli/ # CLI 入口,只负责把请求送进 services/engine
│ │ ├── web/ # FastAPI 接口层
│ │ │ ├── app.py # Web app factory
│ │ │ ├── routes/ # 路由拆分
│ │ │ ├── schemas/ # Web 请求/响应模型
│ │ │ └── deps.py # Web 依赖装配
│ │ ├── gateway/ # 常驻 worker / gateway 入口
│ │ └── channels/ # Telegram/Slack/Email 等渠道入口
│ ├── integrations/ # 外部系统与协议集成
│ │ ├── a2a/ # A2A 协议与 client
│ │ ├── mcp/ # MCP 连接与管理
│ │ ├── outlook/ # Outlook 集成
│ │ ├── whatsapp/ # WhatsApp bridge 适配
│ │ └── providers/ # 外部 provider 特定集成
│ ├── plugins/ # 插件系统
│ │ ├── loader.py # 插件发现与装载
│ │ ├── registry.py # 插件注册表
│ │ └── hooks.py # 插件 hooks
│ └── templates/ # 默认模板、system prompt 模板、内置文本资源
├── tests/ # 测试
│ ├── unit/ # 单元测试
│ ├── integration/ # 集成测试
│ ├── e2e/ # 端到端测试
│ └── fixtures/ # 测试数据与夹具
└── bridge/ # 独立 Node/bridge 代码,作为外部桥接层保留
```
## 8. 最终结论
这次重构的本质不是“把代码拆小一点”,而是完成三件事:
1. 把当前项目从“围绕 `AgentLoop` 生长的单体系统”升级成“所有 agent 共用一个 engine 的可维护 harness 平台”。
2.`swarms` 从“放在 `third_party/` 里的深耦合运行时”降级成“可替换的多智能体 backend”。
3.`skills` 从“静态 Markdown 包”升级成“可学习、可审核、可发布、可回滚的能力系统”。
如果这三件事做成了,后面再扩多智能体架构、自动学习、插件生态、外部接入,代码就不会继续失控。
---
## 9. 最新落地状态Task Team 后三件套
本轮已经把 Task Team 融合后的三个缺口推进到 v1 可用状态:
1. **Task Sub-agent Skill Resolver**
- 新增 `beaver/tasks/skill_resolver.py`
- sub-agent 是临时 generic worker不承载固定角色人设。
- `TaskExecutionPlanner` 的 team node 输出 `skill_query / required_capabilities / expected_output`
- `TaskSkillResolver` 从 published skill catalog 中选择合适 skill并写入 node pinned skills。
- 如果没有命中 published skill会创建 ephemeral guidance并作为本次 sub-agent 的 pinned skill context 使用。
- ephemeral guidance 不写入 draft store不自动 approve/publish不进入 runtime catalog。
- agent registry / target resolver 不参与 Task sub-agent strategy可作为未来外部 agent/A2A 管理面保留。
2. **Task Team Process Projection**
- Task attempt 隐藏事件增加 `skill_queries / selected_skill_names / ephemeral_guidance_ids / skill_resolution_report / node_results / task_synthesis_completed`
- 新增 `GET /api/sessions/{session_id}/process`
- 前端 `ChatWorkbench` 已接入 `ProcessLane` 和移动端 `Process` tab。
- 展示规划、skill selection、ephemeral guidance、team node、main synthesis、validation/retry不把 team summary 直接当最终回答。
3. **Learning Pipeline 闭环**
- 新增 `SkillLearningPipelineService`
- Web API 覆盖 candidates、drafts、submit、approve、reject、publish、disable、rollback。
- `/skills` 页面增加 Published / Candidates / Drafts tabs。
- publish 仍要求 approved draftrejected draft 不可 publishdraft 不进入 runtime catalog。
验证状态:
- 后端:`76 passed`
- 前端:`npm run typecheck` 通过,`npm test` 通过,`npm run lint` 通过但仍有既有 warnings。

View File

@ -1,8 +0,0 @@
# Phase 1
第一阶段先完成结构搭建与边界清理:
1. 创建新的 `beaver` 包结构。
2. 保留旧实现于 `backend-old/`
3. 后续逐步把旧能力迁入新结构。

View File

@ -1,905 +0,0 @@
# Beaver Backend Flow
这份文档只保留**树形运行结构**。
- 原理、参考项目边界、长期蓝图:看 `change.md`
- 施工顺序、阶段目标、完成标准:看 `施工指南.md`
---
## 1. 总入口
```text
用户输入(用户在不同入口发来一句话或一个任务)
├─ CLI命令行入口
├─ Web网页前端入口
├─ Gateway消息通道入口比如以后接 Slack / Telegram
└─ future channels未来扩展入口
└─ AgentService统一服务层所有入口都先汇总到这里
├─ Intent Agent / MainAgentRouter第一层意图判断simple chat / continue task / create task / close / abandon
├─ load intent-agent-router skill内部 skill 指引:只做路由,不回答用户,不使用工具)
├─ classify(...)LLM 语义判断)
├─ session hidden event: intent_agent_decision_snapshotted记录选择 simple_chat / create_task / continue_task 等)
├─ create_loop()(创建 AgentLoop 运行核心)
├─ start()(启动后台运行模式)
├─ submit_direct()(把任务提交到运行队列)
├─ process_direct()(直接处理一次任务,不走队列)
├─ submit_feedback()(记录聊天反馈并驱动内部 Task 状态)
├─ stop()(停止接收新任务,并等待队列收尾)
├─ shutdown()(停止运行并释放资源)
└─ close()(关闭已经创建的 runtime
```
---
## 2. Boot / Loader
```text
AgentService.create_loop()(服务层创建运行核心)
└─ AgentLoop(profile, loader)Agent 主循环:真正跑任务的核心对象)
└─ AgentLoop.boot()(启动前装配依赖)
└─ EngineLoader.load()(加载所有运行时模块)
├─ SessionManager会话管理保存聊天记录和隐藏事件
├─ MemoryStore长期记忆存储真正落盘的 curated memory
├─ MemoryService记忆服务运行时访问 memory 的唯一入口)
├─ RunMemoryStore运行记录存储保存每次 run 的结果)
├─ SkillLearningStore技能学习存储保存表现统计和学习候选
├─ ToolRegistry工具注册表登记系统有哪些工具
├─ ToolAssembler工具选择器决定本轮暴露哪些工具
├─ ToolExecutor工具执行器真正调用工具
├─ SkillsLoader技能目录加载器只加载可用技能
├─ SkillAssembler技能选择器决定本轮激活哪些 skill
├─ SkillSpecStore技能生命周期存储保存版本、草稿、审核
├─ DraftService草稿服务创建 skill draft
├─ ReviewService审核服务approve / reject draft
├─ SkillPublisher发布服务publish / disable / rollback
├─ EvidenceSelector证据选择器为学习闭环挑选历史证据
├─ SkillDraftSynthesizer草稿合成器让 LLM 生成 skill draft
├─ SkillLearningService技能学习服务生成学习候选和草稿
├─ TaskService内部 Task 服务:自动 Task 化、状态、事件、反馈)
├─ TaskExecutionPlannerTask 执行规划器:决定 single / team
├─ ValidationService结果验证服务Task run 完成后的自动验证)
└─ ContextBuilder上下文构建器拼 system prompt 和 messages
```
---
## 3. Main Agent Routing / Internal Task
```text
AgentService.process_direct / submit_direct聊天入口统一进入服务层
├─ resolve session_id复用请求 session或生成新 session
├─ task_service.get_latest_open_task(session_id)(查找同会话未关闭 Task
├─ MainAgentRouter.classify(message, active_task, recent_messages, intent-agent-router skill)Intent Agent 语义分类)
│ ├─ Intent Agent 只返回 JSON 路由结果,不直接回答用户
│ ├─ Intent Agent 没有 tools凡是需要工具、实时/外部数据、文件、执行、验证的请求都应进入 Task
│ ├─ session hidden event: intent_agent_decision_snapshotted调试日志展示 choice / reason / short_title
│ ├─ simple简单问题
│ │ └─ runner(message, include_skill_assembly=False, include_tools=False)(不创建 Task不跑 skills/tools
│ │
│ ├─ continue_task继续当前 Task
│ │ └─ reuse active Task只要话题没有完全无关就继续当前 open Task
│ │
│ ├─ new_task明确开启新任务
│ │ └─ TaskService.create_task(...)(内部创建 Task并保存 short_title
│ │
│ ├─ close_task / abandon_task用户明确结束或放弃
│ │ └─ TaskService.close_task / abandon_task关闭当前 Task
│ │
│ └─ task execution
│ └─ AgentService._run_task_mode(...)(进入 Task 模式执行)
```
```text
TaskService内部 Task 状态机)
├─ TaskRecord
│ ├─ task_id
│ ├─ session_id
│ ├─ goal / description / constraints
│ ├─ metadata.short_title5-15 字左右的短标题,用于前端当前任务标识)
│ ├─ status
│ │ ├─ open
│ │ ├─ running
│ │ ├─ validating
│ │ ├─ awaiting_feedback
│ │ ├─ needs_revision
│ │ ├─ closed
│ │ └─ abandoned
│ ├─ run_ids
│ ├─ skill_names
│ ├─ validation_result
│ └─ feedback
└─ TaskEvent
├─ created
├─ run_started
├─ run_completed
├─ validated
├─ feedback_satisfied
├─ feedback_revise
└─ feedback_abandon
```
```text
Task Mode Execution复杂任务执行
├─ attempt 1
│ ├─ task_service.start_run(...)
│ ├─ TaskExecutionPlanner.plan(...)LLM 规划 single / team
│ ├─ session hidden event: task_execution_planned
│ ├─ if plan.mode == team
│ │ ├─ TeamService.run_team(parent_task_id=task_id, parent_session_id=session_id)
│ │ ├─ sub-agent runs -> parent Task run_ids
│ │ ├─ session hidden event: task_team_run_completed / task_team_run_failed
│ │ └─ team summary + node results -> 主 Agent synthesis execution_context
│ ├─ AgentLoop.process_direct / submit_direct(..., task_id, task_mode=True, attempt_index=1)
│ ├─ ValidationService.validate_task_result(..., team_summaries=...)
│ ├─ TaskService.record_validation(...)
│ ├─ RunMemoryStore.update_run_record(validation_result=...)
│ └─ session hidden event: task_validation_snapshotted
├─ if validation accepted
│ └─ return result with task_id / task_status / validation_result
└─ if validation failed
├─ session_manager.set_run_context_visible(run_id, false)(隐藏失败草稿尝试)
├─ attempt 2 重新规划 single / team
├─ revision request + team result -> 主 Agent synthesis execution_context
└─ 第二次结果无论验证是否通过,都返回并等待用户反馈
```
---
## 4. Direct Run
```text
AgentLoop.process_direct(task)(直接执行一轮用户任务)
├─ 生成 session_id确定这句话属于哪个会话
├─ 生成 run_id给本次运行生成唯一编号
├─ memory_service.capture_snapshot_for_run()(每个 run 捕获独立记忆快照)
│ └─ fresh MemoryStore(root).load_from_disk()(不写共享 `_snapshot`,避免并发串记忆)
├─ session_manager.ensure_session(session_id)(确保会话存在)
├─ session_manager.append_message(event_type="run_started", hidden)(记录隐藏事件:本轮开始)
├─ make_provider_bundle()(装配模型 provider 组合)
│ ├─ main_runtime主模型配置
│ ├─ main_provider主模型调用器
│ ├─ fallback_runtime备用模型配置
│ ├─ fallback_provider备用模型调用器
│ ├─ auxiliary_runtime辅助模型配置
│ ├─ auxiliary_provider辅助模型调用器用于选 skill 等)
│ └─ embedding_runtime向量模型配置用于语义召回
├─ if include_skill_assembly=Falsesimple_chat 默认关闭)
│ └─ skip SkillAssembler不激活 skill不注入 skill 正文)
├─ if include_skill_assembly=TrueTask mode 默认开启,在 Task 创建/复用和规划之后执行)
│ └─ skill_assembler.assemble(...)(选择本轮应该激活哪些 published skill
│ ├─ input task_description = skill_selection_context or current user input
│ │ ├─ Task goal / description
│ │ ├─ current user request
│ │ ├─ attempt / revision / team synthesis phase
│ │ ├─ validation feedback重试时
│ │ ├─ team summary / planteam synthesis 时)
│ │ └─ previously activated skills只作为 reuse bias不是 pinned
│ ├─ SkillsLoader.build_selection_candidates()(列出候选技能摘要)
│ ├─ embedding retrieve skill candidates用向量召回相关技能
│ ├─ LLM shortlist candidate names先用摘要粗选少量候选
│ │ └─ if retrieved candidates <= max_detailed_candidates -> skip shortlist
│ ├─ SkillsLoader.load_published_skill(...)(系统侧内部读取粗选候选正文,不暴露 skill_view 给主 Agent
│ ├─ LLM final select activated skills结合候选正文做最终选择
│ ├─ if no matching skill -> return [] and continue run without skills
│ └─ 返回 activated skills返回本轮被激活的技能
│ ├─ name技能名称
│ ├─ content技能正文
│ ├─ version技能版本
│ ├─ content_hash技能内容哈希用于追踪
│ ├─ activation_reason为什么激活
│ └─ tool_hints技能建议使用哪些工具
├─ ContextBuilder.build_skill_activation_messages(...)(把激活技能变成模型可读消息)
├─ 构造 SkillActivationReceipt[](构造技能激活收据)
├─ session_manager.append_message(...)(记录隐藏事件:本轮用了哪些技能)
│ ├─ event_type="skill_activation_snapshotted"(技能激活快照)
│ ├─ hidden不进入普通聊天上下文
│ └─ payload隐藏数据
│ ├─ receipts技能激活收据
│ └─ activation_messages实际注入给模型的技能消息
├─ tool_assembler.assemble(...)选择本轮应该暴露哪些工具simple_chat 默认跳过)
│ ├─ always tools默认总是可用的工具
│ ├─ activated skill tool hints被激活技能推荐的工具
│ ├─ embedding retrieve tools用向量召回相关工具
│ └─ 返回 selected ToolSpec[](返回本轮工具列表)
├─ session_manager.append_message(event_type="tool_selection_snapshotted", hidden)(记录隐藏事件:工具选择快照)
├─ ContextBuilder.build_messages(...)(构造发给模型的完整 messages
│ ├─ build_system_prompt()(构造 system prompt
│ │ ├─ base system prompt基础系统提示词
│ │ ├─ session metadata当前会话元信息
│ │ ├─ execution context本轮额外执行上下文
│ │ └─ frozen memory snapshot冻结记忆快照
│ ├─ insert activated skill messages插入已激活技能正文
│ ├─ append visible history追加可见历史聊天
│ └─ append current user input追加当前用户输入
├─ session_manager.update_system_prompt(...)(把本轮 system prompt 快照写回会话)
├─ session_manager.append_message(event_type="skill_selection_context_snapshotted", hidden)(完整记录 skill query
├─ session_manager.append_message(event_type="system_prompt_snapshotted", hidden)记录隐藏事件system prompt 快照)
├─ session_manager.append_message(event_type="user_message_added")(记录可见事件:用户消息)
├─ 进入 tool loop进入模型回答和工具调用循环
├─ 成功时(模型正常结束)
│ ├─ session_manager.append_message(event_type="run_completed", hidden)(记录隐藏事件:运行完成)
│ └─ _record_run_receipts(...)(记录运行证据,不生成学习候选)
├─ 失败时(运行中出现异常)
│ ├─ append assistant error message写入 assistant 错误消息)
│ ├─ session_manager.append_message(event_type="run_failed", hidden)(记录隐藏事件:运行失败)
│ └─ _record_run_receipts(...)(即使失败也记录运行证据)
└─ return AgentRunResult返回本轮结果
├─ session_id会话编号
├─ run_id运行编号
├─ output_text最终回复文本
├─ finish_reason结束原因
├─ tool_iterations工具循环次数
├─ provider_name模型供应商
├─ model模型名称
├─ usagetoken 用量)
├─ task_idTask 模式下返回)
├─ task_statusTask 模式下返回)
└─ validation_resultTask 模式下返回)
```
---
## 5. Tool Loop
```text
tool loop工具调用循环
├─ session_manager.append_message(event_type="llm_request_snapshotted", hidden)(完整记录本次 provider messages / tools
├─ provider.chat(messages, tools=schemas)(把消息和工具 schema 发给模型)
├─ session_manager.update_usage(...)(累计 token 用量)
├─ session_manager.append_message(event_type="assistant_message_added")(记录 assistant 回复)
├─ ContextBuilder.add_assistant_message(...)(把 assistant 回复追加到本轮 messages
├─ if no tool calls如果模型没有要求调用工具
│ └─ finish结束本轮回答
└─ if tool calls如果模型要求调用工具
├─ ToolExecutor.execute_tool_call(...)(执行一个工具调用)
├─ session_manager.append_message(event_type="tool_result_recorded")(记录工具结果)
├─ ContextBuilder.add_tool_result(...)(把工具结果追加到 messages
└─ 回到 provider.chat(...)(带着工具结果继续问模型)
```
---
## 6. Run Evidence / Skill Effect Recording
```text
AgentLoop._record_run_receipts(...)(记录本轮运行证据;不直接学习)
├─ 构造 RunRecord构造本轮运行记录
│ ├─ run_id运行编号
│ ├─ session_id会话编号
│ ├─ task_text用户原始任务
│ ├─ task_id内部 Task 编号,简单问题可为空)
│ ├─ attempt_indexTask 模式下的尝试序号)
│ ├─ started_at开始时间
│ ├─ ended_at结束时间
│ ├─ success是否成功
│ ├─ finish_reason结束原因
│ ├─ validation_resultTask 模式下的验证结果)
│ ├─ feedback用户反馈
│ └─ activated_skills本轮激活过的技能收据
├─ 构造 SkillEffectRecord[](构造技能效果记录)
│ └─ 每个 activated skill 一条(每个被用到的技能都单独记一条)
├─ skill_learning_service.collect_run_receipts(...)(收集运行收据)
│ ├─ RunMemoryStore.append_run_record(...)(把 RunRecord 写入 memory/runs/runs.jsonl
│ ├─ RunMemoryStore.append_skill_effect(...)(把 SkillEffectRecord 写入 memory/runs/skill-effects.jsonl
│ ├─ SkillLearningService.rescore_skill_versions()(重新统计每个技能版本表现)
│ │ └─ SkillLearningStore.update_performance_snapshot(...)(更新表现快照)
│ └─ never build learning candidates in runtime hot path运行完成时永不生成候选
└─ session_manager.append_message(...)(记录隐藏事件:技能效果快照)
├─ event_type="skill_effects_snapshotted"(技能效果已快照)
├─ hidden不进入普通聊天上下文
└─ payload隐藏数据
├─ run_record本轮运行记录
├─ skill_effects技能效果记录
├─ candidate_generation_allowed本轮是否允许生成候选runtime 固定 false
└─ learning_candidates学习候选默认空
```
```text
runtime invariant运行期不直接学习
├─ run completed / run failed
│ └─ 只写 RunRecord + SkillEffectRecord + performance snapshot
├─ simple chat
│ └─ 不创建 Task不触发 learning candidate
└─ Task attempt / sub-agent run
└─ 只留下证据,等待 feedback gate 决定是否学习
```
---
## 7. Chat Feedback / Learning Gate
```text
POST /api/chat/feedback聊天反馈接口不是 Task 管理 API
├─ input
│ ├─ session_id
│ ├─ run_id
│ ├─ feedback_type
│ │ ├─ satisfied
│ │ ├─ revise
│ │ └─ abandon
│ └─ comment?
├─ AgentService.submit_feedback(...)
│ ├─ TaskService.get_task_by_run_id(run_id)
│ ├─ reject if task/session mismatch
│ ├─ reject conflicting feedback for same run
│ ├─ same feedback is idempotent
│ └─ TaskService.add_feedback(...)
├─ satisfied
│ ├─ if validation accepted
│ │ ├─ Task status -> closed
│ │ └─ SkillLearningService.build_learning_candidates_for_task(task_id, trigger_run_id)
│ └─ if validation not accepted
│ └─ 记录人工接受但保留验证风险;不自动生成 learning candidate
├─ revise
│ ├─ Task status -> needs_revision
│ ├─ 更新 run / skill effect 为需修订证据
│ └─ 下一条用户消息默认复用该 Task不生成 learning candidate
└─ abandon
├─ Task status -> abandoned
├─ 更新 run / skill effect 为失败证据
├─ 追加 task_failure_evidence_recorded 隐藏事件
└─ 默认不写主 memory不生成成功 Skill draft
```
---
## 8. Agent Team v1 / Local Coordinator
```text
TeamService.run_team(...)(内部 team 执行入口,不暴露产品级 Task API
├─ validate parent task如果传 parent_task_id先校验 Task 存在且 session 匹配)
├─ TeamGraphScheduler.run(...)
│ ├─ graph.validate()
│ │ ├─ v1 implemented strategies: sequence / parallel / dag
│ │ └─ reserved strategies: moa / hierarchy / heavy / group_chat / forest / maker / router
│ ├─ provider_bundle_factory(node)(推荐:每个节点拿 fresh provider bundle
│ ├─ inherited_pinned_skills主 agent 明确委派给 sub-agent 的 pinned skills
│ ├─ inherited_pinned_skill_contextsmissing skill 生成的一次性 ephemeral guidance
│ └─ allow_candidate_generation=False默认只写 receipts不绕过 Task feedback gate
├─ LocalAgentRunner.run(envelope)
│ ├─ 生成 child_session_id
│ ├─ parent_session_id -> 主 session建立 session lineage
│ ├─ AgentLoop.process_direct / submit_direct(...)(复用主 AgentLoop / ContextBuilder / ToolAssembler / SkillAssembler / MemoryService
│ ├─ pinned_skill_names -> AgentLooppublished pinned skill 必须注入)
│ ├─ pinned_skill_contexts -> AgentLoopephemeral guidance 只在本次 run 注入)
│ └─ provider_bundle + node model/provider override 禁止混用
├─ strategy execution
│ ├─ sequence前一节点成功输出进入后一节点 dependency_outputs
│ ├─ parallel同层节点 asyncio.gather 真并发执行
│ └─ dag按依赖拓扑分批并发失败节点会阻断依赖它的后续节点
├─ node-level failure normalization
│ ├─ provider factory / runner 普通异常 -> NodeRunResult(success=False, finish_reason="error")
│ ├─ asyncio.CancelledError 继续抛出
│ └─ blocked dependency -> NodeRunResult(success=False, finish_reason="blocked")
├─ TeamRunResult
│ ├─ success
│ ├─ summary只聚合成功节点输出失败节点列入 Failed nodes
│ ├─ node_results
│ ├─ run_ids
│ ├─ session_ids
│ └─ task_id父 Task
└─ attach runs to parent Task
└─ TaskService.append_run(parent_task_id, sub_run_id, skill_names=...)
```
```text
Team v1 scope当前边界
├─ 已实现
│ ├─ Beaver 自有 coordinator models
│ ├─ sequence / parallel / dag 三个执行原语
│ ├─ pinned skill 继承 + open skill assembly
│ ├─ per-run memory snapshot支持真并发 prompt 构建
│ ├─ per-node provider factory 语义
│ ├─ parent Task 一致性校验
│ └─ 节点失败归一和 summary 失败区块
├─ 已接入 Task mode 内部执行链
│ ├─ TaskExecutionPlanner 先决定 single / team
│ ├─ team run 只作为内部 sub-agent 执行策略
│ ├─ TeamRunResult 不直接返回给用户
│ └─ 主 Agent synthesis run 生成用户可见最终回答
└─ 仍不暴露产品级 team / Task API
└─ 外部仍只使用聊天入口和反馈入口
```
---
## 9. Session Module
```text
SessionManager会话管理门面
├─ ensure_session(...)(确保会话存在)
├─ append_message(...)(追加一条事件或聊天消息)
├─ get_event_records(session_id)(获取完整事件流)
├─ get_run_event_records(session_id, run_id)(获取某次 run 的事件)
├─ update_latest_assistant_event_payload(...)(把 task/validation/feedback 状态投影到最新 assistant 消息)
├─ set_run_context_visible(session_id, run_id, visible)(隐藏失败重试草稿等 run
├─ list_run_ids(session_id)(列出某个会话下所有 run_id
├─ get_messages_as_conversation(session_id)(获取可作为聊天展示的消息)
├─ get_visible_history(session_id)(获取可进入 prompt 的历史)
├─ update_system_prompt(...)(更新当前会话 system prompt 快照)
├─ update_usage(...)(更新 token 用量)
├─ end_session(...)(结束会话)
├─ reopen_session(...)(重新打开会话)
├─ list_sessions_rich(...)(列出带摘要的会话)
├─ search_messages(...)(搜索历史消息)
└─ resolve_session_id(...)(根据前缀解析 session_id
```
```text
SessionStore (SQLite)SQLite 会话数据库)
├─ sessions table会话表
├─ messages table消息和事件表
├─ messages_fts全文搜索索引
├─ WALSQLite 写入日志模式)
├─ parent_session_id父会话字段给未来分支会话用
└─ hidden / visible event split隐藏事件和可见消息分离
```
```text
hidden events隐藏事件类型
├─ run_started运行开始
├─ skill_activation_snapshotted技能激活快照
├─ tool_selection_snapshotted工具选择快照
├─ system_prompt_snapshotted系统提示词快照
├─ run_completed运行完成
├─ run_failed运行失败
├─ skill_effects_snapshotted技能效果快照
├─ task_validation_snapshottedTask 验证快照)
└─ task_feedback_recordedTask 用户反馈快照)
```
---
## 10. Memory Module
```text
MemoryService记忆服务
├─ initialize()(初始化记忆存储)
├─ reload_for_new_run()(每轮开始前刷新记忆快照)
├─ get_snapshot()(获取本轮冻结记忆快照)
└─ get_store()(获取底层 MemoryStore
```
```text
MemoryStore长期记忆存储
├─ target: memory项目/任务级长期记忆)
├─ target: user用户偏好记忆
├─ add(...)(新增记忆)
├─ replace(...)(替换记忆)
├─ remove(...)(删除记忆)
├─ load_from_disk()(从磁盘读取)
├─ save_to_disk()(保存到磁盘)
└─ format_for_system_prompt(...)(格式化成 system prompt 段落)
```
```text
memory runtime semantics记忆运行语义
├─ run start本轮开始
│ └─ refresh live state -> capture frozen snapshot刷新 live memory并冻结本轮快照
├─ run middle本轮进行中
│ ├─ memory tool may write durable statememory 工具可以写入长期记忆)
│ └─ current run prompt snapshot stays frozen但本轮 prompt 里的记忆不变)
└─ next run下一轮
└─ newly written memory becomes visible上一轮写入的新记忆开始可见
```
---
## 11. Skills Module
```text
SkillsLoader技能加载器
├─ workspace published catalog工作区正式发布的技能目录
├─ workspace legacy skills/*/SKILL.md旧格式技能文件
├─ builtin skills内置技能
├─ list_skills()(列出运行时可见技能)
├─ list_published_skills()(只列正式发布技能)
├─ get_current_version()(获取当前正式版本)
├─ load_published_skill()(加载正式版本正文)
├─ get_skill_record()(获取技能元数据记录)
├─ get_skill_metadata()(获取 frontmatter 元数据)
├─ get_skill_tool_hints()(获取技能推荐工具)
├─ load_skills_for_context()(把多个技能加载成上下文块)
├─ build_skills_summary()(构造技能摘要索引)
├─ build_selection_candidates()(构造给 SkillAssembler 的候选摘要)
├─ list_skill_supporting_files()(列出技能支持文件)
└─ get_always_skills()(获取 always 类型技能)
```
```text
SkillAssembler技能选择器
├─ input输入
│ ├─ task_descriptionTask-aware queryTask 描述 / 当前用户消息 / previous skills / attempt context / validation revision context / team context
│ ├─ candidate skill summaries候选技能摘要
│ ├─ embedding runtime向量模型配置
│ └─ selector provider/model用于选择技能的模型
├─ embedding retrieve candidates先用向量召回相关技能
├─ LLM shortlist names用摘要粗选需要查看正文的候选
│ └─ skip when candidate count <= max_detailed_candidates候选很少时直接读取正文
├─ internal load shortlisted SKILL.mdSkillAssembler 内部读取候选正文)
├─ LLM final select names结合候选正文选择最终技能名
├─ no match returns [](没有对应 published skill 时返回空,不阻塞任务)
└─ return SkillContext[](返回技能上下文)
├─ name技能名
├─ content技能正文
├─ version技能版本
├─ content_hash内容哈希
├─ activation_reason激活原因
└─ tool_hints推荐工具
```
```text
skills lifecycle baseline技能生命周期基线
├─ SkillSpecStore技能生命周期文件存储
│ ├─ skill.json技能总信息
│ ├─ current.json当前版本指针
│ ├─ versions/(正式版本目录)
│ ├─ drafts/(草稿目录)
│ ├─ reviews/(审核记录目录)
│ └─ archive/(归档目录)
├─ DraftService草稿服务
│ ├─ create_new_skill_draft(...)(创建新技能草稿)
│ ├─ create_revision_draft(...)(创建修订草稿)
│ ├─ create_merge_draft(...)(创建合并草稿)
│ ├─ create_retire_proposal(...)(创建退役提案)
│ ├─ list_drafts(...)(列出草稿)
│ └─ get_draft(...)(读取单个草稿)
├─ ReviewService审核服务
│ ├─ submit_for_review(...)(提交审核)
│ ├─ approve(...)(批准草稿)
│ └─ reject(...)(拒绝草稿)
└─ SkillPublisher发布服务
├─ publish(...)(发布 approved 草稿为正式版本)
├─ apply_retire_proposal(...)(应用退役提案,不创建新版本)
├─ disable(...)(禁用技能)
└─ rollback(...)(回滚到旧版本)
```
---
## 12. Tools Module
```text
ToolRegistry工具注册表
├─ echo回显工具
├─ memory写入/管理长期记忆)
├─ session_search搜索会话历史
├─ list_directory列目录
├─ read_file读文件
└─ search_files搜索文件
```
```text
ToolAssembler工具选择器
├─ selected = always tools先加入默认工具
├─ selected += activated skill tool hints再加入技能推荐工具
├─ selected += embedding top-k tools再用向量召回任务相关工具
└─ return ToolSpec[](返回本轮可用工具列表;不通过工具动态加载 skill
```
```text
ToolExecutor工具执行器
├─ normalize tool call规范化模型发来的工具调用
├─ resolve tool找到对应工具
├─ invoke tool执行工具
└─ return ToolResult返回工具结果
```
```text
filesystem tool boundary文件系统工具边界
├─ workspace scoped只能访问 workspace 范围)
├─ realpath enforcement用真实路径校验
├─ reject path escape拒绝路径逃逸
├─ reject symlink escape拒绝软链接逃逸
└─ reject binary file reads拒绝读取二进制文件
```
---
## 13. Provider Module
```text
make_provider_bundle(...)(创建模型调用组合)
├─ main_runtime主模型配置
├─ main_provider主模型调用器
├─ fallback_runtime备用模型配置
├─ fallback_provider备用模型调用器
├─ auxiliary_runtime辅助模型配置
├─ auxiliary_provider辅助模型调用器
└─ embedding_runtime向量模型配置
```
```text
provider roles模型角色分工
├─ main主模型
│ └─ assistant/tool loop负责正常回答和工具循环
├─ fallback备用模型
│ └─ main failure recovery主模型失败时兜底
├─ auxiliary辅助模型
│ └─ skill selection / future helper tasks负责选择技能等辅助任务
└─ embedding向量模型
└─ skill/tool semantic retrieval负责 skill / tool 的语义召回)
```
---
## 14. Service Lifecycle
```text
AgentService服务生命周期
├─ MainAgentRouter请求进入 AgentLoop 前先分类 simple / task
├─ submit_feedback(...)(聊天反馈入口,内部更新 Task 状态)
├─ direct mode直接模式适合 CLI / 单次调用)
│ └─ process_direct(...)(直接处理一次任务)
└─ running mode后台运行模式适合 Web / Gateway
├─ start()(启动 AgentLoop.run
├─ submit_direct(...)(向队列提交任务)
├─ stop(timeout_seconds, force)(停止并等待任务收尾)
├─ shutdown(timeout_seconds, force)(停止并释放 runtime
└─ close()(关闭已停止的 loop
```
```text
running mode semantics后台运行语义
├─ start()(启动)
│ └─ AgentLoop.run()(进入队列消费循环)
├─ submit_direct()(提交任务)
│ └─ enqueue _DirectRunRequest把任务放入队列
├─ stop()(停止)
│ ├─ stop accepting new tasks不再接收新任务
│ └─ drain queued tasks等待已排队任务处理完
└─ close()(关闭)
└─ requires loop already stopped必须先 stop才能 close
```
---
## 15. Task Team Registry / Process / Learning 闭环
```text
TaskExecutionPlannerTask 内部执行规划)
├─ LLM planner
│ ├─ 输出 single / team
│ └─ team 只允许 sequence / parallel / dag
├─ TaskSkillResolver
│ ├─ 从 published skill catalog 检索候选
│ ├─ 按 skill_query / required_capabilities / node task 选择 skill
│ ├─ 命中 published skill 后写入 graph.nodes[].inherited_pinned_skills
│ └─ 无命中时创建 ephemeral guidance并写入 graph.nodes[].inherited_pinned_skill_contexts
└─ TaskExecutionPlan
├─ graph.nodes[].agent 只是 generic runtime trace identity
└─ to_event_payload() 写入 skill_queries / selected_skill_names / ephemeral_guidance_ids / skill_resolution_report
```
```text
Task mode attempt每次 Task attempt
├─ task_execution_planned隐藏事件
│ └─ plan_mode / strategy / node_ids / skill_resolution_report
├─ team run仅 team plan
│ ├─ sub-agent run_ids 回填父 Task
│ ├─ team summary 只进入主 Agent synthesis context
│ └─ task_team_run_completed / task_team_run_failed隐藏事件
├─ main Agent synthesis
│ ├─ 输出最终用户可见回答
│ └─ task_synthesis_completed隐藏事件
└─ validation
├─ task_validation_snapshotted隐藏事件
├─ 第一次失败隐藏草稿并重试一次
└─ 第二次或验证通过后等待反馈
```
```text
Frontend process projection
├─ GET /api/sessions/{session_id}/process
│ ├─ 读取隐藏 Task/team/validation 事件
│ ├─ 合并 run memory records
│ └─ 输出 processRuns / processEvents / processArtifacts / agents
└─ ChatWorkbench
├─ 桌面端显示 ProcessLane
├─ 移动端显示 Process tab
└─ 不直接暴露隐藏事件原始 JSON
```
```text
Learning pipeline
├─ evidence recording
│ ├─ every run -> RunRecord
│ ├─ activated skills -> SkillEffectRecord
│ └─ no candidates generated here
├─ feedback gate
│ ├─ validation accepted + satisfied -> scoped learning candidate
│ ├─ validation rejected + satisfied -> 记录人工接受风险,不生成候选
│ ├─ revise -> 保持 Task 打开,不生成候选
│ └─ abandon -> 失败证据,不写主 memory不生成成功候选
├─ scoped candidate generation
│ ├─ source = current task run_ids
│ ├─ no published skill -> new_skill
│ └─ published skill used -> revise_skill
├─ SkillLearningPipelineService
│ ├─ candidate -> queued / synthesizing
│ ├─ worker/run-once -> draft
│ ├─ draft -> safety report
│ ├─ draft -> lightweight eval report
│ ├─ safety_failed / eval_failed 阻断发布
│ ├─ draft -> submit review
│ ├─ approve / reject
│ ├─ approved + safety passed + eval not failed -> publish
│ ├─ retire proposal -> apply retire
│ └─ rollback / disable
├─ SkillLearningWorker
│ ├─ 默认按配置定时扫描 open candidates
│ ├─ 自动生成 draft_ready / safety_failed / eval_failed
│ └─ 永不自动 approve / publish
├─ Web review workbench
│ ├─ Candidates
│ ├─ Draft detail
│ ├─ Safety report
│ └─ Eval report
└─ Runtime catalog
└─ 只有 published skill 进入运行时选择draft 不生效
```
---
## 16. Web / Gateway
```text
Web网页入口
├─ FastAPI lifespanFastAPI 生命周期)
│ ├─ create or receive AgentService创建或接收 AgentService
│ ├─ start() when app owns lifecycle如果 Web app 拥有生命周期,就启动 service
│ ├─ start SkillLearningWorker when enabled按配置启动技能学习 worker
│ └─ shutdown() when app owns lifecycle如果 Web app 拥有生命周期,就关闭 service / worker
├─ GET /api/ping健康检查接口
│ └─ return status / running / mode返回状态、是否运行、运行模式
├─ POST /api/chat聊天接口
│ ├─ validate WebChatRequest校验请求体
│ ├─ agent_service.submit_direct(...)(把用户消息提交给 AgentService
│ └─ return WebChatResponse返回模型回复 + run/task/validation 元数据)
├─ WS /ws/{session_id}(网页 WebSocket 适配层)
│ ├─ ping -> pong
│ ├─ message -> agent_service.submit_direct(...)
│ ├─ return status / assistant message携带 run/task/validation 元数据)
│ └─ return session_updated通知前端刷新 session/process
└─ POST /api/chat/feedback聊天反馈接口
├─ validate WebChatFeedbackRequest
├─ agent_service.submit_feedback(...)
└─ return WebChatFeedbackResponse
Skills learning admin API
├─ GET /api/skills/candidates
├─ POST /api/skills/candidates/{candidate_id}/draft
├─ POST /api/skills/candidates/{candidate_id}/regenerate
├─ POST /api/skills/learning/run-once
├─ GET /api/skills/{skill_name}/drafts/{draft_id}/safety
├─ GET /api/skills/{skill_name}/drafts/{draft_id}/eval
└─ POST /api/skills/{skill_name}/drafts/{draft_id}/publish
└─ requires approved review + safety passed + eval not failed
```
```text
Gateway消息通道入口
├─ MessageBus内部消息总线
├─ ChannelAdapterTelegram / Slack / Email / WhatsApp 等只作为 adapter
├─ inbound -> AgentService.handle_inbound_message(...)(外部消息进入 AgentService
├─ outbound <- OutboundMessageAgentService 返回结构化输出消息)
└─ ChannelManager按 message.channel 分发 outbound
```
---
## 17. Bus Mode Skeleton
```text
AgentLoop.run()(后台队列运行模式)
├─ create queue创建任务队列
├─ mark running标记为运行中
├─ consume _DirectRunRequest消费一个任务请求
├─ call _process_direct_impl(...)(调用真正的单轮执行逻辑)
├─ set future result / exception把结果或异常写回等待方
├─ stop() -> enqueue sentinel停止时放入结束标记
└─ drain pending queue on exit退出时清理未处理任务
```

File diff suppressed because it is too large Load Diff

View File

@ -1,350 +0,0 @@
# backend-old -> backend 可用移植指南
这份文档描述的是:从 `app-instance/backend-old` 迁到新的 `app-instance/backend` 时,哪些 Python 代码是可用的、应该放到新目录的哪里、哪些可以直接迁、哪些必须拆分后迁。
本文默认遵守以下前提:
1. 新后端统一使用 `beaver` 命名。
2. 所有 agent 最终都复用同一套 `beaver.engine.AgentLoop`
3. 新代码按当前新目录落点来放,不再向 `nanobot/` 回写。
4. 不保留 `third_party/`
5. memory 设计以 `hermes-agent` 为基线:统一 CRUD memory tool + frozen snapshot + session_search。
## 1. 迁移范围
本文覆盖的是 `backend-old` 中的自有 Python 源码:
- `backend-old/nanobot/**/*.py`
- `backend-old/nanobot/llm_audit.py`
本文明确不覆盖:
- `.venv/`
- `.pytest_cache/`
- `.ruff_cache/`
- `third_party/`
- `bridge/` 里的 Node 代码
## 2. 迁移判定说明
| 判定 | 含义 |
| --- | --- |
| `可直接迁移` | 改导入路径、命名后可基本原样落到新位置 |
| `小幅重构` | 主体逻辑可复用,但要改依赖注入、类型名或路径 |
| `拆分迁移` | 旧文件职责过大,必须拆成多个新文件 |
| `重写迁移` | 只保留行为目标,不建议原样搬代码 |
| `不迁移` | 不进入新后端 |
## 3. 总体迁移顺序
建议按下面顺序迁:
1. `foundation/config + foundation/events + foundation/models + foundation/utils`
2. `skills + plugins`
3. `memory(curated + session_search baseline)`
4. `engine/session + engine/providers + engine/context + engine/loop`
5. `tools`
6. `coordinator`
7. `interfaces/web + interfaces/cli + interfaces/channels + services`
8. `integrations/a2a + integrations/outlook + integrations/authz + integrations/mcp`
原因:
1. `engine``coordinator``interfaces` 都依赖 `config``events``models`
2. `skills``memory` 必须先定契约,因为 system prompt 注入、memory tool、session_search 都会反向约束 engine。
3. `web/server.py``cli/commands.py` 只有在服务层和内核层稳定后才值得拆。
## 4. 包初始化文件如何处理
下面这些旧 `__init__.py` 不建议原样迁移,只保留“最小 re-export”或空包文件
- `nanobot/__init__.py`
- `nanobot/a2a/__init__.py`
- `nanobot/agent/__init__.py`
- `nanobot/agent_team/__init__.py`
- `nanobot/authz/__init__.py`
- `nanobot/bus/__init__.py`
- `nanobot/channels/__init__.py`
- `nanobot/cli/__init__.py`
- `nanobot/config/__init__.py`
- `nanobot/cron/__init__.py`
- `nanobot/heartbeat/__init__.py`
- `nanobot/providers/__init__.py`
- `nanobot/session/__init__.py`
- `nanobot/templates/__init__.py`
- `nanobot/templates/memory/__init__.py`
- `nanobot/utils/__init__.py`
- `nanobot/web/__init__.py`
统一处理规则:
1. 不复制旧的 lazy import / `__getattr__` 设计。
2. 目标文件稳定后,再在对应 `beaver/*/__init__.py` 里做显式导出。
## 5. Foundation 层迁移映射
| 旧文件 | 关键类/函数 | 新位置 | 判定 | 说明 |
| --- | --- | --- | --- | --- |
| `nanobot/config/schema.py` | `Config`, `AgentDefaults`, `AgentsConfig`, `ProviderConfig`, `ProvidersConfig`, `ToolsConfig`, `ChannelsConfig`, `AuthzConfig`, `BackendIdentityConfig` | `beaver/foundation/config/schema.py` | `小幅重构` | 主体模型可迁;`Config._match_provider/get_provider*` 这类 provider 解析逻辑移到 `beaver/engine/providers/factory.py`。 |
| `nanobot/config/loader.py` | `get_config_path`, `get_data_dir`, `load_config`, `save_config`, `_migrate_config` | `beaver/foundation/config/loader.py` | `可直接迁移` | 迁移时把默认路径和 `beaver` 命名改掉。 |
| `nanobot/config/paths.py` | `get_data_dir`, `get_media_dir` | `beaver/foundation/config/paths.py` | `可直接迁移` | 纯 path helper。 |
| `nanobot/utils/helpers.py` | `ensure_dir`, `get_workspace_path`, `get_sessions_path`, `get_skills_path`, `timestamp`, `truncate_string`, `safe_filename`, `parse_session_key` | `beaver/foundation/utils/helpers.py` | `可直接迁移` | 纯工具函数,直接复用。 |
| `nanobot/bus/events.py` | `InboundMessage`, `OutboundMessage` | `beaver/foundation/events/messages.py` | `可直接迁移` | 建议作为全局消息模型。 |
| `nanobot/bus/queue.py` | `MessageBus` | `beaver/foundation/events/message_bus.py` | `小幅重构` | 类本身可迁,但后续由 `services``interfaces/gateway` 注入。 |
| `nanobot/agent/process_events.py` | `new_run_id`, `utc_now_iso`, `process_event_sink`, `process_run_context`, `current_process_run_id`, `has_process_event_sink`, `emit_process_event` | `beaver/foundation/events/process.py` | `可直接迁移` | 这是全局过程事件层,应该上提。 |
| `nanobot/agent/run_result.py` | `normalize_summary_text`, `contains_placeholder_summary`, `has_meaningful_summary`, `AgentRunResult` | `beaver/foundation/models/run_result.py` | `可直接迁移` | 给 engine、coordinator、services 共享。 |
| `nanobot/cron/types.py` | `CronSchedule`, `CronPayload`, `CronAction`, `CronExecutionResult`, `CronJobState`, `CronJob`, `CronStore` | `beaver/foundation/models/cron.py` | `可直接迁移` | 纯类型定义。 |
| `nanobot/llm_audit.py` | `write_llm_audit_event`, `redact_mapping`, `summarize_messages`, `summarize_tool_calls`, `summarize_tools`, `summarize_exception` | `beaver/foundation/utils/llm_audit.py` | `可直接迁移` | 审计逻辑不应挂在旧根路径。 |
## 6. Engine 层迁移映射
| 旧文件 | 关键类/函数 | 新位置 | 判定 | 说明 |
| --- | --- | --- | --- | --- |
| `nanobot/session/manager.py` | `Session`, `SessionManager` | `beaver/engine/session/models.py`, `beaver/engine/session/manager.py` | `可直接迁移` | `Session``SessionManager` 建议拆开。 |
| `nanobot/agent/context.py` | `ContextBuilder.build_system_prompt`, `build_messages`, `add_tool_result`, `add_assistant_message` | `beaver/engine/context/builder.py` | `小幅重构` | 需要改成只注入 frozen memory snapshot而不是直接读 live memory。 |
| `nanobot/agent/loop.py` | `AgentLoop` 全类 | `beaver/engine/loop.py` | `拆分迁移` | 旧文件过载,不能整块搬。 |
| `nanobot/agent/memory.py` | `MemoryStore.read_long_term`, `write_long_term`, `append_history`, `get_memory_context`, `consolidate` | `beaver/memory/curated/store.py`, `beaver/memory/curated/snapshot.py`, `beaver/memory/search/transcript_store.py` | `重写迁移` | 新标准不再沿用旧 `consolidate()` 模型;按 Hermes 改成 CRUD memory + frozen snapshot + session transcript search。 |
| `nanobot/providers/base.py` | `ToolCallRequest`, `LLMResponse`, `LLMProvider` | `beaver/engine/providers/base.py` | `可直接迁移` | provider 契约层。 |
| `nanobot/providers/registry.py` | `ProviderSpec`, `find_by_model`, `find_gateway`, `find_by_name` | `beaver/engine/providers/registry.py` | `可直接迁移` | registry 自包含度高。 |
| `nanobot/providers/litellm_provider.py` | `LiteLLMProvider` | `beaver/engine/providers/litellm.py` | `小幅重构` | 主要改导入路径和 `Config` 依赖。 |
| `nanobot/providers/openai_codex_provider.py` | `OpenAICodexProvider`, `_convert_messages`, `_consume_sse`, `_friendly_error` | `beaver/engine/providers/codex.py` | `小幅重构` | 主体可迁。 |
| `nanobot/providers/custom_provider.py` | `CustomProvider` | `beaver/engine/providers/custom.py` | `可直接迁移` | 文件小,直接迁。 |
| `nanobot/providers/transcription.py` | `GroqTranscriptionProvider` | `beaver/engine/providers/transcription.py` | `可直接迁移` | 辅助 provider不阻塞主线。 |
| `nanobot/agent/subagent.py` | `SubagentManager.run_local_task`, `_build_local_tools`, `_build_subagent_prompt`, `_strip_think`, `_tool_hint` | `beaver/engine/runtime/local_runner.py` | `重写迁移` | 新架构下不应保留 `SubagentManager` 这个名字;只抽出本地 agent 运行能力。 |
| `nanobot/agent/subagents.py` | `normalize_subagent_id`, `SubagentSpec`, `LocalSubagentStore` | `beaver/coordinator/registry/local_subagent_store.py` | `小幅重构` | 数据结构可复用,但要切到 `beaver` 命名和新 registry。 |
### 6.1 `agent/loop.py` 函数级拆分
`nanobot/agent/loop.py` 应这样拆:
| 旧函数 | 新位置 |
| --- | --- |
| `AgentLoop.__init__` | `beaver/engine/loop.py` |
| `apply_runtime_config` | `beaver/engine/loader.py` |
| `_register_default_tools` | `beaver/engine/loader.py` + `beaver/tools/registry/tool_registry.py` |
| `_connect_mcp`, `_clear_mcp_tools`, `reload_mcp_servers`, `get_mcp_servers_view` | `beaver/engine/runtime/mcp_runtime.py` |
| `_set_tool_context` | `beaver/engine/loop.py` |
| `_build_skills_loader` | `beaver/skills/resolver/runtime.py` |
| `_run_agent_loop`, `_process_message`, `_save_turn`, `process_system_announcement`, `process_direct` | `beaver/engine/loop.py` |
| `_get_consolidation_lock`, `_prune_consolidation_lock`, `_consolidate_memory` | `不直接迁移;改成 beaver/tools/builtins/memory.py + beaver/memory/curated/* + beaver/memory/search/*` |
| `run`, `stop`, `close_mcp` | `beaver/engine/loop.py` |
## 7. Tools 层迁移映射
| 旧文件 | 关键类/函数 | 新位置 | 判定 | 说明 |
| --- | --- | --- | --- | --- |
| `nanobot/agent/tools/base.py` | `Tool`, `validate_params`, `to_schema` | `beaver/tools/base.py` | `可直接迁移` | 工具基类。 |
| `nanobot/agent/tools/registry.py` | `ToolRegistry` | `beaver/tools/registry/tool_registry.py` | `可直接迁移` | registry 逻辑自包含。 |
| `nanobot/agent/tools/filesystem.py` | `ReadFileTool`, `WriteFileTool`, `EditFileTool`, `ListDirTool` | `beaver/tools/builtins/filesystem.py` | `小幅重构` | 路径保护规则建议进一步抽到 `beaver/permissions/guards/filesystem.py`。 |
| `nanobot/agent/tools/shell.py` | `ExecTool`, `_guard_command`, `_guard_protected_paths` | `beaver/tools/builtins/shell.py` | `小幅重构` | 命令保护逻辑建议下沉到 `permissions/guards/shell.py`。 |
| `nanobot/agent/tools/web.py` | `WebSearchTool`, `WebFetchTool` | `beaver/tools/builtins/web.py` | `可直接迁移` | 改导入路径即可。 |
| `nanobot/agent/tools/message.py` | `MessageTool`, `set_context`, `set_send_callback`, `start_turn` | `beaver/tools/builtins/message.py` | `可直接迁移` | 保持 tool 形态。 |
| `nanobot/agent/tools/spawn.py` | `DelegationTool`, `SpawnSubagentTool`, `SpawnAgentTeamTool`, `NestedDelegateTool` | `beaver/tools/builtins/spawn.py` | `小幅重构` | tool 壳可留;执行全部接 `beaver/coordinator/delegation/manager.py`。 |
| `nanobot/agent/tools/cron.py` | `CronTool` | `beaver/tools/builtins/cron.py` | `可直接迁移` | 与 `CronService` 对接即可。 |
| `nanobot/agent/tools/cron_action.py` | `CronActionTool` | `beaver/tools/builtins/cron.py` | `小幅重构` | 合并成同一 cron 工具模块更合理。 |
| `nanobot/agent/tools/mcp.py` | `MCPToolWrapper`, `connect_mcp_servers`, `_describe_mcp_exception` | `beaver/tools/mcp/wrapper.py`, `beaver/tools/mcp/connect.py` | `小幅重构` | 连接逻辑和 wrapper 分开。 |
| `无旧文件一一对应(以 Hermes 为准新增)` | `memory_tool(action, target, content, old_text)` | `beaver/tools/builtins/memory.py` | `新增实现` | 统一 memory CRUD 工具,优先级高于旧 `MemoryStore.consolidate()` 逻辑。 |
| `无旧文件一一对应(以 Hermes 为准新增)` | `session_search(query, role_filter, limit)` | `beaver/tools/builtins/session_search.py` | `新增实现` | 历史会话检索不再靠把大量过程细节塞进 memory。 |
## 8. Skills、Plugins、Memory 层迁移映射
| 旧文件 | 关键类/函数 | 新位置 | 判定 | 说明 |
| --- | --- | --- | --- | --- |
| `nanobot/agent/skills.py` | `SkillsLoader.list_skills`, `load_skill`, `load_skills_for_context`, `build_skills_summary`, `get_always_skills`, `get_skill_metadata`, `get_skill_agent_cards`, `list_skill_agent_cards` | `beaver/skills/catalog/loader.py`, `beaver/skills/resolver/runtime.py` | `拆分迁移` | catalog 负责扫描/索引/元数据resolver 负责运行时注入。 |
| `nanobot/agent/skill_reviews.py` | `SkillReviewManager` | `beaver/skills/reviews/manager.py` | `小幅重构` | ZIP 解包、安全检查、review 状态管理都能复用。 |
| `nanobot/agent/plugins.py` | `PluginAgent`, `PluginCommand`, `Plugin`, `PluginLoader` | `beaver/plugins/models.py`, `beaver/plugins/loader.py`, `beaver/plugins/registry.py` | `小幅重构` | 这是最适合先迁的一批。 |
| `nanobot/agent/marketplace.py` | `MarketplaceEntry`, `MarketplacePluginInfo`, `MarketplaceManager` | `beaver/plugins/marketplace.py` | `可直接迁移` | 市场逻辑相对独立。 |
| `nanobot/agent_team/memory.py` | `ProcedureMemory`, `RunMemory`, `task_tokens`, `similarity_score`, `clip_confidence` | `beaver/memory/procedures/procedure_memory.py`, `beaver/memory/runs/run_memory.py` | `小幅重构` | 这些不再代表主 memory 契约,而是 coordinator/analytics 的可选优化层。 |
| `nanobot/agent/memory.py` | `MemoryStore` | `beaver/memory/curated/store.py`, `beaver/memory/curated/snapshot.py`, `beaver/memory/search/transcript_store.py` | `重写迁移` | 见第 6 节memory 基线改成 Hermes 风格。 |
| `nanobot/skills/subagent-manager/scripts/subagentctl.py` | `cmd_list`, `cmd_show`, `cmd_create`, `cmd_delete`, `cmd_set_system_prompt`, `cmd_add_mcp_http`, `cmd_add_mcp_stdio`, `cmd_remove_mcp` | `beaver/interfaces/cli/subagentctl.py` | `小幅重构` | 只迁 CLI 管理能力,不再绑定 `nanobot` store。 |
### 8.1 `agent/skills.py` 函数级拆分
| 旧函数/方法 | 新位置 |
| --- | --- |
| `list_skills`, `get_skill_metadata`, `get_skill_agent_cards`, `list_skill_agent_cards` | `beaver/skills/catalog/loader.py` |
| `load_skill`, `load_skills_for_context`, `build_skills_summary`, `get_always_skills` | `beaver/skills/resolver/runtime.py` |
| `_strip_frontmatter`, `_parse_nanobot_metadata`, `_check_requirements`, `_get_missing_requirements`, `_get_skill_description` | `beaver/skills/catalog/utils.py``loader.py` 内部私有函数 |
### 8.2 Memory 迁移基线
新的 memory 迁移必须遵守下面这条,而不是直接复制旧 `MemoryStore + ProcedureMemory` 设计:
1. 持久化记忆只保留两类:
- `memory`
- `user`
2. 写操作统一通过 `memory` tool
- `add`
- `replace`
- `remove`
3. `replace/remove` 使用短语义片段匹配,不要求 UUID
4. 写入协议必须是:
- 注入扫描
- 文件锁
- 锁内 reload
- 重复/上限检测
- 原子写入
5. system prompt 只注入 frozen snapshot
6. 历史细节通过 `session_search` 检索,不扩大 memory
7. 稳定的方法论、工作法、可复用技巧进入 `skills`
8. `ProcedureMemory` 只保留为 coordinator 的复用优化
## 9. Coordinator 层迁移映射
| 旧文件 | 关键类/函数 | 新位置 | 判定 | 说明 |
| --- | --- | --- | --- | --- |
| `nanobot/agent/agent_registry.py` | `AgentDescriptor`, `WorkspaceAgentStore`, `AgentRegistry` | `beaver/coordinator/registry/models.py`, `workspace_store.py`, `agent_registry.py` | `拆分迁移` | descriptor、store、registry 三类职责应拆开。 |
| `nanobot/agent/delegation.py` | `DelegationRun`, `DelegationManager` | `beaver/coordinator/delegation/manager.py`, `beaver/coordinator/execution/delegation_run.py`, `beaver/coordinator/delegation/events.py` | `拆分迁移` | 旧文件职责最重,不能原样搬。 |
| `nanobot/a2a/client.py` | `A2AClient`, `A2AError`, `A2AUnsupportedMethodError`, `A2AStreamEvent` | `beaver/integrations/a2a/client.py` | `小幅重构` | A2A 是协议层,适合独立迁。 |
| `nanobot/agent_team/types.py` | `ExecutionMode`, `ResolvedTeamPlan`, `SwarmsRunSpec`, `SwarmsRunResult`, `BridgeResult` | `beaver/coordinator/models.py` | `重写迁移` | v1 已改为 Beaver 自有 `AgentDescriptor / ExecutionGraph / TeamRunResult`,不直接保留 swarms wire shape。 |
| `nanobot/agent_team/orchestrator.py` | `AgentTeamOrchestrator.run_task` | `beaver/services/team_service.py`, `beaver/coordinator/execution/scheduler.py` | `重写迁移` | v1 入口是 `TeamService.run_team(...)`,调度由 `TeamGraphScheduler` 承担。 |
| `nanobot/agent_team/provisioning.py` | `ProvisioningManager`, `SpecialistProvisionResult` | 后续 `beaver/coordinator/team/provisioning.py` | `暂缓迁移` | v1 不做自动 provisioning先由显式 `AgentDescriptor` 描述节点。 |
| `nanobot/agent_team/target_resolver.py` | `TargetResolver.resolve_team_targets`, `_select_existing_for_role_with_llm` | 后续 `beaver/coordinator/team/target_resolver.py` | `暂缓迁移` | v1 不做 registry/target resolver后续高级策略再补。 |
| `nanobot/agent_team/swarms_policy.py` | `SwarmsPolicy` | 后续 `beaver/coordinator/backends/swarms/policy.py` 或 strategy preset policy | `暂缓迁移` | v1 不接 swarms runtime策略约束先落在 Beaver graph validation / scheduler。 |
| `nanobot/agent_team/swarms_planner.py` | `SwarmsRunPlanner` | 后续 strategy preset -> `ExecutionGraph` | `重写迁移` | 只吸收策略形态,不保留 `third_party` 假设。 |
| `nanobot/agent_team/swarms_bridge.py` | `SwarmsBridge` | 后续 `beaver/coordinator/backends/swarms/bridge.py` | `暂缓迁移` | 只有确实接外部 swarms backend 时才需要。 |
| `nanobot/agent_team/swarms_adapter.py` | `ensure_swarms_importable`, `load_swarms_runtime`, `safe_swarms_name`, `NanobotAgentAdapter` | 后续 `beaver/coordinator/backends/swarms/runtime.py`, `adapter.py` | `重写迁移` | 不再允许 `third_party/` 路径探测v1 不依赖 swarms runtime。 |
### 9.1 `agent/delegation.py` 函数级拆分
| 旧函数/方法 | 新位置 |
| --- | --- |
| `dispatch_subagent`, `dispatch_agent_team`, `_dispatch`, `_run_dispatch` | `beaver/coordinator/delegation/manager.py` |
| `_emit_team_progress`, `_emit_agent_started`, `_emit_agent_finished`, `_emit_agent_cancelled`, `_emit_group_started`, `_emit_group_finished`, `_publish_prefixed_progress`, `_emit_direct_user_message` | `beaver/coordinator/delegation/events.py` |
| `_run_team_member_for_swarms`, `_execute_descriptor`, `_build_progress_callback`, `_build_task_callback` | `beaver/coordinator/execution/member_runner.py` |
| `_resolve_single`, `_resolve_nested_delegate`, `_normalize_skill_names`, `_build_skill_context`, `_augment_task_with_skills` | `beaver/coordinator/delegation/manager.py` |
| `cancel`, `cancel_all`, `_cancel_remote_tasks`, `_announce_cancelled` | `beaver/coordinator/execution/cancel.py` 或先保留在 `manager.py` |
| `_announce_single_result`, `_announce_orchestrator_result`, `_publish_announcement`, `_notify_direct_announcement` | `beaver/coordinator/delegation/announcement.py` |
## 10. Services 层迁移映射
| 旧文件 | 关键类/函数 | 新位置 | 判定 | 说明 |
| --- | --- | --- | --- | --- |
| `nanobot/cron/service.py` | `CronService` | `beaver/services/cron_service.py` | `小幅重构` | 保持后台服务角色,不要再埋在 `nanobot/cron/` 目录。 |
| `nanobot/cron/runtime.py` | `run_cron_job`, `_build_execution_context`, `_resolve_session_key` | `beaver/services/cron_runtime.py` | `小幅重构` | 与 `AgentLoop`、Web、CLI 的对接点要更新。 |
| `nanobot/heartbeat/service.py` | `HeartbeatService` | `beaver/services/heartbeat_service.py` | `可直接迁移` | 后台服务类,自包含度高。 |
## 11. Interfaces 层迁移映射
### 11.1 CLI
| 旧文件 | 关键类/函数 | 新位置 | 判定 | 说明 |
| --- | --- | --- | --- | --- |
| `nanobot/__main__.py` | 模块入口 | `beaver/interfaces/cli/main.py` | `可直接迁移` | 只保留入口壳。 |
| `nanobot/cli/commands.py` | `main`, `version_callback`, `agent`, `gateway`, `web`, `onboard`, `_create_workspace_templates`, `status`, `channels_*`, `cron_*`, `provider_*` | `beaver/interfaces/cli/main.py`, `beaver/interfaces/cli/commands/*.py`, `beaver/services/*.py` | `拆分迁移` | 旧 CLI 同时做了命令声明、provider 装配、gateway/web 启动、cron 管理。 |
### 11.2 `cli/commands.py` 函数级拆分
| 旧函数 | 新位置 |
| --- | --- |
| `main`, `version_callback` | `beaver/interfaces/cli/main.py` |
| `agent` | `beaver/interfaces/cli/commands/agent.py`,内部调用 `beaver/services/agent_service.py` |
| `gateway` | `beaver/interfaces/gateway/main.py` |
| `web` | `beaver/interfaces/cli/commands/web.py`,内部调用 `beaver/interfaces/web/app.py` |
| `_make_provider` | `beaver/engine/providers/factory.py` |
| `onboard`, `_create_workspace_templates`, `status` | `beaver/services/admin_service.py` + CLI 薄包装 |
| `channels_main`, `channels_status`, `channels_login` | `beaver/interfaces/cli/commands/channels.py` |
| `cron_main`, `cron_list`, `cron_add`, `cron_remove`, `cron_enable`, `cron_run` | `beaver/interfaces/cli/commands/cron.py` + `beaver/services/cron_service.py` |
| `provider_main`, `_register_login`, `provider_login`, `_login_openai_codex`, `_login_github_copilot` | `beaver/interfaces/cli/commands/providers.py` + `beaver/services/admin_service.py` |
| `_flush_pending_tty_input`, `_restore_terminal`, `_init_prompt_session`, `_print_agent_response`, `_is_exit_command`, `_read_interactive_input_async`, `_exit_after_group_help`, `_get_bridge_dir` | `beaver/interfaces/cli/tty.py` |
### 11.3 Web
| 旧文件 | 关键类/函数 | 新位置 | 判定 | 说明 |
| --- | --- | --- | --- | --- |
| `nanobot/web/server.py` | `create_app`, `WebSocketBroadcaster`, 所有 `*Request/*Response` 模型、所有 auth / handoff / route helper | `beaver/interfaces/web/app.py`, `deps.py`, `realtime.py`, `auth.py`, `routes/*.py`, `schemas/*.py` | `拆分迁移` | 这是第二个最大拆分热点。 |
| `nanobot/web/files.py` | `save_file`, `get_file_metadata`, `list_files`, `browse_workspace`, `save_to_workspace`, `delete_workspace_path`, `create_workspace_dir` | `beaver/interfaces/web/files.py`, `beaver/interfaces/web/routes/files.py` | `小幅重构` | 纯文件 API 逻辑,应该从 `server.py` 分离出去。 |
| `nanobot/web/outlook.py` | `connect_workspace`, `disconnect_workspace`, `outlook_status`, `get_overview`, `get_message_detail`, `list_messages`, `list_events`, `ensure_outlook_mcp_registration` | `beaver/integrations/outlook/service.py`,由 `beaver/interfaces/web/routes/outlook.py` 调用 | `小幅重构` | 核心逻辑应放 integration不应继续留在 web 包下。 |
### 11.4 `web/server.py` 函数级拆分
| 旧函数/类型 | 新位置 |
| --- | --- |
| `create_app` | `beaver/interfaces/web/app.py` |
| `ChatRequest`, `ChatResponse` | `beaver/interfaces/web/schemas/chat.py` |
| `AddCronJobRequest`, `ToggleCronJobRequest` | `beaver/interfaces/web/schemas/cron.py` |
| `AddMarketplaceRequest`, `ApproveSkillReviewRequest` | `beaver/interfaces/web/schemas/plugins.py`, `skills.py` |
| `AddAgentRequest`, `_discover_agent_payload`, `_manual_agent_payload`, `_should_auto_discover_agent` | `beaver/interfaces/web/routes/agents.py` + `beaver/interfaces/web/schemas/agents.py` |
| `MCPServerRequest` | `beaver/interfaces/web/schemas/mcp.py` |
| `SubagentRequest` | `beaver/interfaces/web/schemas/delegation.py` |
| `OutlookConnectionRequest` | `beaver/interfaces/web/schemas/outlook.py` |
| `LoginRequest`, `RegisterRequest`, `AuthzRegisterBackendRequest`, `LocalBackendIdentityRequest`, `HandoffConsumeRequest` | `beaver/interfaces/web/schemas/auth.py` |
| `WebSocketBroadcaster` | `beaver/interfaces/web/realtime.py` |
| `_issue_web_token`, `_require_web_user`, `_issue_handoff_code`, `_consume_handoff_code`, `_prune_handoff_codes`, `_handoff_*` | `beaver/interfaces/web/auth.py` |
| `_register_routes` | 删除;改为 `beaver/interfaces/web/routes/*.py` 各自注册 |
| `_make_provider` | 删除;使用 `beaver/engine/providers/factory.py` |
| `_serialize_job` | `beaver/interfaces/web/serializers/cron.py``schemas/cron.py` |
### 11.5 Channels
| 旧文件 | 关键类/函数 | 新位置 | 判定 | 说明 |
| --- | --- | --- | --- | --- |
| `nanobot/channels/base.py` | `BaseChannel` | `beaver/interfaces/channels/base.py` | `可直接迁移` | 通道抽象基类。 |
| `nanobot/channels/manager.py` | `ChannelManager` | `beaver/interfaces/channels/manager.py` | `小幅重构` | 初始化逻辑要改为 `beaver` config。 |
| `nanobot/channels/dingtalk.py` | `NanobotDingTalkHandler`, `DingTalkChannel` | `beaver/interfaces/channels/dingtalk.py` | `小幅重构` | 改命名和 config 依赖。 |
| `nanobot/channels/discord.py` | `_split_message`, `DiscordChannel` | `beaver/interfaces/channels/discord.py` | `可直接迁移` | 主要改导入路径。 |
| `nanobot/channels/email.py` | `EmailChannel` | `beaver/interfaces/channels/email.py` | `可直接迁移` | 通道逻辑自包含。 |
| `nanobot/channels/feishu.py` | `_extract_*`, `FeishuChannel` | `beaver/interfaces/channels/feishu.py` | `可直接迁移` | 保持通道粒度。 |
| `nanobot/channels/matrix.py` | `_filter_matrix_html_attribute`, `_render_markdown_html`, `_build_matrix_text_content`, `MatrixChannel` | `beaver/interfaces/channels/matrix.py` | `可直接迁移` | 主要改导入。 |
| `nanobot/channels/mochat.py` | `MochatBufferedEntry`, `DelayState`, `MochatTarget`, `MochatChannel` | `beaver/interfaces/channels/mochat.py` | `小幅重构` | 依赖 config 较多。 |
| `nanobot/channels/qq.py` | `_make_bot_class`, `QQChannel` | `beaver/interfaces/channels/qq.py` | `可直接迁移` | 主要改配置路径。 |
| `nanobot/channels/slack.py` | `SlackChannel` | `beaver/interfaces/channels/slack.py` | `可直接迁移` | 主要改导入路径。 |
| `nanobot/channels/telegram.py` | `_markdown_to_telegram_html`, `_split_message`, `TelegramChannel` | `beaver/interfaces/channels/telegram.py` | `可直接迁移` | 通道逻辑自包含。 |
| `nanobot/channels/whatsapp.py` | `WhatsAppChannel` | `beaver/interfaces/channels/whatsapp.py` | `小幅重构` | 作为通道入口保留;桥接细节可抽到 `beaver/integrations/whatsapp/bridge.py`。 |
## 12. Integrations 层迁移映射
| 旧文件 | 关键类/函数 | 新位置 | 判定 | 说明 |
| --- | --- | --- | --- | --- |
| `nanobot/a2a/client.py` | `A2AClient` 全类 | `beaver/integrations/a2a/client.py` | `小幅重构` | 见第 9 节。 |
| `nanobot/web/outlook.py` | Outlook MCP 连接、状态、消息和日历方法 | `beaver/integrations/outlook/service.py` | `小幅重构` | 见第 11 节。 |
| `nanobot/authz/client.py` | `BackendRegistrationResult`, `AuthzClient` | `beaver/integrations/authz/client.py` | `可直接迁移` | 纯外部服务 client新目录里需新增 `authz/`。 |
| `nanobot/providers/transcription.py` | `GroqTranscriptionProvider` | `beaver/integrations/providers/transcription.py``beaver/engine/providers/transcription.py` | `可直接迁移` | 二选一,取决于后续是否把 transcription 视为主 provider。 |
| `nanobot/agent/tools/mcp.py` | `connect_mcp_servers` | `beaver/integrations/mcp/connection.py` + `beaver/tools/mcp/wrapper.py` | `小幅重构` | 协议连接与工具包装分开。 |
## 13. 明确不迁移的内容
| 路径 | 处理方式 | 原因 |
| --- | --- | --- |
| `backend-old/third_party/**` | `不迁移` | 新后端不保留 vendored 第三方目录。 |
| `backend-old/.venv/**` | `不迁移` | 环境文件。 |
| `backend-old/.pytest_cache/**` | `不迁移` | 缓存。 |
| `backend-old/.ruff_cache/**` | `不迁移` | 缓存。 |
| `backend-old/bridge/**` | `保留为外部桥接层,不按 Python 代码移植` | 这是独立 Node bridge。 |
## 14. 第一批最值得迁的文件
如果要先挑“收益最高、风险最低”的一批,顺序建议是:
1. `nanobot/config/loader.py` -> `beaver/foundation/config/loader.py`
2. `nanobot/config/paths.py` -> `beaver/foundation/config/paths.py`
3. `nanobot/config/schema.py` -> `beaver/foundation/config/schema.py`
4. `nanobot/utils/helpers.py` -> `beaver/foundation/utils/helpers.py`
5. `nanobot/agent/process_events.py` -> `beaver/foundation/events/process.py`
6. `nanobot/agent/run_result.py` -> `beaver/foundation/models/run_result.py`
7. `nanobot/session/manager.py` -> `beaver/engine/session/models.py` + `manager.py`
8. `nanobot/providers/base.py` / `registry.py` / `custom_provider.py` / `litellm_provider.py` / `openai_codex_provider.py`
9. `nanobot/agent/context.py` -> `beaver/engine/context/builder.py`
10. `nanobot/agent/tools/base.py` / `registry.py` / `filesystem.py` / `shell.py` / `web.py` / `message.py`
11. `nanobot/agent/plugins.py` -> `beaver/plugins/*`
12. `nanobot/agent/skills.py` -> `beaver/skills/catalog/loader.py` + `resolver/runtime.py`
13. `nanobot/agent_team/types.py` -> `beaver/coordinator/models.py`(按 v1 models 重写)
14. `nanobot/agent_team/memory.py` -> `beaver/memory/procedures/*` + `beaver/memory/runs/*`
15. 以 Hermes 基线新增 `beaver/tools/builtins/memory.py`
16. 以 Hermes 基线新增 `beaver/tools/builtins/session_search.py`
## 15. 最后一句话
`backend-old` 移到新的 `backend`,最重要的不是“先把文件复制过来”,而是始终按这个原则落:
1. `foundation` 放公共模型、配置、事件、工具函数
2. `engine` 放统一 agent 内核
3. `tools` 放工具本身
4. `skills` 放全系统指导层
5. `memory` 放经验沉淀
6. `coordinator` 放委派和多 agent 编排
7. `services` 放应用服务
8. `interfaces` 放 CLI / Web / Gateway / Channels 薄入口
9. `integrations` 放 A2A / MCP / Outlook / Authz 这类外部系统
只要旧文件进入新目录时严格按这条边界落,新后端就不会再次长回 `backend-old` 那种结构。