feat(beaver): 完成Task Team功能v1实现,重构后端架构支持统一内核
新增内部Task系统,包括验证、反馈门控机制,实现自动质量验证 (通过率>=0.75)和用户反馈闭环(satisfied/revise/abandon)。 实现Agent Team v1协调器,支持sequence/parallel/dag执行策略, sub-agent复用主AgentLoop,每个run使用独立memory snapshot。 建立Skill学习pipeline,包含draft/审核/发布/回滚完整生命周期, 通过Task验证通过且用户满意才生成学习候选。 重构目录结构,移除third_party依赖,建立统一engine内核, 所有agent共享运行时基础组件。 更新ContextBuilder清理provider消息字段,增强SkillContext版本管理, 集成TaskExecutionPlanner和TaskSkillResolver实现技能解析机制。
This commit is contained in:
@ -17,11 +17,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from beaver.skills.specs.storage import SkillSpecStore
|
||||
|
||||
from .utils import (
|
||||
check_requirements,
|
||||
escape_xml,
|
||||
@ -39,6 +41,13 @@ class SkillRecord:
|
||||
name: str
|
||||
path: Path
|
||||
source: str
|
||||
version: str = "legacy"
|
||||
content_hash: str | None = None
|
||||
source_kind: str = "legacy"
|
||||
status: str = "active"
|
||||
tool_hints: list[str] = field(default_factory=list)
|
||||
frontmatter: dict[str, Any] = field(default_factory=dict)
|
||||
description: str = ""
|
||||
|
||||
|
||||
class SkillsLoader:
|
||||
@ -50,11 +59,13 @@ class SkillsLoader:
|
||||
*,
|
||||
builtin_skills_dir: str | Path | None = None,
|
||||
extra_dirs: list[str | Path] | None = None,
|
||||
skill_store: SkillSpecStore | None = None,
|
||||
) -> None:
|
||||
self.workspace = Path(workspace)
|
||||
self.workspace_skills = self.workspace / "skills"
|
||||
self.builtin_skills = Path(builtin_skills_dir) if builtin_skills_dir is not None else Path(__file__).resolve().parent.parent / "builtin"
|
||||
self.extra_dirs = [Path(item) for item in (extra_dirs or [])]
|
||||
self.skill_store = skill_store or SkillSpecStore(self.workspace)
|
||||
|
||||
def list_skills(self, *, filter_unavailable: bool = True) -> list[SkillRecord]:
|
||||
"""列出当前可见的 skills。
|
||||
@ -67,14 +78,19 @@ class SkillsLoader:
|
||||
重名 skill 只保留优先级更高的那一个。
|
||||
"""
|
||||
|
||||
ordered_roots: list[tuple[str, Path]] = [
|
||||
("workspace", self.workspace_skills),
|
||||
*[("plugin", path) for path in self.extra_dirs],
|
||||
("builtin", self.builtin_skills),
|
||||
]
|
||||
found: dict[str, SkillRecord] = {}
|
||||
|
||||
for source, root in ordered_roots:
|
||||
for record in self.list_published_skills():
|
||||
if record.name in found:
|
||||
continue
|
||||
if filter_unavailable and not self._record_available(record):
|
||||
continue
|
||||
found[record.name] = record
|
||||
|
||||
for source, root in [
|
||||
*[("plugin", path) for path in self.extra_dirs],
|
||||
("builtin", self.builtin_skills),
|
||||
]:
|
||||
if not root.exists():
|
||||
continue
|
||||
for skill_dir in root.iterdir():
|
||||
@ -84,12 +100,62 @@ class SkillsLoader:
|
||||
name = skill_dir.name
|
||||
if name in found:
|
||||
continue
|
||||
record = SkillRecord(name=name, path=skill_file, source=source)
|
||||
frontmatter, body = parse_frontmatter(skill_file.read_text(encoding="utf-8"))
|
||||
normalized_frontmatter = dict(frontmatter)
|
||||
record = SkillRecord(
|
||||
name=name,
|
||||
path=skill_file,
|
||||
source=source,
|
||||
version="legacy",
|
||||
source_kind=source,
|
||||
tool_hints=self._coerce_tool_names(frontmatter.get("tools")),
|
||||
frontmatter=normalized_frontmatter,
|
||||
description=str(frontmatter.get("description") or summarize_body(body) or name),
|
||||
)
|
||||
if filter_unavailable and not self._record_available(record):
|
||||
continue
|
||||
found[name] = record
|
||||
return list(found.values())
|
||||
|
||||
def list_published_skills(self, *, filter_unavailable: bool = True) -> list[SkillRecord]:
|
||||
"""只列 workspace 中正式 published 的 skill catalog。"""
|
||||
|
||||
results: list[SkillRecord] = []
|
||||
for name in self.skill_store.list_published_skill_names():
|
||||
loaded = self.skill_store.read_published_skill(name)
|
||||
if loaded is None:
|
||||
continue
|
||||
if loaded.version.version == "legacy":
|
||||
path = self.workspace_skills / name / "SKILL.md"
|
||||
else:
|
||||
path = self.workspace_skills / name / "versions" / loaded.version.version / "SKILL.md"
|
||||
record = SkillRecord(
|
||||
name=name,
|
||||
path=path,
|
||||
source="workspace",
|
||||
version=loaded.version.version,
|
||||
content_hash=loaded.version.content_hash,
|
||||
source_kind=str(loaded.version.provenance.get("source_kind") or "workspace"),
|
||||
status=str(loaded.version.review_state or "published"),
|
||||
tool_hints=list(loaded.version.tool_hints),
|
||||
frontmatter=dict(loaded.version.frontmatter),
|
||||
description=str(loaded.version.frontmatter.get("description") or loaded.version.summary or name),
|
||||
)
|
||||
if filter_unavailable and not self._record_available(record):
|
||||
continue
|
||||
results.append(record)
|
||||
return results
|
||||
|
||||
def get_current_version(self, name: str) -> str | None:
|
||||
record = self._find_record(name)
|
||||
return record.version if record is not None else None
|
||||
|
||||
def load_published_skill(self, name: str, version: str | None = None) -> str | None:
|
||||
loaded = self.skill_store.read_published_skill(name, version=version)
|
||||
if loaded is not None:
|
||||
return loaded.content
|
||||
return self.load_skill(name)
|
||||
|
||||
def load_skill(self, name: str) -> str | None:
|
||||
"""按名称加载 skill 原始内容。"""
|
||||
|
||||
@ -106,6 +172,9 @@ class SkillsLoader:
|
||||
def get_skill_metadata(self, name: str) -> dict[str, Any] | None:
|
||||
"""读取 skill frontmatter 元数据。"""
|
||||
|
||||
record = self._find_record(name)
|
||||
if record is not None and record.frontmatter:
|
||||
return dict(record.frontmatter)
|
||||
content = self.load_skill(name)
|
||||
if content is None:
|
||||
return None
|
||||
@ -125,6 +194,10 @@ class SkillsLoader:
|
||||
- 兼容 metadata JSON blob 里的 `tools`
|
||||
"""
|
||||
|
||||
record = self._find_record(name)
|
||||
if record is not None and record.tool_hints:
|
||||
return list(record.tool_hints)
|
||||
|
||||
frontmatter = self.get_skill_metadata(name) or {}
|
||||
meta_blob = parse_skill_metadata_blob(frontmatter.get("metadata", ""))
|
||||
names = [
|
||||
@ -143,7 +216,7 @@ class SkillsLoader:
|
||||
|
||||
sections: list[str] = []
|
||||
for name in skill_names:
|
||||
content = self.load_skill(name)
|
||||
content = self.load_published_skill(name)
|
||||
if not content:
|
||||
continue
|
||||
body = strip_frontmatter(content).strip()
|
||||
@ -167,14 +240,15 @@ class SkillsLoader:
|
||||
|
||||
lines = ["<skills>"]
|
||||
for record in skills:
|
||||
frontmatter = self.get_skill_metadata(record.name) or {}
|
||||
frontmatter = record.frontmatter or self.get_skill_metadata(record.name) or {}
|
||||
meta_blob = parse_skill_metadata_blob(frontmatter.get("metadata", ""))
|
||||
available = check_requirements(meta_blob)
|
||||
description = frontmatter.get("description") or record.name
|
||||
description = frontmatter.get("description") or record.description or record.name
|
||||
load_hint = f'Use skill_view(name="{record.name}") to load the full skill.'
|
||||
lines.append(f' <skill available="{str(available).lower()}">')
|
||||
lines.append(f" <name>{escape_xml(record.name)}</name>")
|
||||
lines.append(f" <description>{escape_xml(description)}</description>")
|
||||
lines.append(f" <version>{escape_xml(record.version)}</version>")
|
||||
lines.append(f" <load_hint>{escape_xml(load_hint)}</load_hint>")
|
||||
support_files = self.list_skill_supporting_files(record.name)
|
||||
if support_files:
|
||||
@ -205,10 +279,10 @@ class SkillsLoader:
|
||||
|
||||
candidates: list[dict[str, str]] = []
|
||||
for record in self.list_skills(filter_unavailable=True):
|
||||
frontmatter = self.get_skill_metadata(record.name) or {}
|
||||
description = str(frontmatter.get("description") or "").strip()
|
||||
frontmatter = record.frontmatter or self.get_skill_metadata(record.name) or {}
|
||||
description = str(frontmatter.get("description") or record.description or "").strip()
|
||||
if not description:
|
||||
raw_content = self.load_skill(record.name) or ""
|
||||
raw_content = self.load_published_skill(record.name) or ""
|
||||
body = strip_frontmatter(raw_content).strip()
|
||||
if body:
|
||||
description = " ".join(body.splitlines()[:3])[:240].strip()
|
||||
@ -216,6 +290,8 @@ class SkillsLoader:
|
||||
{
|
||||
"name": record.name,
|
||||
"description": description or record.name,
|
||||
"version": record.version,
|
||||
"content_hash": record.content_hash or "",
|
||||
}
|
||||
)
|
||||
return candidates
|
||||
@ -249,7 +325,7 @@ class SkillsLoader:
|
||||
if record is None:
|
||||
return None
|
||||
if not self._record_available(record):
|
||||
frontmatter = self.get_skill_metadata(name) or {}
|
||||
frontmatter = record.frontmatter or self.get_skill_metadata(name) or {}
|
||||
meta_blob = parse_skill_metadata_blob(frontmatter.get("metadata", ""))
|
||||
missing = get_missing_requirements(meta_blob)
|
||||
detail = f" Missing requirements: {missing}." if missing else ""
|
||||
@ -274,7 +350,7 @@ class SkillsLoader:
|
||||
|
||||
result: list[str] = []
|
||||
for record in self.list_skills(filter_unavailable=True):
|
||||
frontmatter = self.get_skill_metadata(record.name) or {}
|
||||
frontmatter = record.frontmatter or self.get_skill_metadata(record.name) or {}
|
||||
meta_blob = parse_skill_metadata_blob(frontmatter.get("metadata", ""))
|
||||
if meta_blob.get("always") or str(frontmatter.get("always", "")).lower() == "true":
|
||||
result.append(record.name)
|
||||
@ -326,3 +402,8 @@ class SkillsLoader:
|
||||
if record is None:
|
||||
return False
|
||||
return self._record_available(record)
|
||||
|
||||
|
||||
def summarize_body(body: str) -> str:
|
||||
cleaned = " ".join(line.strip() for line in body.splitlines()[:3] if line.strip()).strip()
|
||||
return cleaned[:240]
|
||||
|
||||
Reference in New Issue
Block a user