feat(tasks): add skill-templated task graph execution
This commit is contained in:
@ -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 == []
|
||||
|
||||
Reference in New Issue
Block a user