From 8109d01992d3c39df5b4d4bbad05aa13ecb922bf Mon Sep 17 00:00:00 2001 From: Daniel Maddern Date: Sat, 15 Aug 2026 17:06:56 +0700 Subject: [PATCH] Delegate task execution to native graph runtime --- .../migrations/0003_commitrecord_graph_run.py | 19 ++++++++++++ control_plane/projects/models.py | 3 ++ graph/bootstrap.py | 29 +++++++++++++++++++ graph/task_nodes.py | 3 ++ runtime_loop/autonomous_task_loop.py | 17 +++++++++++ tests/test_m2_autonomous_loop.py | 2 ++ 6 files changed, 73 insertions(+) create mode 100644 control_plane/projects/migrations/0003_commitrecord_graph_run.py create mode 100644 graph/bootstrap.py diff --git a/control_plane/projects/migrations/0003_commitrecord_graph_run.py b/control_plane/projects/migrations/0003_commitrecord_graph_run.py new file mode 100644 index 0000000..444ee45 --- /dev/null +++ b/control_plane/projects/migrations/0003_commitrecord_graph_run.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("graph", "0002_graphnoderun_visit_index"), + ("projects", "0002_commitrecord_coder_commitrecord_judge_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="commitrecord", + name="graph_run", + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="commits", to="graph.graphrun"), + ), + ] diff --git a/control_plane/projects/models.py b/control_plane/projects/models.py index 27b80f4..3deb747 100644 --- a/control_plane/projects/models.py +++ b/control_plane/projects/models.py @@ -173,6 +173,9 @@ class CommitRecord(TimestampedModel): verification = models.ForeignKey( "verification.Verification", on_delete=models.SET_NULL, null=True, blank=True, related_name="commits" ) + graph_run = models.ForeignKey( + "graph.GraphRun", on_delete=models.SET_NULL, null=True, blank=True, related_name="commits" + ) sha = models.CharField(max_length=64) branch_name = models.CharField(max_length=255) message = models.TextField() diff --git a/graph/bootstrap.py b/graph/bootstrap.py new file mode 100644 index 0000000..dfa7b2e --- /dev/null +++ b/graph/bootstrap.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from django.utils import timezone + +from graph.models import ExecutionGraphDefinition, ExecutionGraphVersion, ExecutionGraphVersionStatus +from graph.task_execution import task_execution_graph_v1 + + +def champion_task_execution_graph_v1() -> ExecutionGraphVersion: + spec = task_execution_graph_v1() + definition, _ = ExecutionGraphDefinition.objects.get_or_create( + name=spec.name, + defaults={"graph_type": spec.graph_type, "description": str(spec.metadata.get("description", ""))}, + ) + version, created = ExecutionGraphVersion.objects.get_or_create( + graph=definition, + version=spec.version, + defaults={ + "status": ExecutionGraphVersionStatus.CHAMPION, + "graph_spec": spec.to_dict(), + "metadata": {"immutable_after_use": True}, + "promoted_at": timezone.now(), + }, + ) + if not created and version.status != ExecutionGraphVersionStatus.CHAMPION: + version.status = ExecutionGraphVersionStatus.CHAMPION + version.promoted_at = timezone.now() + version.save(update_fields=["status", "promoted_at"]) + return version diff --git a/graph/task_nodes.py b/graph/task_nodes.py index efd0e0e..ece3049 100644 --- a/graph/task_nodes.py +++ b/graph/task_nodes.py @@ -130,6 +130,8 @@ class BuildContextNode(TaskNode): attempt.save(update_fields=["context_snapshot", "updated_at"]) metadata["current_attempt_id"] = str(attempt.id) self.save_metadata(context, metadata) + context.graph_run.task_attempt = attempt + context.graph_run.save(update_fields=["task_attempt", "updated_at"]) return NodeResult("COMPLETE", "success", {"attempt_id": str(attempt.id), "attempt_number": attempt.attempt_number}) @@ -306,6 +308,7 @@ class CommitNode(TaskNode): test_run=test_run, review_id=metadata.get("review_id"), verification_id=metadata.get("verification_id"), + graph_run=context.graph_run, sha=sha, branch_name=worktree.branch_name, message=f"Artifex task: {task.goal[:80]}", diff --git a/runtime_loop/autonomous_task_loop.py b/runtime_loop/autonomous_task_loop.py index e25e19f..c5f3fbc 100644 --- a/runtime_loop/autonomous_task_loop.py +++ b/runtime_loop/autonomous_task_loop.py @@ -10,7 +10,11 @@ from control_plane.events.bus import EventBus from control_plane.events.models import EventType from control_plane.projects.models import CommitRecord, Task, TaskAttempt, TaskStatus, Worktree from control_plane.verification.models import VerificationResult +from graph.bootstrap import champion_task_execution_graph_v1 +from graph.models import GraphRun +from graph.native_runtime import NativeGraphRuntime from graph.scheduler import TaskScheduler +from graph.task_nodes import TaskExecutionServices, task_execution_registry from knowledge.context_builder import WorkerContextBuilder from model_router.router import ModelRouter from tools.capabilities import Capability @@ -45,6 +49,19 @@ class AutonomousTaskLoop: return task def _execute_task(self, task: Task, test_command: list[str]) -> None: + graph_version = champion_task_execution_graph_v1() + graph_run = GraphRun.objects.create( + execution_graph_version=graph_version, + project=task.project, + milestone=task.milestone, + feature=task.feature, + task=task, + current_node=graph_version.graph_spec["entry"], + ) + services = TaskExecutionServices(self.router, bus=self.bus, test_command=test_command) + NativeGraphRuntime(task_execution_registry(services), bus=self.bus).run_until_terminal_or_paused(graph_run) + return + coder_version = self._champion(AgentRole.CODER) reviewer_version = self._champion(AgentRole.REVIEWER) judge_version = self._champion(AgentRole.PROJECT_JUDGE) diff --git a/tests/test_m2_autonomous_loop.py b/tests/test_m2_autonomous_loop.py index d5bfd83..2b2cf64 100644 --- a/tests/test_m2_autonomous_loop.py +++ b/tests/test_m2_autonomous_loop.py @@ -85,6 +85,8 @@ def assert_accepted_trace(task: Task) -> CommitRecord: assert review.status == "PASS" assert verification.result == VerificationResult.PASS assert commit.sha + assert commit.graph_run_id is not None + assert commit.graph_run.execution_graph_version.graph.name == "task_execution" assert commit.coder.agent.role == "CODER" assert commit.reviewer.agent.role == "REVIEWER" assert commit.judge.agent.role == "PROJECT_JUDGE"