Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions backend/druks/build/journal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
from dataclasses import dataclass, field

from druks.build.contracts import (
EvaluationOutput,
HumanFeedback,
ImplementationOutput,
PlanData,
ReviewOutput,
)
from druks.build.enums import ReviewDecision


@dataclass
class PlanRecord:
plan: PlanData
reviews: list[ReviewOutput] = field(default_factory=list)


class BuildJournal:
"""The run's working memory, rebuilt on replay. Mutate only from workflow-body
code after a memoized call — never inside a @step, where replay skips the
write."""

def __init__(self) -> None:
self.plans: list[PlanRecord] = []
self.implementations: list[ImplementationOutput] = []
self.evaluations: list[EvaluationOutput] = []
self.human_feedback: list[HumanFeedback] = []

def add_plan(self, plan: PlanData) -> PlanData:
self.plans.append(PlanRecord(plan=plan))
return plan

def add_plan_review(self, review: ReviewOutput) -> ReviewOutput:
self.plans[-1].reviews.append(review)
return review

def add_implementation(self, implementation: ImplementationOutput) -> ImplementationOutput:
self.implementations.append(implementation)
return implementation

def add_evaluation(self, evaluation: EvaluationOutput) -> EvaluationOutput:
self.evaluations.append(evaluation)
return evaluation

def add_feedback(self, feedback: HumanFeedback) -> HumanFeedback:
self.human_feedback.append(feedback)
return feedback

@property
def plan(self) -> PlanData:
return self.plans[-1].plan if self.plans else PlanData()

@property
def last_implementation(self) -> ImplementationOutput | None:
return self.implementations[-1] if self.implementations else None

@property
def plan_revision(self) -> int:
return len(self.plans)

@property
def implementation_revision(self) -> int:
return len(self.implementations)

@property
def assignee_github_login(self) -> str | None:
for record in reversed(self.plans):
for review in reversed(record.reviews):
if review.assignee_github_login:
return review.assignee_github_login
return None

def reviewer_requirements(self) -> list[ReviewOutput]:
# Approve-with-required-changes verdicts on the current plan draft only —
# reviews of superseded drafts don't bind the implementer.
current = self.plans[-1].reviews if self.plans else []
return [
review
for review in current
if review.decision == ReviewDecision.APPROVE_WITH_REQUIRED_CHANGES
and review.body.strip()
]
23 changes: 4 additions & 19 deletions backend/druks/build/prompt_context.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
from dataclasses import dataclass

from druks.build.contracts import (
AcceptanceCriterionOutput,
EvaluationOutput,
HumanFeedback,
ReviewOutput,
)
from druks.build.journal import BuildJournal
from druks.build.models import ProjectRepo


@dataclass(frozen=True)
class BuildPromptContext:
"""What build's prompt templates render against — a flat snapshot of the run's
state, assembled once per agent call by ``BuildWorkflow.get_prompt_context``.
"""What build's prompt templates render against — the run's identity facts
plus its journal, assembled per agent call by ``BuildWorkflow.get_prompt_context``.
The templates read ``build.<field>`` and nothing else off the run, so this is
the whole contract between the workflow and its prompts; the workflow itself
stays free of template accessors.
Expand All @@ -27,15 +22,5 @@ class BuildPromptContext:
issue_number: int | None
task_owner_name: str | None
task_owner_email: str | None
# Where the run stands in its plan / implement loops.
plan_revision: int
implementation_revision: int
finalized_base_sha: str | None
finalized_pr_sha: str | None
# The current plan the downstream agents build on.
current_plan: str
acceptance_criteria: list[AcceptanceCriterionOutput]
reviewer_requirements: list[ReviewOutput]
implementation_reviews: list[EvaluationOutput]
human_feedback: list[HumanFeedback]
related_repos: list[ProjectRepo]
journal: BuildJournal
6 changes: 3 additions & 3 deletions backend/druks/build/subscribers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from druks.build.models import Project, ProjectRepo, WorkItem
from druks.build.policy import RepoPolicy
from druks.build.scoping.workflows import Scope
from druks.build.workflows import BuildWorkflow, Profile, _delete_branch
from druks.build.workflows import BuildWorkflow, Profile
from druks.core.apis.exceptions import GitHubAppNotConfiguredError, GitHubAppNotInstalledError
from druks.core.apis.github import get_github_client
from druks.settings import load_settings
Expand Down Expand Up @@ -137,8 +137,8 @@ async def _observe_external_close(*, repo: str, pr_number: int, work_item: WorkI
# failure must not strand the ticket — its reset below still runs.
try:
if (await RepoPolicy.resolve(repo)).delete_branch:
await _delete_branch(repo, work_item.branch)
except Exception: # noqa: BLE001 — cleanup only, like _delete_branch itself
await get_github_client(load_settings()).delete_branch(repo, work_item.branch)
except Exception: # noqa: BLE001 — cleanup only
logger.warning("Skipped branch cleanup for %s#%s.", repo, pr_number, exc_info=True)
# The attempt was abandoned, not the ticket: send it back to the
# provider's resting pool rather than stranding it in In Progress.
Expand Down
Loading
Loading