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:
@ -1,2 +1,6 @@
|
||||
"""Run records."""
|
||||
|
||||
from .models import RunOutcome, RunRecord, SkillEffectRecord
|
||||
from .store import RunMemoryStore
|
||||
|
||||
__all__ = ["RunMemoryStore", "RunOutcome", "RunRecord", "SkillEffectRecord"]
|
||||
|
||||
142
app-instance/backend/beaver/memory/runs/models.py
Normal file
142
app-instance/backend/beaver/memory/runs/models.py
Normal file
@ -0,0 +1,142 @@
|
||||
"""Run-level receipts and skill effect records."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from beaver.skills.specs import SkillActivationReceipt
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RunOutcome:
|
||||
success: bool
|
||||
finish_reason: str
|
||||
feedback_score: float | None = None
|
||||
notes: str = ""
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"success": self.success,
|
||||
"finish_reason": self.finish_reason,
|
||||
"feedback_score": self.feedback_score,
|
||||
"notes": self.notes,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, payload: dict[str, Any]) -> "RunOutcome":
|
||||
return cls(
|
||||
success=bool(payload.get("success")),
|
||||
finish_reason=str(payload.get("finish_reason") or ""),
|
||||
feedback_score=_coerce_optional_float(payload.get("feedback_score")),
|
||||
notes=str(payload.get("notes") or ""),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class RunRecord:
|
||||
run_id: str
|
||||
session_id: str
|
||||
task_text: str
|
||||
started_at: str
|
||||
ended_at: str
|
||||
success: bool
|
||||
finish_reason: str
|
||||
feedback: dict[str, Any] = field(default_factory=dict)
|
||||
activated_skills: list[SkillActivationReceipt] = field(default_factory=list)
|
||||
task_id: str | None = None
|
||||
attempt_index: int | None = None
|
||||
validation_result: dict[str, Any] | None = None
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"run_id": self.run_id,
|
||||
"session_id": self.session_id,
|
||||
"task_id": self.task_id,
|
||||
"attempt_index": self.attempt_index,
|
||||
"task_text": self.task_text,
|
||||
"started_at": self.started_at,
|
||||
"ended_at": self.ended_at,
|
||||
"success": self.success,
|
||||
"finish_reason": self.finish_reason,
|
||||
"feedback": dict(self.feedback),
|
||||
"activated_skills": [receipt.to_dict() for receipt in self.activated_skills],
|
||||
"validation_result": self.validation_result,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, payload: dict[str, Any]) -> "RunRecord":
|
||||
return cls(
|
||||
run_id=str(payload["run_id"]),
|
||||
session_id=str(payload["session_id"]),
|
||||
task_id=_coerce_optional_str(payload.get("task_id")),
|
||||
attempt_index=_coerce_optional_int(payload.get("attempt_index")),
|
||||
task_text=str(payload.get("task_text") or ""),
|
||||
started_at=str(payload.get("started_at") or ""),
|
||||
ended_at=str(payload.get("ended_at") or ""),
|
||||
success=bool(payload.get("success")),
|
||||
finish_reason=str(payload.get("finish_reason") or ""),
|
||||
feedback=dict(payload.get("feedback") or {}),
|
||||
activated_skills=[
|
||||
SkillActivationReceipt.from_dict(item)
|
||||
for item in payload.get("activated_skills") or []
|
||||
if isinstance(item, dict)
|
||||
],
|
||||
validation_result=(
|
||||
dict(payload["validation_result"])
|
||||
if isinstance(payload.get("validation_result"), dict)
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class SkillEffectRecord:
|
||||
run_id: str
|
||||
skill_name: str
|
||||
skill_version: str
|
||||
success: bool
|
||||
feedback_score: float | None
|
||||
notes: str
|
||||
created_at: str
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"run_id": self.run_id,
|
||||
"skill_name": self.skill_name,
|
||||
"skill_version": self.skill_version,
|
||||
"success": self.success,
|
||||
"feedback_score": self.feedback_score,
|
||||
"notes": self.notes,
|
||||
"created_at": self.created_at,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, payload: dict[str, Any]) -> "SkillEffectRecord":
|
||||
return cls(
|
||||
run_id=str(payload["run_id"]),
|
||||
skill_name=str(payload["skill_name"]),
|
||||
skill_version=str(payload["skill_version"]),
|
||||
success=bool(payload.get("success")),
|
||||
feedback_score=_coerce_optional_float(payload.get("feedback_score")),
|
||||
notes=str(payload.get("notes") or ""),
|
||||
created_at=str(payload.get("created_at") or ""),
|
||||
)
|
||||
|
||||
|
||||
def _coerce_optional_float(value: Any) -> float | None:
|
||||
if value in (None, ""):
|
||||
return None
|
||||
return float(value)
|
||||
|
||||
|
||||
def _coerce_optional_int(value: Any) -> int | None:
|
||||
if value in (None, ""):
|
||||
return None
|
||||
return int(value)
|
||||
|
||||
|
||||
def _coerce_optional_str(value: Any) -> str | None:
|
||||
if value in (None, ""):
|
||||
return None
|
||||
return str(value)
|
||||
98
app-instance/backend/beaver/memory/runs/store.py
Normal file
98
app-instance/backend/beaver/memory/runs/store.py
Normal file
@ -0,0 +1,98 @@
|
||||
"""File-backed run receipt store."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from .models import RunRecord, SkillEffectRecord
|
||||
|
||||
|
||||
class RunMemoryStore:
|
||||
def __init__(self, root: str | Path) -> None:
|
||||
self.root = Path(root)
|
||||
self.root.mkdir(parents=True, exist_ok=True)
|
||||
self.runs_path = self.root / "runs.jsonl"
|
||||
self.effects_path = self.root / "skill-effects.jsonl"
|
||||
|
||||
def append_run_record(self, record: RunRecord) -> None:
|
||||
self._append_jsonl(self.runs_path, record.to_dict())
|
||||
|
||||
def update_run_record(self, run_id: str, **updates: object) -> RunRecord | None:
|
||||
records = self.list_runs()
|
||||
updated: RunRecord | None = None
|
||||
for index, record in enumerate(records):
|
||||
if record.run_id != run_id:
|
||||
continue
|
||||
payload = record.to_dict()
|
||||
payload.update(updates)
|
||||
updated = RunRecord.from_dict(payload)
|
||||
records[index] = updated
|
||||
break
|
||||
if updated is None:
|
||||
return None
|
||||
self.runs_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
self.runs_path.write_text(
|
||||
"".join(
|
||||
json.dumps(record.to_dict(), ensure_ascii=False, sort_keys=True) + "\n"
|
||||
for record in records
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return updated
|
||||
|
||||
def append_skill_effect(self, effect: SkillEffectRecord) -> None:
|
||||
self._append_jsonl(self.effects_path, effect.to_dict())
|
||||
|
||||
def list_runs(self) -> list[RunRecord]:
|
||||
return [RunRecord.from_dict(item) for item in self._read_jsonl(self.runs_path)]
|
||||
|
||||
def list_runs_by_skill(self, skill_name: str, version: str | None = None, limit: int | None = None) -> list[RunRecord]:
|
||||
results: list[RunRecord] = []
|
||||
for record in self.list_runs():
|
||||
matched = False
|
||||
for receipt in record.activated_skills:
|
||||
if receipt.skill_name != skill_name:
|
||||
continue
|
||||
if version is not None and receipt.skill_version != version:
|
||||
continue
|
||||
matched = True
|
||||
break
|
||||
if matched:
|
||||
results.append(record)
|
||||
if limit is not None:
|
||||
return results[-limit:]
|
||||
return results
|
||||
|
||||
def list_skill_effects(self, skill_name: str, version: str | None = None, limit: int | None = None) -> list[SkillEffectRecord]:
|
||||
results: list[SkillEffectRecord] = []
|
||||
for payload in self._read_jsonl(self.effects_path):
|
||||
effect = SkillEffectRecord.from_dict(payload)
|
||||
if effect.skill_name != skill_name:
|
||||
continue
|
||||
if version is not None and effect.skill_version != version:
|
||||
continue
|
||||
results.append(effect)
|
||||
if limit is not None:
|
||||
return results[-limit:]
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
def _append_jsonl(path: Path, payload: dict) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("a", encoding="utf-8") as handle:
|
||||
handle.write(json.dumps(payload, ensure_ascii=False, sort_keys=True) + "\n")
|
||||
|
||||
@staticmethod
|
||||
def _read_jsonl(path: Path) -> list[dict]:
|
||||
if not path.exists():
|
||||
return []
|
||||
results: list[dict] = []
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
cleaned = line.strip()
|
||||
if not cleaned:
|
||||
continue
|
||||
payload = json.loads(cleaned)
|
||||
if isinstance(payload, dict):
|
||||
results.append(payload)
|
||||
return results
|
||||
Reference in New Issue
Block a user