logic-engine / ace /runners /base.py
ghostdrive1's picture
Upload folder using huggingface_hub
116524e verified
Raw
History Blame Contribute Delete
5.75 kB
"""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)
@property
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