Add execution engine and task lifecycle services
This commit is contained in:
@@ -28,10 +28,28 @@ def test_post_tasks_creates_planned_task() -> None:
|
||||
payload = response.json()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert payload["status"] == "planned"
|
||||
assert payload["status"] == "running"
|
||||
assert payload["progress"]["total"] == 2
|
||||
|
||||
|
||||
def test_cancel_endpoint_marks_task_cancelled() -> None:
|
||||
client = TestClient(app)
|
||||
|
||||
created = client.post(
|
||||
"/tasks",
|
||||
json={
|
||||
"project_id": "default",
|
||||
"goal": "Cancel me",
|
||||
"inputs": {},
|
||||
"execution_mode": "agent_graph",
|
||||
},
|
||||
).json()
|
||||
response = client.post(f"/tasks/{created['task_id']}/cancel", json={"reason": "stop"})
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "cancelled"
|
||||
|
||||
|
||||
def test_worker_registration_is_exposed_via_list_endpoint() -> None:
|
||||
client = TestClient(app)
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
from ai_orchestrator.application.services.execution import (
|
||||
DefaultFinalizer,
|
||||
FinalizerNodeRunner,
|
||||
GraphExecutionEngine,
|
||||
NoOpReviewer,
|
||||
PlannerNodeRunner,
|
||||
RunnerRegistry,
|
||||
)
|
||||
from ai_orchestrator.domain.enums import NodeStatus, NodeType, TaskStatus
|
||||
from ai_orchestrator.domain.models import ExecutionGraph, ExecutionNode, Task
|
||||
from ai_orchestrator.infrastructure.storage.memory import InMemoryEventStore
|
||||
|
||||
|
||||
def test_execution_engine_runs_ready_nodes_and_records_events() -> None:
|
||||
task = Task(project_id="default", goal="Run planner", inputs={})
|
||||
planner = ExecutionNode(task_id=task.task_id, node_type=NodeType.PLANNER, input_data={})
|
||||
finalizer = ExecutionNode(
|
||||
task_id=task.task_id,
|
||||
node_type=NodeType.FINALIZER,
|
||||
input_data={},
|
||||
dependencies=[planner.node_id],
|
||||
)
|
||||
graph = ExecutionGraph(task_id=task.task_id, nodes=[planner, finalizer])
|
||||
event_store = InMemoryEventStore()
|
||||
registry = RunnerRegistry()
|
||||
registry.register(PlannerNodeRunner())
|
||||
registry.register(FinalizerNodeRunner(finalizer=DefaultFinalizer()))
|
||||
engine = GraphExecutionEngine(
|
||||
event_store=event_store,
|
||||
runner_registry=registry,
|
||||
reviewer=NoOpReviewer(),
|
||||
)
|
||||
|
||||
first_pass = engine.execute_ready_nodes(task=task, graph=graph)
|
||||
second_pass = engine.execute_ready_nodes(task=task, graph=graph)
|
||||
|
||||
assert len(first_pass) == 1
|
||||
assert len(second_pass) == 1
|
||||
assert planner.status == NodeStatus.COMPLETED
|
||||
assert finalizer.status == NodeStatus.COMPLETED
|
||||
assert task.status == TaskStatus.RUNNING
|
||||
assert [event.event_type for event in event_store.items] == [
|
||||
"node_started",
|
||||
"node_completed",
|
||||
"node_started",
|
||||
"node_completed",
|
||||
]
|
||||
@@ -0,0 +1,76 @@
|
||||
from ai_orchestrator.application.services.lifecycle import TaskLifecycleService
|
||||
from ai_orchestrator.domain.enums import NodeStatus, NodeType, PolicyDecisionType, TaskStatus
|
||||
from ai_orchestrator.domain.models import (
|
||||
ConfirmationRequest,
|
||||
ExecutionGraph,
|
||||
ExecutionNode,
|
||||
PolicyDecision,
|
||||
Task,
|
||||
)
|
||||
from ai_orchestrator.infrastructure.storage.memory import (
|
||||
InMemoryConfirmationRepository,
|
||||
InMemoryEventStore,
|
||||
InMemoryGraphRepository,
|
||||
InMemoryTaskRepository,
|
||||
)
|
||||
|
||||
|
||||
def _build_service() -> tuple[
|
||||
TaskLifecycleService,
|
||||
InMemoryTaskRepository,
|
||||
InMemoryGraphRepository,
|
||||
InMemoryConfirmationRepository,
|
||||
]:
|
||||
task_repository = InMemoryTaskRepository()
|
||||
graph_repository = InMemoryGraphRepository()
|
||||
confirmation_repository = InMemoryConfirmationRepository()
|
||||
event_store = InMemoryEventStore()
|
||||
service = TaskLifecycleService(
|
||||
task_repository=task_repository,
|
||||
graph_repository=graph_repository,
|
||||
confirmation_repository=confirmation_repository,
|
||||
event_store=event_store,
|
||||
)
|
||||
return service, task_repository, graph_repository, confirmation_repository
|
||||
|
||||
|
||||
def test_approve_confirmation_returns_task_to_running() -> None:
|
||||
service, task_repository, graph_repository, confirmation_repository = _build_service()
|
||||
task = Task(project_id="default", goal="Approve", inputs={})
|
||||
task.wait_for_confirmation("node_1")
|
||||
task_repository.create(task)
|
||||
node = ExecutionNode(task_id=task.task_id, node_type=NodeType.TOOL_CALL, input_data={})
|
||||
node.node_id = "node_1"
|
||||
node.mark_waiting_confirmation()
|
||||
graph_repository.save(ExecutionGraph(task_id=task.task_id, nodes=[node]))
|
||||
confirmation = ConfirmationRequest(
|
||||
task_id=task.task_id,
|
||||
node_id=node.node_id,
|
||||
decision=PolicyDecision(
|
||||
decision=PolicyDecisionType.CONFIRM,
|
||||
reason="Need approval",
|
||||
requires_confirmation=True,
|
||||
),
|
||||
)
|
||||
confirmation_repository.create(confirmation)
|
||||
|
||||
updated = service.approve_confirmation(confirmation.confirmation_id, "ok")
|
||||
|
||||
assert updated.status.value == "approved"
|
||||
assert task_repository.get(task.task_id).status == TaskStatus.RUNNING
|
||||
assert graph_repository.get(task.task_id).nodes[0].status == NodeStatus.READY
|
||||
|
||||
|
||||
def test_cancel_task_marks_active_nodes_cancelled() -> None:
|
||||
service, task_repository, graph_repository, _ = _build_service()
|
||||
task = Task(project_id="default", goal="Cancel", inputs={})
|
||||
task.mark_running("node_1")
|
||||
task_repository.create(task)
|
||||
node = ExecutionNode(task_id=task.task_id, node_type=NodeType.PLANNER, input_data={})
|
||||
node.mark_running()
|
||||
graph_repository.save(ExecutionGraph(task_id=task.task_id, nodes=[node]))
|
||||
|
||||
cancelled = service.cancel_task(task.task_id, "stop")
|
||||
|
||||
assert cancelled.status == TaskStatus.CANCELLED
|
||||
assert graph_repository.get(task.task_id).nodes[0].status == NodeStatus.CANCELLED
|
||||
Reference in New Issue
Block a user