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:
2026-05-08 17:14:14 +08:00
parent 5ba5c7e4c1
commit 8a12c30141
93 changed files with 16724 additions and 1247 deletions

View File

@ -0,0 +1,188 @@
"""Publishing, retirement, and rollback flows for Beaver skills."""
from __future__ import annotations
from beaver.skills.catalog.utils import strip_frontmatter
from beaver.skills.specs import SkillDraft, SkillReviewState, SkillSpec, SkillSpecStore, SkillStatus, SkillVersion
from beaver.skills.specs.serialization import canonical_hash, normalize_frontmatter, summarize_skill_content
class SkillPublisher:
def __init__(self, store: SkillSpecStore) -> None:
self.store = store
def publish(self, skill_name: str, draft_id: str, publisher: str, notes: str = "") -> SkillVersion:
draft = self._require_draft(skill_name, draft_id)
if draft.status != SkillReviewState.APPROVED.value:
raise ValueError("Draft must be approved before publish")
if draft.proposal_kind == "retire_skill":
raise ValueError("Retire proposals must be applied through apply_retire_proposal")
next_version = self._next_version(skill_name)
content = self._render_skill_content(draft.proposed_frontmatter, draft.proposed_content)
body = strip_frontmatter(content).strip()
if not body:
raise ValueError("Published skill content cannot be empty")
version = SkillVersion(
skill_name=skill_name,
version=next_version,
content_hash=canonical_hash(content),
summary_hash=canonical_hash(body),
created_at=_utc_now(),
created_by=publisher,
change_reason=notes or draft.reason,
parent_version=draft.base_version,
review_state=SkillReviewState.PUBLISHED.value,
frontmatter=normalize_frontmatter(draft.proposed_frontmatter),
summary=summarize_skill_content(body),
tool_hints=self.store._extract_tool_hints(normalize_frontmatter(draft.proposed_frontmatter)),
provenance={
"draft_id": draft_id,
"proposal_kind": draft.proposal_kind,
"trigger_run_id": draft.trigger_run_id,
"trigger_session_id": draft.trigger_session_id,
},
)
self.store.write_skill_version(version, content)
self.store.set_current_version(skill_name, next_version)
spec = self.store.get_skill_spec(skill_name)
if spec is None:
description = str(version.frontmatter.get("description") or skill_name)
spec = SkillSpec(
name=skill_name,
display_name=skill_name,
description=description,
created_at=_utc_now(),
updated_at=_utc_now(),
current_version=next_version,
status=SkillStatus.ACTIVE.value,
tags=[],
owners=[publisher],
source_kind="managed",
lineage=[],
)
else:
spec.current_version = next_version
spec.updated_at = _utc_now()
spec.status = SkillStatus.ACTIVE.value
if not spec.description:
spec.description = str(version.frontmatter.get("description") or skill_name)
self.store.write_skill_spec(spec)
draft.status = SkillReviewState.PUBLISHED.value
self.store.write_draft(draft)
self._refresh_indexes(skill_name, spec.status)
return version
def apply_retire_proposal(self, skill_name: str, draft_id: str, actor: str, notes: str = "") -> SkillSpec:
draft = self._require_draft(skill_name, draft_id)
if draft.status != SkillReviewState.APPROVED.value:
raise ValueError("Retire proposal must be approved before apply")
if draft.proposal_kind != "retire_skill":
raise ValueError("Only retire_skill proposals can be applied as retire proposals")
spec = self._require_spec(skill_name)
if draft.base_version and spec.current_version and draft.base_version != spec.current_version:
raise ValueError(
f"Retire proposal targets {draft.base_version}, but current version is {spec.current_version}"
)
reason = notes or draft.reason
spec.status = SkillStatus.DISABLED.value
spec.updated_at = _utc_now()
if actor and actor not in spec.owners:
spec.owners.append(actor)
spec.lineage.append(f"retire_proposal:{draft_id}:{reason}")
self.store.write_skill_spec(spec)
draft.status = SkillReviewState.DISABLED.value
self.store.write_draft(draft)
self._refresh_indexes(skill_name, spec.status)
return spec
def disable(self, skill_name: str, actor: str, reason: str) -> SkillSpec:
spec = self._require_spec(skill_name)
spec.status = SkillStatus.DISABLED.value
spec.updated_at = _utc_now()
if actor and actor not in spec.owners:
spec.owners.append(actor)
if reason:
spec.lineage.append(f"disabled:{reason}")
self.store.write_skill_spec(spec)
self._refresh_indexes(skill_name, spec.status)
return spec
def rollback(self, skill_name: str, target_version: str, actor: str, reason: str) -> SkillSpec:
if self.store.read_published_skill(skill_name, target_version) is None:
raise ValueError(f"Unknown skill version for rollback: {skill_name}/{target_version}")
spec = self._require_spec(skill_name)
spec.current_version = target_version
spec.updated_at = _utc_now()
spec.status = SkillStatus.ACTIVE.value
if reason:
spec.lineage.append(f"rollback:{target_version}:{reason}")
if actor and actor not in spec.owners:
spec.owners.append(actor)
self.store.write_skill_spec(spec)
self.store.set_current_version(skill_name, target_version)
self._refresh_indexes(skill_name, spec.status)
return spec
def _next_version(self, skill_name: str) -> str:
versions = [item for item in self.store.list_versions(skill_name) if item.startswith("v")]
if not versions:
return "v0001"
numbers = [int(item[1:]) for item in versions if item[1:].isdigit()]
return f"v{(max(numbers) if numbers else 0) + 1:04d}"
@staticmethod
def _render_skill_content(frontmatter: dict, body: str) -> str:
normalized = normalize_frontmatter(frontmatter)
if not normalized:
return body.strip() + ("\n" if body.strip() else "")
lines = ["---"]
for key, value in normalized.items():
if isinstance(value, list):
lines.append(f"{key}:")
for item in value:
lines.append(f" - {item}")
else:
lines.append(f"{key}: {value}")
lines.append("---")
lines.append("")
lines.append(body.strip())
return "\n".join(lines).rstrip() + "\n"
def _refresh_indexes(self, skill_name: str, status: str) -> None:
published = self.store.read_index("published")
disabled = self.store.read_index("disabled")
if status == SkillStatus.DISABLED.value:
if skill_name in published:
published = [item for item in published if item != skill_name]
if skill_name not in disabled:
disabled.append(skill_name)
else:
if skill_name not in published:
published.append(skill_name)
disabled = [item for item in disabled if item != skill_name]
self.store.update_index("published", published)
self.store.update_index("disabled", disabled)
def _require_draft(self, skill_name: str, draft_id: str) -> SkillDraft:
draft = self.store.read_draft(skill_name, draft_id)
if draft is None:
raise ValueError(f"Draft not found: {skill_name}/{draft_id}")
return draft
def _require_spec(self, skill_name: str) -> SkillSpec:
spec = self.store.get_skill_spec(skill_name)
if spec is None:
raise ValueError(f"Skill spec not found: {skill_name}")
return spec
def _utc_now() -> str:
from datetime import datetime, timezone
return datetime.now(timezone.utc).isoformat()