feat(tasks): add skill-templated task graph execution

This commit is contained in:
2026-06-23 10:22:58 +08:00
parent 6843d89b2c
commit 53b13e8eac
53 changed files with 4773 additions and 756 deletions

View File

@ -222,3 +222,179 @@ def test_task_skill_resolver_keeps_summary_nodes_skillless(tmp_path: Path) -> No
assert reports[0].ephemeral_used is False
assert reports[0].reason == "summary node uses dependency outputs directly"
assert provider.calls == []
def test_resolver_exact_binds_use_skill_before_dynamic_lookup(tmp_path: Path) -> None:
_publish_skill(tmp_path, skill_name="official-source-research")
provider = RecordingProvider(['["wrong-dynamic-skill"]'])
resolver = TaskSkillResolver(
skills_loader=SkillsLoader(tmp_path),
draft_service=DraftService(SkillSpecStore(tmp_path)),
)
graph = ExecutionGraph(
strategy="sequence",
nodes=[
ExecutionNode(
"collect",
"Collect official sources",
AgentDescriptor(
name="collect",
metadata={
"use_skill": "official-source-research",
"skill_query": "generic web research",
},
),
)
],
)
resolved, reports = asyncio.run(
resolver.resolve_graph(
graph,
task=_task(),
user_message="collect sources",
attempt_index=1,
provider_bundle=_bundle(provider),
)
)
node = resolved.nodes[0]
assert node.inherited_pinned_skills == ["official-source-research"]
assert [context.name for context in node.inherited_pinned_skill_contexts] == ["official-source-research"]
assert node.agent.metadata["exact_binding_used"] is True
assert reports[0].selected_skill_names == ["official-source-research"]
assert reports[0].exact_binding_used is True
assert reports[0].warnings == []
assert provider.calls == []
def test_resolver_falls_back_to_skill_query_when_use_skill_missing(tmp_path: Path) -> None:
_publish_skill(tmp_path, skill_name="financial-metric-extraction")
provider = RecordingProvider(['["financial-metric-extraction"]'])
resolver = TaskSkillResolver(
skills_loader=SkillsLoader(tmp_path),
draft_service=DraftService(SkillSpecStore(tmp_path)),
)
graph = ExecutionGraph(
strategy="sequence",
nodes=[
ExecutionNode(
"extract",
"Extract metrics",
AgentDescriptor(
name="extract",
metadata={
"use_skill": "missing-exact-skill",
"skill_query": "financial metric extraction",
},
),
)
],
)
resolved, reports = asyncio.run(
resolver.resolve_graph(
graph,
task=_task(),
user_message="extract financial metrics",
attempt_index=1,
provider_bundle=_bundle(provider),
)
)
assert resolved.nodes[0].inherited_pinned_skills == ["financial-metric-extraction"]
assert reports[0].exact_binding_used is False
assert reports[0].selected_skill_names == ["financial-metric-extraction"]
assert reports[0].warnings == ["use_skill unresolved: missing-exact-skill"]
assert "financial metric extraction" in provider.calls[0][1]["content"]
def test_resolver_falls_back_to_ephemeral_when_exact_and_query_miss(tmp_path: Path) -> None:
_publish_skill(tmp_path, skill_name="unrelated-skill")
provider = RecordingProvider(
[
"[]",
"""
{
"guidance_name": "financial-extraction-guidance",
"description": "Extract financial metrics",
"content": "# Financial Extraction\\n\\nExtract the requested metrics.",
"tags": ["finance"]
}
""",
]
)
resolver = TaskSkillResolver(
skills_loader=SkillsLoader(tmp_path),
draft_service=DraftService(SkillSpecStore(tmp_path)),
missing_skill_synthesizer=EphemeralGuidanceSynthesizer(),
)
graph = ExecutionGraph(
strategy="sequence",
nodes=[
ExecutionNode(
"extract",
"Extract metrics",
AgentDescriptor(
name="extract",
metadata={
"use_skill": "missing-exact-skill",
"skill_query": "financial metric extraction",
},
),
)
],
)
resolved, reports = asyncio.run(
resolver.resolve_graph(
graph,
task=_task(),
user_message="extract financial metrics",
attempt_index=1,
provider_bundle=_bundle(provider),
)
)
assert resolved.nodes[0].inherited_pinned_skills == []
assert resolved.nodes[0].inherited_pinned_skill_contexts[0].name == "ephemeral:financial-extraction-guidance"
assert reports[0].ephemeral_used is True
assert reports[0].warnings == ["use_skill unresolved: missing-exact-skill"]
def test_explicit_use_skill_is_preserved_for_summary_without_nested_expansion(tmp_path: Path) -> None:
_publish_skill(tmp_path, skill_name="summary-formatting")
provider = RecordingProvider([])
resolver = TaskSkillResolver(
skills_loader=SkillsLoader(tmp_path),
draft_service=DraftService(SkillSpecStore(tmp_path)),
)
graph = ExecutionGraph(
strategy="dag",
nodes=[
ExecutionNode(
"summarize",
"Compile a summary from dependency outputs",
AgentDescriptor(
name="summarize",
metadata={"use_skill": "summary-formatting", "skill_query": "Summarization"},
),
depends_on=["collect"],
)
],
)
resolved, reports = asyncio.run(
resolver.resolve_graph(
graph,
task=_task(),
user_message="summarize",
attempt_index=1,
provider_bundle=_bundle(provider),
)
)
assert len(resolved.nodes) == 1
assert resolved.nodes[0].inherited_pinned_skills == ["summary-formatting"]
assert reports[0].exact_binding_used is True
assert provider.calls == []