feat(skill-learning): merge plugin skill updates
This commit is contained in:
@ -41,6 +41,55 @@ class SkillDraftSynthesizer:
|
||||
) -> dict[str, Any]:
|
||||
return await self._synthesize(candidate, evidence_packet, provider, model, "merge", base_skill=base_skill)
|
||||
|
||||
async def synthesize_plugin_update(
|
||||
self,
|
||||
candidate: SkillLearningCandidate,
|
||||
evidence_packet: EvidencePacket,
|
||||
provider: LLMProvider,
|
||||
model: str,
|
||||
*,
|
||||
old_upstream: dict[str, Any],
|
||||
current_local: dict[str, Any],
|
||||
new_upstream: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
prompt = self._build_plugin_update_prompt(
|
||||
candidate,
|
||||
evidence_packet,
|
||||
old_upstream=old_upstream,
|
||||
current_local=current_local,
|
||||
new_upstream=new_upstream,
|
||||
)
|
||||
response = await provider.chat(
|
||||
messages=[
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"You merge Beaver plugin skill updates. Return JSON only with keys: "
|
||||
"frontmatter, content, change_reason, preserved_local_sections, "
|
||||
"adopted_upstream_sections, resolved_conflicts, dropped_sections. "
|
||||
"Preserve valid local learning, adopt upstream fixes and safety changes, "
|
||||
"do not concatenate duplicate sections, and list every intentional drop."
|
||||
),
|
||||
},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
tools=None,
|
||||
model=model,
|
||||
max_tokens=4096,
|
||||
temperature=0,
|
||||
)
|
||||
payload = self._parse_plugin_update_payload(response.content or "")
|
||||
if payload:
|
||||
return payload
|
||||
fallback = self._fallback_payload(candidate, evidence_packet, "plugin_update")
|
||||
return {
|
||||
**fallback,
|
||||
"preserved_local_sections": [],
|
||||
"adopted_upstream_sections": [],
|
||||
"resolved_conflicts": [],
|
||||
"dropped_sections": [],
|
||||
}
|
||||
|
||||
async def _synthesize(
|
||||
self,
|
||||
candidate: SkillLearningCandidate,
|
||||
@ -119,6 +168,28 @@ class SkillDraftSynthesizer:
|
||||
+ "\nThe JSON may include preserved_sections, changed_sections, and dropped_sections arrays."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _build_plugin_update_prompt(
|
||||
candidate: SkillLearningCandidate,
|
||||
evidence_packet: EvidencePacket,
|
||||
*,
|
||||
old_upstream: dict[str, Any],
|
||||
current_local: dict[str, Any],
|
||||
new_upstream: dict[str, Any],
|
||||
) -> str:
|
||||
return (
|
||||
f"Candidate kind: {candidate.kind}\n"
|
||||
f"Reason: {candidate.reason}\n"
|
||||
f"Task summaries:\n- " + "\n- ".join(evidence_packet.task_summaries or ["No historical run evidence."])
|
||||
+ "\n\nOLD UPSTREAM (merge base B):\n"
|
||||
+ str(old_upstream.get("content") or "")
|
||||
+ "\n\nCURRENT LOCAL (Beaver learned version L):\n"
|
||||
+ str(current_local.get("content") or "")
|
||||
+ "\n\nNEW UPSTREAM (plugin update U):\n"
|
||||
+ str(new_upstream.get("content") or "")
|
||||
+ "\n\nReturn JSON only. Preserve useful CURRENT LOCAL learning and adopt important NEW UPSTREAM changes."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _parse_payload(content: str) -> dict[str, Any]:
|
||||
cleaned = content.strip()
|
||||
@ -145,6 +216,33 @@ class SkillDraftSynthesizer:
|
||||
"dropped_sections": _coerce_string_list(payload.get("dropped_sections")),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _parse_plugin_update_payload(content: str) -> dict[str, Any]:
|
||||
cleaned = content.strip()
|
||||
if cleaned.startswith("```"):
|
||||
lines = cleaned.splitlines()
|
||||
if len(lines) >= 3 and lines[0].startswith("```") and lines[-1].startswith("```"):
|
||||
cleaned = "\n".join(lines[1:-1]).strip()
|
||||
try:
|
||||
payload = json.loads(cleaned)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
if not isinstance(payload, dict):
|
||||
return {}
|
||||
frontmatter = payload.get("frontmatter")
|
||||
content_value = payload.get("content")
|
||||
if not isinstance(frontmatter, dict) or not isinstance(content_value, str):
|
||||
return {}
|
||||
return {
|
||||
"frontmatter": frontmatter,
|
||||
"content": content_value.strip(),
|
||||
"change_reason": str(payload.get("change_reason") or ""),
|
||||
"preserved_local_sections": _coerce_string_list(payload.get("preserved_local_sections")),
|
||||
"adopted_upstream_sections": _coerce_string_list(payload.get("adopted_upstream_sections")),
|
||||
"resolved_conflicts": _coerce_string_list(payload.get("resolved_conflicts")),
|
||||
"dropped_sections": _coerce_string_list(payload.get("dropped_sections")),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _normalize_payload(payload: dict[str, Any], evidence_packet: EvidencePacket) -> dict[str, Any]:
|
||||
frontmatter = normalize_skill_frontmatter(
|
||||
|
||||
Reference in New Issue
Block a user