107 lines
4.1 KiB
Python
107 lines
4.1 KiB
Python
# SPDX-FileCopyrightText: 2026 Art Sea LLC
|
|
#
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
from __future__ import annotations
|
|
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass, field
|
|
import logging
|
|
from typing import Generic, TypeVar
|
|
|
|
from band_aider.ai_client import llm_logging
|
|
from band_aider.context.contex_bag import ContextBag
|
|
from band_aider.context.global_context import GlobalContext
|
|
from band_aider.loop.loop_gate import LoopGate
|
|
from band_aider.ui.status import publish_loop_status
|
|
|
|
T = TypeVar("T", bound=LoopGate)
|
|
U = TypeVar("U", bound=ContextBag)
|
|
|
|
|
|
@dataclass(init=False)
|
|
class ProtoLoop(Generic[T, U], ABC):
|
|
"""ProtoLoop runs one gated workflow phase and records child-loop summaries as evidence."""
|
|
|
|
gate: T
|
|
context: U
|
|
_logger_stack: list[logging.Logger] = field(default_factory=list, init=False, repr=False)
|
|
|
|
def __init__(self, gate: T, context: U, logger: logging.Logger | None = None) -> None:
|
|
self.gate = gate
|
|
self.context = context
|
|
self._logger_stack = [logger] if logger is not None else []
|
|
|
|
@abstractmethod
|
|
def _prepare_work(self) -> "ProtoLoop | None":
|
|
"""Return the next child loop to run, or None when this phase has no child work."""
|
|
|
|
...
|
|
|
|
@abstractmethod
|
|
def _post_run(self) -> str:
|
|
"""Return this loop's summary after all gate-controlled work has finished."""
|
|
|
|
...
|
|
|
|
def _run(self, inner: "ProtoLoop") -> None:
|
|
"""Run one child loop and append its summary to this context's marginal results."""
|
|
|
|
publish_loop_status(
|
|
previous_loop=type(self).__name__,
|
|
current_loop=type(inner).__name__,
|
|
current_item=getattr(getattr(inner, "context", None), "goal", None),
|
|
)
|
|
result = inner.run()
|
|
if isinstance(self.context, GlobalContext):
|
|
self.context.story_cursor.marginal_result.append(result)
|
|
else:
|
|
self.context.marginal_result.append(result)
|
|
|
|
@property
|
|
def logger(self) -> logging.Logger:
|
|
return self._logger_stack[-1] if self._logger_stack else llm_logging.logger_for_loop(self)
|
|
|
|
def run(self, logger_name: str | None = None) -> str:
|
|
"""Run child work while the gate allows entry, retry, or continuation, then return a summary."""
|
|
|
|
if not self._logger_stack: self._logger_stack.append(llm_logging.logger_for_loop(self))
|
|
if logger_name: self._logger_stack.append(self.logger.getChild(llm_logging.safe_log_segment(logger_name)))
|
|
llm_logging.set_current_logger(self.logger)
|
|
count = 0
|
|
try:
|
|
publish_loop_status(
|
|
current_loop=type(self).__name__,
|
|
current_item=getattr(self.context, "goal", None),
|
|
)
|
|
while ((self.gate.may_enter() and count == 0) or self.gate.should_retry() or self.gate.should_continue()):
|
|
count += 1;
|
|
inner = self._prepare_work()
|
|
if inner is None: break
|
|
self._run(inner)
|
|
return self._post_run()
|
|
finally:
|
|
if logger_name: self._logger_stack.pop()
|
|
llm_logging.set_current_logger(self.logger if self._logger_stack else None)
|
|
|
|
"""
|
|
FAQ
|
|
|
|
Why return str instead of bool?
|
|
Because loop output is evidence, not control flow. A summary can describe success, partial progress, blocked state, requested files, or frontier expansion.
|
|
|
|
Why not return (bool, str)?
|
|
Because that compresses ambiguous workflow states too early. “Needs more files” is not success or failure; it is continuation evidence.
|
|
|
|
Who controls whether the loop continues?
|
|
The gate. may_enter(), shall_retry(), and should_continue() own control flow.
|
|
|
|
Where do child results go?
|
|
Into context.marginal_result as plain string summaries.
|
|
|
|
Where should success/failure be judged?
|
|
In loop-specific gates or judgement checks that inspect context, marginal summaries, FileManager CDC, and current task state.
|
|
|
|
Why remove next and parent?
|
|
Because _prepare_work() directly returns the child loop. The loop tree is produced by execution, not stored as mutable pointers.
|
|
"""
|