from __future__ import annotations from .backends import AgentBackend, FakeAgentBackend from .models import Decision, JobSnapshot from .store import SQLiteStatusStore PHASES = ("plan", "research", "code", "test", "verify", "result") class LocalAgentKernel: """Small in-process orchestration boundary for V1. The public methods are intentionally daemon-shaped. The GUI should keep calling this facade when the implementation later moves behind IPC. """ def __init__(self, store: SQLiteStatusStore | None = None, agent_backend: AgentBackend | None = None) -> None: self.store = store or SQLiteStatusStore() self.agent_backend = agent_backend or FakeAgentBackend() def submit_intent(self, text: str) -> int: intent = text.strip() if not intent: raise ValueError("Intent must not be empty.") job = self.store.create_job(intent) self.store.create_phases(job.id, PHASES) self.store.update_phase(job.id, "plan", "running", "Intent wird in einen Arbeitsplan verdichtet.") self.store.update_phase( job.id, "plan", "completed", f"Plan erstellt: {intent}", ) self.store.update_phase( job.id, "research", "waiting", "Wartet auf menschliche Freigabe des verdichteten Plans.", ) self.store.create_decision( job_id=job.id, phase_name="plan", title="Plan freigeben", context=( "Der Kernel hat den Intent in einen kleinen V1-Plan übersetzt. " "Die nächsten Phasen laufen automatisch, wenn der Plan freigegeben wird." ), options=["Plan freigeben", "Plan anpassen", "Auftrag blockieren"], ) self.store.update_job_status(job.id, "waiting_decision") return job.id def get_job_status(self, job_id: int) -> JobSnapshot: return self.store.snapshot(job_id) def list_jobs(self): return self.store.list_jobs() def list_open_decisions(self, job_id: int) -> list[Decision]: return self.store.list_open_decisions(job_id) def answer_decision(self, decision_id: int, selected_option: str, note: str = "") -> JobSnapshot: answered = self.store.answer_decision(decision_id, selected_option, note) if selected_option == "Auftrag blockieren": self._block_job(answered.job_id, note) return self.store.snapshot(answered.job_id) if selected_option == "Plan anpassen": self._request_plan_adjustment(answered.job_id, note) return self.store.snapshot(answered.job_id) self._run_remaining_phases(answered.job_id) return self.store.snapshot(answered.job_id) def approve_result(self, job_id: int, approved: bool) -> JobSnapshot: self.store.set_result_approval(job_id, approved) self.store.update_job_status(job_id, "approved" if approved else "rejected") return self.store.snapshot(job_id) def _request_plan_adjustment(self, job_id: int, note: str) -> None: adjustment = note.strip() or "Bitte Plan vor Ausführung anpassen." self.store.update_phase(job_id, "plan", "waiting", "Plananpassung angefordert.") self.store.create_decision( job_id=job_id, phase_name="plan", title="Angepassten Plan freigeben", context=( "Gewünschte Anpassung: " f"{adjustment} " "Der Skeleton-Kernel hält den Auftrag, bis diese Anpassung freigegeben wird." ), options=["Plan freigeben", "Auftrag blockieren"], ) self.store.update_job_status(job_id, "waiting_decision") def _block_job(self, job_id: int, note: str) -> None: reason = note.strip() or "Auftrag wurde an einem Human-Gate blockiert." for phase in self.store.list_phases(job_id): if phase.status in {"pending", "waiting", "running"}: self.store.update_phase(job_id, phase.name, "blocked", reason) self.store.save_result( job_id=job_id, summary="Der Auftrag wurde vor der automatischen Ausführung blockiert.", blocked=True, decisions=self._decision_summaries(job_id), next_steps=["Blocker klären", "Auftrag mit geschärftem Ziel neu starten"], ) self.store.update_job_status(job_id, "blocked") def _run_remaining_phases(self, job_id: int) -> None: job = self.store.get_job(job_id) for phase_name in PHASES[1:]: phase = self.store.update_phase(job_id, phase_name, "running", f"{phase_name} läuft.") outcome = self.agent_backend.run_phase( job, phase, context="Current CLFGUI V1 skeleton flow. Return a concise phase summary only.", ) self.store.update_phase(job_id, phase_name, outcome.status, outcome.summary) self.store.save_result( job_id=job_id, summary=( "V1-Skeleton abgeschlossen: Intent aufgenommen, Plan freigegeben, " "Phasen abgearbeitet und Ergebnispaket erzeugt." ), blocked=False, decisions=self._decision_summaries(job_id), next_steps=[ "Echte Agent-Adapter für Research/Code/Test anbinden", "Kernel bei Bedarf in separaten Headless-Daemon auslagern", f"Folgeauftrag für '{job.intent}' starten", ], ) self.store.update_job_status(job_id, "awaiting_approval") def _decision_summaries(self, job_id: int) -> list[str]: summaries: list[str] = [] for decision in self.store.list_decisions(job_id): if decision.selected_option: note = f" ({decision.note})" if decision.note else "" summaries.append(f"{decision.title}: {decision.selected_option}{note}") else: summaries.append(f"{decision.title}: offen") return summaries