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

@ -4,10 +4,12 @@ import asyncio
from pathlib import Path
from types import SimpleNamespace
from beaver.engine import EngineLoader
from beaver.engine import AgentRunResult, EngineLoader
from beaver.engine.context import SkillContext
from beaver.engine.providers.base import LLMProvider, LLMResponse
from beaver.engine.providers.factory import ProviderBundle
from beaver.services.agent_service import AgentService
from beaver.skills.assembler import SkillAssemblyResult
from beaver.tasks import TaskExecutionPlan, TaskService
@ -39,6 +41,44 @@ class StubTaskExecutionPlanner:
return TaskExecutionPlan.single("test-single")
class RecordingTaskExecutionPlanner:
def __init__(self) -> None:
self.calls: list[dict] = []
async def plan(self, **kwargs) -> TaskExecutionPlan:
self.calls.append(dict(kwargs))
return TaskExecutionPlan.single("test-single")
class RecordingSkillAssembler:
def __init__(self, skills: list[SkillContext]) -> None:
self.skills = list(skills)
self.calls: list[dict] = []
async def assemble(self, **kwargs) -> SkillAssemblyResult:
self.calls.append(dict(kwargs))
return SkillAssemblyResult(activated_skills=list(self.skills))
class RecordingTaskAttemptOrchestrator:
def __init__(self) -> None:
self.calls: list[dict] = []
async def run(self, **kwargs) -> AgentRunResult:
self.calls.append(dict(kwargs))
task = kwargs["task"]
task.task_id = "task-from-orchestrator"
return AgentRunResult(
session_id=kwargs["kwargs"]["session_id"],
run_id="run-from-orchestrator",
output_text="orchestrated",
finish_reason="stop",
tool_iterations=0,
task_id=task.task_id,
task_status=task.status,
)
class FakeLearningCandidate:
def to_dict(self) -> dict:
return {"candidate_id": "candidate-1", "kind": "new_skill", "status": "open"}
@ -101,6 +141,91 @@ def test_task_run_records_evidence_and_waits_for_acceptance(tmp_path: Path) -> N
assert "validated" not in event_types
def test_agent_service_records_router_latency(tmp_path: Path) -> None:
service = AgentService(
loader=EngineLoader(
workspace=tmp_path,
task_execution_planner=StubTaskExecutionPlanner(),
)
)
result = asyncio.run(
service.process_direct(
"draft release notes",
session_id="web:latency",
provider_bundle=_bundle("Done"),
)
)
latency = result.usage["latency_ms"]
assert latency["router_ms"] > 0
def test_task_mode_preselects_skills_for_planner_and_reuses_them_in_main_run(tmp_path: Path) -> None:
skill = SkillContext(
name="docker-debug",
content="Use docker logs before editing config.",
version="v1",
content_hash="hash-v1",
activation_reason="llm_selected",
tool_hints=["terminal"],
)
skill_assembler = RecordingSkillAssembler([skill])
planner = RecordingTaskExecutionPlanner()
service = AgentService(
loader=EngineLoader(
workspace=tmp_path,
skill_assembler=skill_assembler,
task_execution_planner=planner,
)
)
result = asyncio.run(
service.process_direct(
"debug this workflow",
session_id="web:skill-aware-task",
provider_bundle=_bundle("Done"),
)
)
assert result.task_id
assert len(skill_assembler.calls) == 1
assert planner.calls
assert planner.calls[0]["skill_summaries"] == ["docker-debug: Use docker logs before editing config."]
assert planner.calls[0]["tool_hints"] == ["terminal"]
task_service = service.create_loop().boot().task_service
assert task_service is not None
task = task_service.get_task(result.task_id)
assert task is not None
assert task.skill_names == ["docker-debug"]
def test_task_mode_delegates_attempt_execution_to_orchestrator(tmp_path: Path) -> None:
orchestrator = RecordingTaskAttemptOrchestrator()
service = AgentService(
loader=EngineLoader(
workspace=tmp_path,
task_execution_planner=StubTaskExecutionPlanner(),
)
)
service._build_task_attempt_orchestrator = lambda loaded: orchestrator # type: ignore[attr-defined]
result = asyncio.run(
service.process_direct(
"draft release notes",
session_id="web:orchestrator",
provider_bundle=_bundle("main runner should not be used"),
)
)
assert result.output_text == "orchestrated"
assert result.run_id == "run-from-orchestrator"
assert len(orchestrator.calls) == 1
assert orchestrator.calls[0]["message"] == "draft release notes"
assert orchestrator.calls[0]["task"].description == "draft release notes"
def test_task_mode_injects_prompt_locale_output_language(tmp_path: Path) -> None:
service = AgentService(
loader=EngineLoader(