Artifex/control_plane/agents/models.py

325 lines
18 KiB
Python
Raw Normal View History

2026-08-15 13:50:24 +07:00
from __future__ import annotations
import uuid
2026-08-15 18:14:21 +07:00
from django.core.exceptions import ValidationError
2026-08-15 13:50:24 +07:00
from django.db import models
from control_plane.common import TimestampedModel
class AgentRole(models.TextChoices):
PROJECT_ARCHAEOLOGIST = "PROJECT_ARCHAEOLOGIST"
PLANNER = "PLANNER"
CODER = "CODER"
REVIEWER = "REVIEWER"
PROJECT_JUDGE = "PROJECT_JUDGE"
2026-08-15 20:03:59 +07:00
FRONTEND_DESIGNER = "FRONTEND_DESIGNER"
FRONTEND_ENGINEER = "FRONTEND_ENGINEER"
UX_ACCESSIBILITY_REVIEWER = "UX_ACCESSIBILITY_REVIEWER"
VISUAL_JUDGE = "VISUAL_JUDGE"
2026-08-15 13:50:24 +07:00
class PromotionStatus(models.TextChoices):
2026-08-15 20:03:59 +07:00
DRAFT = "DRAFT"
2026-08-15 13:50:24 +07:00
CANDIDATE = "CANDIDATE"
CHALLENGER = "CHALLENGER"
CHAMPION = "CHAMPION"
REJECTED = "REJECTED"
2026-08-15 20:03:59 +07:00
RETIRED = "RETIRED"
DISABLED = "DISABLED"
class AgentScope(models.TextChoices):
GLOBAL = "GLOBAL"
STUDIO = "STUDIO"
PROJECT = "PROJECT"
2026-08-15 13:50:24 +07:00
class Agent(TimestampedModel):
name = models.CharField(max_length=200, unique=True)
role = models.CharField(max_length=80, choices=AgentRole.choices)
description = models.TextField(blank=True)
2026-08-15 20:03:59 +07:00
purpose = models.TextField(blank=True)
scope = models.CharField(max_length=32, choices=AgentScope.choices, default=AgentScope.GLOBAL)
studio = models.CharField(max_length=120, blank=True)
project = models.ForeignKey("projects.Project", on_delete=models.SET_NULL, null=True, blank=True, related_name="scoped_agents")
status = models.CharField(max_length=32, default="ACTIVE")
metadata = models.JSONField(default=dict, blank=True)
2026-08-15 13:50:24 +07:00
champion_version = models.ForeignKey(
"AgentVersion", on_delete=models.SET_NULL, null=True, blank=True, related_name="championed_by"
)
class AgentVersion(TimestampedModel):
agent = models.ForeignKey(Agent, on_delete=models.CASCADE, related_name="versions")
version = models.PositiveIntegerField()
model = models.CharField(max_length=120)
2026-08-15 20:03:59 +07:00
model_config = models.JSONField(default=dict, blank=True)
2026-08-15 13:50:24 +07:00
system_contract = models.TextField()
capabilities = models.JSONField(default=list, blank=True)
tools = models.JSONField(default=list, blank=True)
permissions = models.JSONField(default=dict, blank=True)
context_policy = models.JSONField(default=dict, blank=True)
2026-08-15 20:03:59 +07:00
retrieval_policy = models.JSONField(default=dict, blank=True)
tool_policy = models.JSONField(default=dict, blank=True)
2026-08-15 13:50:24 +07:00
workflow = models.JSONField(default=dict, blank=True)
retry_policy = models.JSONField(default=dict, blank=True)
2026-08-15 20:03:59 +07:00
resource_preferences = models.JSONField(default=dict, blank=True)
graph_usage_policy = models.JSONField(default=dict, blank=True)
scope = models.CharField(max_length=32, choices=AgentScope.choices, default=AgentScope.GLOBAL)
parent_version = models.ForeignKey("self", on_delete=models.SET_NULL, null=True, blank=True, related_name="children")
ancestry = models.JSONField(default=list, blank=True)
creation_source = models.CharField(max_length=80, default="seed")
2026-08-15 13:50:24 +07:00
evaluator = models.JSONField(default=dict, blank=True)
benchmark_status = models.CharField(max_length=32, default="UNBENCHMARKED")
promotion_status = models.CharField(max_length=32, choices=PromotionStatus.choices, default=PromotionStatus.CANDIDATE)
2026-08-15 20:03:59 +07:00
status = models.CharField(max_length=32, default="ACTIVE")
immutable_since = models.DateTimeField(null=True, blank=True)
metadata = models.JSONField(default=dict, blank=True)
2026-08-15 13:50:24 +07:00
class Meta:
constraints = [models.UniqueConstraint(fields=["agent", "version"], name="unique_agent_version")]
2026-08-15 20:03:59 +07:00
def save(self, *args: object, **kwargs: object) -> None:
if self.promotion_status == PromotionStatus.CHAMPION:
existing = AgentVersion.objects.filter(agent=self.agent, promotion_status=PromotionStatus.CHAMPION).exclude(pk=self.pk)
if existing.exists():
raise ValidationError("Only one champion AgentVersion is allowed per Agent.")
super().save(*args, **kwargs)
2026-08-15 13:50:24 +07:00
class AgentPlan(TimestampedModel):
agent = models.ForeignKey(Agent, on_delete=models.CASCADE, related_name="plans", null=True, blank=True)
name = models.CharField(max_length=200)
role = models.CharField(max_length=80, choices=AgentRole.choices)
model = models.CharField(max_length=120)
system_contract = models.TextField()
capabilities = models.JSONField(default=list, blank=True)
tools = models.JSONField(default=list, blank=True)
permissions = models.JSONField(default=dict, blank=True)
context_policy = models.JSONField(default=dict, blank=True)
workflow = models.JSONField(default=dict, blank=True)
benchmarks = models.JSONField(default=list, blank=True)
success_criteria = models.JSONField(default=dict, blank=True)
2026-08-15 20:03:59 +07:00
target_agent_version = models.ForeignKey(AgentVersion, on_delete=models.SET_NULL, null=True, blank=True, related_name="plans_targeting_version")
plan_type = models.CharField(max_length=32, default="CREATE")
scope = models.CharField(max_length=32, choices=AgentScope.choices, default=AgentScope.GLOBAL)
evidence = models.JSONField(default=dict, blank=True)
status = models.CharField(max_length=32, default="DRAFT")
2026-08-15 13:50:24 +07:00
created_by = models.CharField(max_length=120, default="sol")
2026-08-15 20:03:59 +07:00
class Competency(TimestampedModel):
key = models.CharField(max_length=120, unique=True)
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
domain = models.CharField(max_length=120, blank=True)
metadata = models.JSONField(default=dict, blank=True)
class AgentCompetency(TimestampedModel):
agent = models.ForeignKey(Agent, on_delete=models.CASCADE, related_name="competencies", null=True, blank=True)
agent_version = models.ForeignKey(AgentVersion, on_delete=models.CASCADE, related_name="competencies", null=True, blank=True)
competency = models.ForeignKey(Competency, on_delete=models.CASCADE, related_name="agent_assignments")
proficiency = models.FloatField(default=0.5)
confidence = models.FloatField(default=0.5)
evidence = models.JSONField(default=dict, blank=True)
source = models.CharField(max_length=80, default="manual")
last_evaluated_at = models.DateTimeField(null=True, blank=True)
class AgentTeam(TimestampedModel):
name = models.CharField(max_length=200)
purpose = models.TextField(blank=True)
scope = models.CharField(max_length=32, choices=AgentScope.choices, default=AgentScope.GLOBAL)
status = models.CharField(max_length=32, default="ACTIVE")
execution_graph_version = models.ForeignKey("graph.ExecutionGraphVersion", on_delete=models.SET_NULL, null=True, blank=True, related_name="agent_teams")
metadata = models.JSONField(default=dict, blank=True)
class AgentTeamMember(TimestampedModel):
team = models.ForeignKey(AgentTeam, on_delete=models.CASCADE, related_name="members")
agent_version = models.ForeignKey(AgentVersion, on_delete=models.CASCADE, related_name="team_memberships")
role = models.CharField(max_length=120)
responsibilities = models.JSONField(default=list, blank=True)
status = models.CharField(max_length=32, default="ACTIVE")
metadata = models.JSONField(default=dict, blank=True)
class Meta:
constraints = [models.UniqueConstraint(fields=["team", "agent_version", "role"], name="unique_agent_team_member_role")]
2026-08-15 13:50:24 +07:00
class AgentRun(TimestampedModel):
agent_version = models.ForeignKey(AgentVersion, on_delete=models.PROTECT, related_name="runs")
project = models.ForeignKey("projects.Project", on_delete=models.CASCADE, null=True, blank=True)
task = models.ForeignKey("projects.Task", on_delete=models.SET_NULL, null=True, blank=True)
status = models.CharField(max_length=32, default="QUEUED")
input_contract = models.JSONField(default=dict, blank=True)
output_contract = models.JSONField(default=dict, blank=True)
metrics = models.JSONField(default=dict, blank=True)
class BenchmarkRun(TimestampedModel):
champion = models.ForeignKey(AgentVersion, on_delete=models.PROTECT, related_name="champion_benchmarks")
challenger = models.ForeignKey(AgentVersion, on_delete=models.PROTECT, related_name="challenger_benchmarks")
benchmark_set = models.JSONField(default=list, blank=True)
metrics = models.JSONField(default=dict, blank=True)
decision = models.CharField(max_length=32, default="PENDING")
class ProgenySignal(TimestampedModel):
project = models.ForeignKey("projects.Project", on_delete=models.SET_NULL, null=True, blank=True)
task = models.ForeignKey("projects.Task", on_delete=models.SET_NULL, null=True, blank=True)
milestone = models.ForeignKey("projects.Milestone", on_delete=models.SET_NULL, null=True, blank=True)
agent_version = models.ForeignKey(AgentVersion, on_delete=models.SET_NULL, null=True, blank=True)
graph_run = models.ForeignKey("graph.GraphRun", on_delete=models.SET_NULL, null=True, blank=True)
graph_node_run = models.ForeignKey("graph.GraphNodeRun", on_delete=models.SET_NULL, null=True, blank=True)
execution_graph_version = models.ForeignKey("graph.ExecutionGraphVersion", on_delete=models.SET_NULL, null=True, blank=True)
source = models.CharField(max_length=80)
severity = models.CharField(max_length=32, default="INFO")
failure_category = models.CharField(max_length=80, blank=True)
summary = models.TextField()
evidence = models.JSONField(default=dict, blank=True)
status = models.CharField(max_length=32, default="OPEN")
grouping_key = models.CharField(max_length=120, blank=True)
model = models.CharField(max_length=120, blank=True)
2026-08-15 17:53:52 +07:00
class ProgenyInvestigation(TimestampedModel):
status = models.CharField(max_length=32, default="OPEN")
signals = models.ManyToManyField(ProgenySignal, related_name="investigations", blank=True)
signal_clusters = models.JSONField(default=list, blank=True)
affected_projects = models.JSONField(default=list, blank=True)
affected_agents = models.JSONField(default=list, blank=True)
affected_graph_versions = models.JSONField(default=list, blank=True)
affected_nodes = models.JSONField(default=list, blank=True)
hypotheses = models.JSONField(default=list, blank=True)
recommended_target = models.CharField(max_length=80)
confidence = models.FloatField(default=0.0)
recommended_route = models.CharField(max_length=120)
proposed_experiments = models.JSONField(default=list, blank=True)
expected_impact = models.TextField(blank=True)
estimated_cost = models.CharField(max_length=80, blank=True)
class ImprovementCandidate(TimestampedModel):
status = models.CharField(max_length=32, default="PROPOSED")
target_type = models.CharField(max_length=80)
target_id = models.CharField(max_length=120, blank=True)
target_label = models.CharField(max_length=240, blank=True)
hypothesis = models.TextField()
recommended_route = models.CharField(max_length=120, blank=True)
evidence = models.JSONField(default=dict, blank=True)
investigation = models.ForeignKey(ProgenyInvestigation, on_delete=models.SET_NULL, null=True, blank=True, related_name="improvement_candidates")
execution_graph_version = models.ForeignKey("graph.ExecutionGraphVersion", on_delete=models.SET_NULL, null=True, blank=True, related_name="improvement_candidates")
agent_version = models.ForeignKey(AgentVersion, on_delete=models.SET_NULL, null=True, blank=True, related_name="improvement_candidates")
2026-08-15 18:14:21 +07:00
class ReplayDataset(TimestampedModel):
name = models.CharField(max_length=200)
description = models.TextField(blank=True)
version = models.PositiveIntegerField(default=1)
status = models.CharField(max_length=32, default="DRAFT")
selection_criteria = models.JSONField(default=dict, blank=True)
frozen_at = models.DateTimeField(null=True, blank=True)
metadata = models.JSONField(default=dict, blank=True)
class Meta:
constraints = [models.UniqueConstraint(fields=["name", "version"], name="unique_replay_dataset_version")]
class ReplayCase(TimestampedModel):
replay_dataset = models.ForeignKey(ReplayDataset, on_delete=models.CASCADE, related_name="cases")
source_project = models.ForeignKey("projects.Project", on_delete=models.SET_NULL, null=True, blank=True, related_name="replay_cases")
source_task = models.ForeignKey("projects.Task", on_delete=models.SET_NULL, null=True, blank=True, related_name="replay_cases")
source_task_attempt = models.ForeignKey("projects.TaskAttempt", on_delete=models.SET_NULL, null=True, blank=True, related_name="replay_cases")
source_graph_run = models.ForeignKey("graph.GraphRun", on_delete=models.SET_NULL, null=True, blank=True, related_name="replay_cases")
task_type = models.CharField(max_length=80)
project_type = models.CharField(max_length=100, blank=True)
original_goal = models.TextField()
acceptance_criteria = models.JSONField(default=list, blank=True)
repository_path = models.TextField(blank=True)
repository_baseline_ref = models.CharField(max_length=80)
expected_evaluator_inputs = models.JSONField(default=dict, blank=True)
failure_classification = models.CharField(max_length=120, blank=True)
selection_metadata = models.JSONField(default=dict, blank=True)
def save(self, *args: object, **kwargs: object) -> None:
if self.replay_dataset.status == "FROZEN":
raise ValidationError("Frozen replay dataset membership is immutable; create a new dataset version.")
super().save(*args, **kwargs)
class ProgenyExperiment(TimestampedModel):
investigation = models.ForeignKey(ProgenyInvestigation, on_delete=models.SET_NULL, null=True, blank=True, related_name="experiments")
improvement_candidate = models.ForeignKey(ImprovementCandidate, on_delete=models.SET_NULL, null=True, blank=True, related_name="experiments")
target_type = models.CharField(max_length=80)
target_identifier = models.CharField(max_length=240, blank=True)
replay_dataset = models.ForeignKey(ReplayDataset, on_delete=models.PROTECT, related_name="experiments")
hypothesis = models.TextField()
success_criteria = models.JSONField(default=dict, blank=True)
status = models.CharField(max_length=32, default="DRAFT")
started_at = models.DateTimeField(null=True, blank=True)
completed_at = models.DateTimeField(null=True, blank=True)
metadata = models.JSONField(default=dict, blank=True)
class ExperimentVariant(TimestampedModel):
experiment = models.ForeignKey(ProgenyExperiment, on_delete=models.CASCADE, related_name="variants")
role = models.CharField(max_length=32)
target_type = models.CharField(max_length=80)
target_reference = models.CharField(max_length=240)
execution_graph_version = models.ForeignKey("graph.ExecutionGraphVersion", on_delete=models.SET_NULL, null=True, blank=True, related_name="experiment_variants")
agent_version = models.ForeignKey(AgentVersion, on_delete=models.SET_NULL, null=True, blank=True, related_name="experiment_variants")
configuration_snapshot = models.JSONField(default=dict)
metadata = models.JSONField(default=dict, blank=True)
def save(self, *args: object, **kwargs: object) -> None:
if not self._state.adding:
previous = ExperimentVariant.objects.get(pk=self.pk)
if previous.configuration_snapshot != self.configuration_snapshot or previous.target_reference != self.target_reference:
raise ValidationError("Experiment variant configuration is immutable; create a new variant.")
super().save(*args, **kwargs)
class ReplayRun(TimestampedModel):
experiment = models.ForeignKey(ProgenyExperiment, on_delete=models.CASCADE, related_name="replay_runs")
variant = models.ForeignKey(ExperimentVariant, on_delete=models.CASCADE, related_name="replay_runs")
replay_case = models.ForeignKey(ReplayCase, on_delete=models.CASCADE, related_name="replay_runs")
graph_run = models.ForeignKey("graph.GraphRun", on_delete=models.SET_NULL, null=True, blank=True, related_name="replay_runs")
replay_task = models.ForeignKey("projects.Task", on_delete=models.SET_NULL, null=True, blank=True, related_name="replay_runs")
started_at = models.DateTimeField(null=True, blank=True)
completed_at = models.DateTimeField(null=True, blank=True)
status = models.CharField(max_length=32, default="PENDING")
failure_classification = models.CharField(max_length=80, blank=True)
commit_candidate_sha = models.CharField(max_length=64, blank=True)
telemetry = models.JSONField(default=dict, blank=True)
failure_evidence = models.JSONField(default=dict, blank=True)
metadata = models.JSONField(default=dict, blank=True)
class ReplayResult(TimestampedModel):
replay_run = models.OneToOneField(ReplayRun, on_delete=models.CASCADE, related_name="result")
completion_status = models.CharField(max_length=32)
tests_status = models.CharField(max_length=32, blank=True)
reviewer_status = models.CharField(max_length=32, blank=True)
judge_status = models.CharField(max_length=32, blank=True)
accepted_candidate = models.BooleanField(default=False)
metrics = models.JSONField(default=dict, blank=True)
safety = models.JSONField(default=dict, blank=True)
evidence = models.JSONField(default=dict, blank=True)
class ExperimentComparison(TimestampedModel):
experiment = models.OneToOneField(ProgenyExperiment, on_delete=models.CASCADE, related_name="comparison")
champion_variant = models.ForeignKey(ExperimentVariant, on_delete=models.SET_NULL, null=True, blank=True, related_name="champion_comparisons")
challenger_variant = models.ForeignKey(ExperimentVariant, on_delete=models.SET_NULL, null=True, blank=True, related_name="challenger_comparisons")
aggregate_metrics = models.JSONField(default=dict, blank=True)
paired_outcomes = models.JSONField(default=dict, blank=True)
regression_cases = models.JSONField(default=list, blank=True)
verdict = models.CharField(max_length=80, default="INCONCLUSIVE")
reasons = models.JSONField(default=list, blank=True)
approved_at = models.DateTimeField(null=True, blank=True)
rejected_at = models.DateTimeField(null=True, blank=True)
decided_by = models.CharField(max_length=120, blank=True)