Spaces:
Sleeping
Sleeping
| """ACERunner — shared runner infrastructure for all ACE runners.""" | |
| from __future__ import annotations | |
| import logging | |
| from collections.abc import Iterable, Sequence | |
| from typing import Any | |
| from pipeline import Pipeline | |
| from pipeline.errors import CancellationToken | |
| from pipeline.protocol import SampleResult | |
| from ..core.context import ACEStepContext, SkillbookView | |
| from ..core.skillbook import Skillbook | |
| logger = logging.getLogger(__name__) | |
| class ACERunner: | |
| """Shared runner infrastructure for all ACE runners. | |
| Composes a ``Pipeline`` (does not extend it). Manages the epoch loop | |
| and delegates per-sample iteration, error isolation, foreground/background | |
| split, and concurrent workers to ``Pipeline.run()``. | |
| Subclasses override two methods: | |
| - ``run()`` — public API with a subclass-specific signature. | |
| - ``_build_context()`` — maps a single input item to ``ACEStepContext``. | |
| You can also construct an ``ACERunner`` directly with a hand-composed | |
| pipeline:: | |
| from ace import Pipeline, ACERunner, AgentStep, learning_tail | |
| pipe = Pipeline([AgentStep(agent, sb), *learning_tail(reflector, sm, sb)]) | |
| runner = ACERunner(pipeline=pipe, skillbook=sb) | |
| Attributes: | |
| pipeline: The composed ``Pipeline`` instance. Accessible for | |
| inspection after construction. | |
| skillbook: The ``Skillbook`` this runner operates on. | |
| """ | |
| def __init__( | |
| self, | |
| pipeline: Pipeline, | |
| skillbook: Skillbook, | |
| ) -> None: | |
| self.pipeline = pipeline | |
| self.skillbook = skillbook | |
| # ------------------------------------------------------------------ | |
| # Lifecycle helpers | |
| # ------------------------------------------------------------------ | |
| def save(self, path: str) -> None: | |
| """Save the current skillbook to disk.""" | |
| self.skillbook.save_to_file(path) | |
| def load(self, path: str) -> None: | |
| """Load a skillbook from disk, replacing the current one.""" | |
| self.skillbook = Skillbook.load_from_file(path) | |
| def wait_for_background(self, timeout: float | None = None) -> None: | |
| """Block until all background learning tasks complete. | |
| Delegates to ``Pipeline.wait_for_background()``. Call after | |
| ``run(wait=False)`` before saving the skillbook or reading final | |
| results. | |
| """ | |
| self.pipeline.wait_for_background(timeout) | |
| def learning_stats(self) -> dict[str, int]: | |
| """Return background learning progress. | |
| Delegates to ``Pipeline.background_stats()``. | |
| """ | |
| return self.pipeline.background_stats() | |
| # ------------------------------------------------------------------ | |
| # Generic epoch loop (called by subclasses) | |
| # ------------------------------------------------------------------ | |
| def _run( | |
| self, | |
| items: Sequence[Any] | Iterable[Any], | |
| *, | |
| epochs: int, | |
| wait: bool = True, | |
| cancel_token: CancellationToken | None = None, | |
| **kwargs: Any, | |
| ) -> list[SampleResult]: | |
| """Generic run loop handling epochs and Iterable validation. | |
| Returns when ``wait=True`` (default). Returns after foreground | |
| steps when ``wait=False`` — background learning continues. | |
| Args: | |
| cancel_token: Optional cancellation signal. Forwarded to | |
| ``Pipeline.run()`` — checked between steps and inside | |
| LLM calls (via contextvar). | |
| Raises ``ValueError`` if ``epochs > 1`` and *items* is not a | |
| ``Sequence``. | |
| """ | |
| if epochs > 1 and not isinstance(items, Sequence): | |
| raise ValueError( | |
| "Multi-epoch requires a Sequence, not a consumed Iterable." | |
| ) | |
| results: list[SampleResult] = [] | |
| n: int | None = len(items) if isinstance(items, Sequence) else None | |
| for epoch in range(1, epochs + 1): | |
| if cancel_token is not None and cancel_token.is_cancelled: | |
| break | |
| logger.info( | |
| "Epoch %d/%d: processing %s samples", | |
| epoch, | |
| epochs, | |
| n if n is not None else "unknown", | |
| ) | |
| contexts: list[ACEStepContext] = [ | |
| self._build_context( | |
| item, | |
| epoch=epoch, | |
| total_epochs=epochs, | |
| index=idx, | |
| total=n, | |
| global_sample_index=( | |
| (epoch - 1) * n + idx if n is not None else idx | |
| ), | |
| **kwargs, | |
| ) | |
| for idx, item in enumerate(items, start=1) | |
| ] | |
| epoch_results = self.pipeline.run(contexts, cancel_token=cancel_token) | |
| results.extend(epoch_results) | |
| if wait: | |
| self.pipeline.wait_for_background() | |
| return results | |
| # ------------------------------------------------------------------ | |
| # Subclass interface | |
| # ------------------------------------------------------------------ | |
| def _build_context( | |
| self, | |
| item: Any, | |
| *, | |
| epoch: int, | |
| total_epochs: int, | |
| index: int, | |
| total: int | None, | |
| global_sample_index: int, | |
| **kwargs: Any, | |
| ) -> ACEStepContext: | |
| """Map a single input item to an ``ACEStepContext``. | |
| Must be overridden by subclasses. Stateless — depends only on | |
| the item and the provided counters. | |
| """ | |
| raise NotImplementedError | |