feat(tasks): add skill-templated task graph execution
This commit is contained in:
@ -7,9 +7,11 @@ from dataclasses import dataclass, field, replace
|
||||
from typing import Any
|
||||
|
||||
from beaver.coordinator.models import AgentDescriptor, ExecutionGraph, ExecutionNode
|
||||
from beaver.engine.context import SkillContext
|
||||
from beaver.engine.providers import ProviderBundle
|
||||
from beaver.skills.assembler.embedding_retriever import SkillEmbeddingRetriever
|
||||
from beaver.skills.catalog.loader import SkillsLoader
|
||||
from beaver.skills.catalog.utils import strip_frontmatter
|
||||
from beaver.skills.drafts import DraftService
|
||||
from beaver.skills.learning import EphemeralGuidanceSynthesizer
|
||||
from beaver.tasks.models import TaskRecord
|
||||
@ -24,6 +26,9 @@ class SkillResolutionReport:
|
||||
ephemeral_guidance_id: str | None = None
|
||||
ephemeral_guidance_name: str | None = None
|
||||
ephemeral_used: bool = False
|
||||
requested_skill_name: str | None = None
|
||||
exact_binding_used: bool = False
|
||||
warnings: list[str] = field(default_factory=list)
|
||||
reason: str = ""
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
@ -35,6 +40,9 @@ class SkillResolutionReport:
|
||||
"ephemeral_guidance_id": self.ephemeral_guidance_id,
|
||||
"ephemeral_guidance_name": self.ephemeral_guidance_name,
|
||||
"ephemeral_used": self.ephemeral_used,
|
||||
"requested_skill_name": self.requested_skill_name,
|
||||
"exact_binding_used": self.exact_binding_used,
|
||||
"warnings": list(self.warnings),
|
||||
"reason": self.reason,
|
||||
}
|
||||
|
||||
@ -87,12 +95,45 @@ class TaskSkillResolver:
|
||||
attempt_index: int,
|
||||
provider_bundle: ProviderBundle,
|
||||
) -> tuple[ExecutionNode, SkillResolutionReport]:
|
||||
use_skill = str(node.agent.metadata.get("use_skill") or "").strip()
|
||||
skill_query = str(node.agent.metadata.get("skill_query") or node.task or node.node_id).strip()
|
||||
warnings: list[str] = []
|
||||
required_capabilities = [
|
||||
str(item).strip()
|
||||
for item in node.agent.metadata.get("required_capabilities", [])
|
||||
if str(item).strip()
|
||||
]
|
||||
if use_skill:
|
||||
exact_context = self._load_exact_skill_context(use_skill)
|
||||
if exact_context is not None:
|
||||
resolved = self._generic_node(
|
||||
node,
|
||||
pinned_skill_names=_merge_names(node.inherited_pinned_skills, [use_skill]),
|
||||
pinned_skill_contexts=_merge_skill_contexts(
|
||||
node.inherited_pinned_skill_contexts,
|
||||
[exact_context],
|
||||
),
|
||||
metadata={
|
||||
**node.agent.metadata,
|
||||
"use_skill": use_skill,
|
||||
"skill_query": skill_query,
|
||||
"required_capabilities": required_capabilities,
|
||||
"selected_skill_names": [use_skill],
|
||||
"ephemeral_skill_names": [],
|
||||
"exact_binding_used": True,
|
||||
},
|
||||
)
|
||||
return resolved, SkillResolutionReport(
|
||||
node_id=node.node_id,
|
||||
skill_query=skill_query,
|
||||
required_capabilities=required_capabilities,
|
||||
selected_skill_names=[use_skill],
|
||||
requested_skill_name=use_skill,
|
||||
exact_binding_used=True,
|
||||
reason="exact use_skill binding",
|
||||
)
|
||||
warnings.append(f"use_skill unresolved: {use_skill}")
|
||||
|
||||
if self._is_summary_only_node(node, skill_query=skill_query, required_capabilities=required_capabilities):
|
||||
resolved = self._generic_node(
|
||||
node,
|
||||
@ -104,6 +145,7 @@ class TaskSkillResolver:
|
||||
"required_capabilities": required_capabilities,
|
||||
"selected_skill_names": [],
|
||||
"ephemeral_skill_names": [],
|
||||
"exact_binding_used": False,
|
||||
"summary_uses_dependency_outputs_only": True,
|
||||
},
|
||||
)
|
||||
@ -113,6 +155,9 @@ class TaskSkillResolver:
|
||||
required_capabilities=required_capabilities,
|
||||
selected_skill_names=[],
|
||||
ephemeral_used=False,
|
||||
requested_skill_name=use_skill or None,
|
||||
exact_binding_used=False,
|
||||
warnings=warnings,
|
||||
reason="summary node uses dependency outputs directly",
|
||||
)
|
||||
|
||||
@ -141,6 +186,7 @@ class TaskSkillResolver:
|
||||
"required_capabilities": required_capabilities,
|
||||
"selected_skill_names": selected,
|
||||
"ephemeral_skill_names": [],
|
||||
"exact_binding_used": False,
|
||||
},
|
||||
)
|
||||
return resolved, SkillResolutionReport(
|
||||
@ -149,6 +195,9 @@ class TaskSkillResolver:
|
||||
required_capabilities=required_capabilities,
|
||||
selected_skill_names=selected,
|
||||
ephemeral_used=False,
|
||||
requested_skill_name=use_skill or None,
|
||||
exact_binding_used=False,
|
||||
warnings=warnings,
|
||||
reason="matched published skill",
|
||||
)
|
||||
|
||||
@ -174,6 +223,7 @@ class TaskSkillResolver:
|
||||
"ephemeral_guidance_id": missing.guidance_id,
|
||||
"ephemeral_guidance_name": missing.guidance_name,
|
||||
"ephemeral_skill_names": [missing.skill_context.name],
|
||||
"exact_binding_used": False,
|
||||
},
|
||||
)
|
||||
return resolved, SkillResolutionReport(
|
||||
@ -183,9 +233,27 @@ class TaskSkillResolver:
|
||||
ephemeral_guidance_id=missing.guidance_id,
|
||||
ephemeral_guidance_name=missing.guidance_name,
|
||||
ephemeral_used=True,
|
||||
requested_skill_name=use_skill or None,
|
||||
exact_binding_used=False,
|
||||
warnings=warnings,
|
||||
reason="generated ephemeral guidance for missing sub-agent capability",
|
||||
)
|
||||
|
||||
def _load_exact_skill_context(self, name: str) -> SkillContext | None:
|
||||
record = self.skills_loader.get_skill_record(name)
|
||||
raw_content = self.skills_loader.load_published_skill(name)
|
||||
content = strip_frontmatter(raw_content).strip() if raw_content else ""
|
||||
if record is None or not content:
|
||||
return None
|
||||
return SkillContext(
|
||||
name=name,
|
||||
content=content,
|
||||
version=record.version,
|
||||
content_hash=record.content_hash or "",
|
||||
activation_reason="explicit_node_binding",
|
||||
tool_hints=list(record.tool_hints),
|
||||
)
|
||||
|
||||
async def _select_published_skills(self, *, query: str, provider_bundle: ProviderBundle) -> list[str]:
|
||||
candidates = self.skills_loader.build_selection_candidates()
|
||||
if not candidates:
|
||||
@ -336,3 +404,14 @@ def _merge_names(parent: list[str], selected: list[str]) -> list[str]:
|
||||
if name and name not in result:
|
||||
result.append(name)
|
||||
return result
|
||||
|
||||
|
||||
def _merge_skill_contexts(parent: list[SkillContext], selected: list[SkillContext]) -> list[SkillContext]:
|
||||
result: list[SkillContext] = []
|
||||
seen: set[str] = set()
|
||||
for context in [*parent, *selected]:
|
||||
if context.name in seen:
|
||||
continue
|
||||
seen.add(context.name)
|
||||
result.append(context)
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user