logic-engine / docs /design /ACE_REFERENCE.md
ghostdrive1's picture
Upload folder using huggingface_hub
116524e verified
|
Raw
History Blame Contribute Delete
35.5 kB

ACE Code Reference

Full code examples, API signatures, step implementations, and usage patterns.

For architecture and concepts, see ACE_ARCHITECTURE.md. For design decisions and rejected alternatives, see ACE_DECISIONS.md.


Public API

All pipeline primitives, ACE steps, and context types are importable from ace:

# Pipeline engine
from ace import Pipeline, Branch, MergeStrategy, StepProtocol, SampleResult

# ACE context
from ace import ACEStepContext, SkillbookView

# Runner base class (for custom runners)
from ace import ACERunner

# Core steps
from ace import (
    AgentStep, EvaluateStep, ReflectStep, UpdateStep,
    DeduplicateStep, CheckpointStep, LoadTracesStep, ExportSkillbookMarkdownStep,
    ObservabilityStep, PersistStep, learning_tail,
)

Integration steps live in ace.integrations (they have framework-specific dependencies):

from ace.integrations.browser_use import BrowserExecuteStep, BrowserToTrace
from ace.integrations.langchain import LangChainExecuteStep, LangChainToTrace
from ace.integrations.claude_code import ClaudeCodeExecuteStep, ClaudeCodeToTrace
from ace.integrations.claude_sdk import ClaudeSDKExecuteStep, ClaudeSDKToTrace
from ace.integrations.openclaw import OpenClawToTraceStep

Every runner also exposes a build_steps() classmethod that returns the step list it would compose internally.


Core Type Definitions

Sample

@dataclass
class Sample:
    question: str
    context: str = ""
    ground_truth: str | None = None
    metadata: dict = field(default_factory=dict)
    id: str | None = None

ACESample protocol

class ACESample(Protocol):
    """Minimal interface that Sample satisfies."""

    @property
    def question(self) -> str: ...

    @property
    def context(self) -> str: ...

    @property
    def ground_truth(self) -> str | None: ...

    @property
    def metadata(self) -> dict: ...

SkillbookView

class SkillbookView:
    """Read-only projection of a Skillbook. Safe on a frozen context."""

    __slots__ = ("_sb",)

    def __init__(self, skillbook: Skillbook) -> None:
        self._sb = skillbook

    def as_prompt(self) -> str:
        return self._sb.as_prompt()

    def get_skill(self, skill_id: str) -> Skill | None:
        return self._sb.get_skill(skill_id)

    def skills(self, include_invalid: bool = False) -> list[Skill]:
        return self._sb.skills(include_invalid=include_invalid)

    def stats(self) -> dict[str, object]:
        return self._sb.stats()

    def __len__(self) -> int:
        return len(self._sb.skills())

    def __iter__(self):
        return iter(self._sb.skills())

    def __repr__(self) -> str:
        return f"SkillbookView({len(self)} skills)"

ACEStepContext

@dataclass(frozen=True)
class ACEStepContext(StepContext):
    """Immutable context for the ACE pipeline.

    The skillbook field is a SkillbookView (read-only). Steps that need to
    write to the skillbook receive the real Skillbook via constructor injection.
    """

    sample: ACESample | None = None
    skillbook: SkillbookView | None = None
    trace: object | None = None
    agent_output: AgentOutput | None = None
    reflections: tuple[ReflectorOutput, ...] = ()
    skill_manager_output: UpdateBatch | None = None
    epoch: int = 1
    total_epochs: int = 1
    step_index: int = 0
    total_steps: int | None = None
    global_sample_index: int = 0

Protocol Definitions

All protocols live in ace/protocols/ (one file per protocol, re-exported from __init__.py).

class AgentLike(Protocol):
    def generate(self, question: str, context: str, skillbook: SkillbookView,
                 reflection: str | None = None, **kwargs) -> AgentOutput: ...

class ReflectorLike(Protocol):
    def reflect(self, question: str, agent_output: AgentOutput, skillbook: SkillbookView,
                ground_truth: str | None = None, feedback: str | None = None,
                **kwargs) -> ReflectorOutput: ...

class SkillManagerLike(Protocol):
    def update_skills(self, reflections: tuple[ReflectorOutput, ...],
                      skillbook: SkillbookView, question_context: str,
                      progress: str, **kwargs) -> SkillManagerOutput: ...

class DeduplicationManagerLike(Protocol):
    def get_similarity_report(self, skillbook: Skillbook) -> str | None: ...

Step Implementations

AgentStep

class AgentStep:
    requires = frozenset({"sample", "skillbook"})
    provides = frozenset({"agent_output", "injected_skill_ids"})

    def __init__(self, agent: AgentLike, skillbook: Skillbook) -> None:
        self.agent = agent
        self.skillbook = skillbook

    def __call__(self, ctx: ACEStepContext) -> ACEStepContext:
        injected_ids = tuple(s.id for s in self.skillbook.skills())
        agent_output = self.agent.generate(
            question=ctx.sample.question,
            context=ctx.sample.context,
            skillbook=ctx.skillbook,       # SkillbookView (read-only)
            sample=ctx.sample,
        )
        self.skillbook.mark_used(injected_ids)
        return ctx.replace(
            agent_output=agent_output,
            injected_skill_ids=injected_ids,
        )

EvaluateStep

Bridges the execute head (typed ACE objects) to the learning tail (raw traces). Optionally evaluates against a TaskEnvironment.

class EvaluateStep:
    requires = frozenset({"sample", "agent_output"})
    provides = frozenset({"trace"})

    def __init__(self, environment: TaskEnvironment | None = None) -> None:
        self.environment = environment

    def __call__(self, ctx: ACEStepContext) -> ACEStepContext:
        trace = {
            "question": ctx.sample.question,
            "context": ctx.sample.context,
            "ground_truth": ctx.sample.ground_truth,
            "reasoning": ctx.agent_output.reasoning,
            "answer": ctx.agent_output.final_answer,
            "skill_ids": ctx.agent_output.skill_ids,
        }
        if self.environment:
            result = self.environment.evaluate(
                sample=ctx.sample, agent_output=ctx.agent_output,
            )
            trace["feedback"] = result.feedback
        return ctx.replace(trace=trace)

ReflectStep

Handles two trace formats: (1) dict from EvaluateStep β€” extracts known fields; (2) any other object from TraceAnalyser or integrations β€” passes raw trace via **kwargs.

class ReflectStep:
    requires = frozenset({"trace", "skillbook"})
    provides = frozenset({"reflections"})

    async_boundary = True
    max_workers = 3

    def __init__(self, reflector: ReflectorLike) -> None:
        self.reflector = reflector

    def __call__(self, ctx: ACEStepContext) -> ACEStepContext:
        trace = ctx.trace

        if isinstance(trace, dict):
            agent_output = AgentOutput(
                reasoning=trace.get("reasoning", ""),
                final_answer=trace.get("answer", ""),
                skill_ids=trace.get("skill_ids", []),
            )
            reflection = self.reflector.reflect(
                question=trace.get("question", ""),
                agent_output=agent_output,
                skillbook=ctx.skillbook,
                ground_truth=trace.get("ground_truth"),
                feedback=trace.get("feedback"),
            )
        else:
            reflection = self.reflector.reflect(
                question="",
                agent_output=AgentOutput(reasoning="", final_answer=""),
                skillbook=ctx.skillbook,
                trace=trace,
            )

        return ctx.replace(reflections=(reflection,))

UpdateStep

Runs the agentic SkillManager. The SM's tools mutate the real Skillbook directly; the returned skill_manager_output on the context is the post-hoc audit log. There is no separate ApplyStep β€” the skillbook already reflects the changes when UpdateStep returns.

class UpdateStep:
    requires = frozenset({"reflections", "skillbook"})
    provides = frozenset({"skill_manager_output"})

    max_workers = 1

    def __init__(
        self, skill_manager: SkillManagerLike, skillbook: Skillbook
    ) -> None:
        self.skill_manager = skill_manager
        self.skillbook = skillbook

    def __call__(self, ctx: ACEStepContext) -> ACEStepContext:
        output = self.skill_manager.update_skills(
            reflections=ctx.reflections,
            skillbook=self.skillbook,  # real Skillbook β€” SM tools mutate it
            question_context=...,
            progress=...,
            injected_skill_ids=ctx.injected_skill_ids,
        )
        return ctx.replace(skill_manager_output=output.update)

DeduplicateStep

Optional β€” consolidates similar skills at a configurable interval.

class DeduplicateStep:
    requires = frozenset({"global_sample_index"})
    provides = frozenset()

    max_workers = 1

    def __init__(self, manager: DeduplicationManagerLike, skillbook: Skillbook, *, interval: int = 10) -> None:
        self.manager = manager
        self.skillbook = skillbook
        self.interval = interval

    def __call__(self, ctx: ACEStepContext) -> ACEStepContext:
        if ctx.global_sample_index % self.interval != 0:
            return ctx
        report = self.manager.get_similarity_report(self.skillbook)
        if report:
            logger.info("DeduplicateStep: similarity report at sample %d:\n%s",
                        ctx.global_sample_index, report)
        return ctx

CheckpointStep

Optional β€” periodically saves the skillbook to disk.

class CheckpointStep:
    requires = frozenset({"global_sample_index"})
    provides = frozenset()

    def __init__(self, directory: str | Path, skillbook: Skillbook, *, interval: int = 10) -> None:
        self.directory = Path(directory)
        self.skillbook = skillbook
        self.interval = interval

    def __call__(self, ctx: ACEStepContext) -> ACEStepContext:
        if ctx.global_sample_index % self.interval != 0:
            return ctx
        self.directory.mkdir(parents=True, exist_ok=True)
        self.skillbook.save_to_file(str(self.directory / f"checkpoint_{ctx.global_sample_index}.json"))
        self.skillbook.save_to_file(str(self.directory / "latest.json"))
        return ctx

LoadTracesStep

Generic JSONL file loader β€” reads a file path from ctx.sample, parses each line as JSON.

class LoadTracesStep:
    requires = frozenset({"sample"})
    provides = frozenset({"trace"})

    def __call__(self, ctx: ACEStepContext) -> ACEStepContext:
        path = Path(ctx.sample)
        events: list[dict] = []
        for line in path.read_text().splitlines():
            line = line.strip()
            if not line:
                continue
            try:
                events.append(json.loads(line))
            except json.JSONDecodeError:
                continue
        return ctx.replace(trace=events)

PersistStep

Writes the current skillbook to an external file (e.g. CLAUDE.md for Claude Code).

class PersistStep:
    requires = frozenset({"skillbook"})
    provides = frozenset()

    def __init__(self, target_path: str | Path, skillbook: Skillbook) -> None:
        self.target_path = Path(target_path)
        self.skillbook = skillbook

    def __call__(self, ctx: ACEStepContext) -> ACEStepContext:
        self.skillbook.save_to_file(str(self.target_path))
        return ctx

ExportSkillbookMarkdownStep

Exports the skillbook as a human-readable markdown file, grouped by section.

class ExportSkillbookMarkdownStep:
    requires = frozenset({"skillbook"})
    provides = frozenset()

    def __init__(self, path: str | Path, skillbook: Skillbook) -> None:
        self.path = Path(path)
        self.skillbook = skillbook

    def __call__(self, ctx: ACEStepContext) -> ACEStepContext:
        # Rewrites the markdown file from the current skillbook state
        ...
        return ctx

Factory Methods

learning_tail() β€” reusable learning steps

# ace/steps/__init__.py

def learning_tail(
    reflector: ReflectorLike,
    skill_manager: SkillManagerLike,
    skillbook: Skillbook,
    *,
    dedup_manager: DeduplicationManagerLike | None = None,
    dedup_interval: int = 10,
    checkpoint_dir: str | Path | None = None,
    checkpoint_interval: int = 10,
) -> list[StepProtocol[ACEStepContext]]:
    """Return the standard ACE learning steps."""
    steps: list[StepProtocol[ACEStepContext]] = [
        ReflectStep(reflector),
        UpdateStep(skill_manager, skillbook),
    ]
    if dedup_manager:
        steps.append(DeduplicateStep(dedup_manager, skillbook, interval=dedup_interval))
    if checkpoint_dir:
        steps.append(CheckpointStep(checkpoint_dir, skillbook, interval=checkpoint_interval))
    return steps

TraceAnalyser from_roles

@classmethod
def from_roles(cls, *, reflector, skill_manager, skillbook=None,
               dedup_manager=None, dedup_interval=10,
               checkpoint_dir=None, checkpoint_interval=10,
               extra_steps=None):
    skillbook = skillbook or Skillbook()
    steps = learning_tail(
        reflector, skill_manager, skillbook,
        dedup_manager=dedup_manager, dedup_interval=dedup_interval,
        checkpoint_dir=checkpoint_dir, checkpoint_interval=checkpoint_interval,
    )
    if extra_steps:
        steps.extend(extra_steps)
    return cls(pipeline=Pipeline(steps), skillbook=skillbook)

ACE from_roles

@classmethod
def from_roles(cls, *, agent, reflector, skill_manager, environment=None,
               skillbook=None, dedup_manager=None, dedup_interval=10,
               checkpoint_dir=None, checkpoint_interval=10,
               extra_steps=None):
    skillbook = skillbook or Skillbook()
    steps = [
        AgentStep(agent, skillbook),
        EvaluateStep(environment),
        *learning_tail(
            reflector, skill_manager, skillbook,
            dedup_manager=dedup_manager, dedup_interval=dedup_interval,
            checkpoint_dir=checkpoint_dir, checkpoint_interval=checkpoint_interval,
        ),
    ]
    if extra_steps:
        steps.extend(extra_steps)
    return cls(pipeline=Pipeline(steps), skillbook=skillbook)

Runner Implementations

ACERunner base

class ACERunner:
    """Shared runner infrastructure for all ACE runners."""

    def __init__(self, pipeline: Pipeline, skillbook: Skillbook) -> None:
        self.pipeline = pipeline
        self.skillbook = skillbook

    def save(self, path: str) -> None:
        self.skillbook.save_to_file(path)

    def wait_for_background(self, timeout: float | None = None) -> None:
        self.pipeline.wait_for_background(timeout)

    @property
    def learning_stats(self) -> dict:
        return self.pipeline.background_stats()

Generic run loop (_run)

def _run(self, items, *, epochs, wait=True, **kwargs) -> list[SampleResult]:
    if epochs > 1 and not isinstance(items, Sequence):
        raise ValueError("Multi-epoch requires a Sequence, not a consumed Iterable.")

    results: list[SampleResult] = []
    n = len(items) if isinstance(items, Sequence) else None

    for epoch in range(1, epochs + 1):
        contexts = [
            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)
        results.extend(epoch_results)

    if wait:
        self.pipeline.wait_for_background()
    return results

TraceAnalyser

class TraceAnalyser(ACERunner):
    """Analyse pre-recorded traces to build a skillbook."""

    @classmethod
    def from_roles(cls, *, reflector, skill_manager, skillbook=None, **kwargs) -> "TraceAnalyser": ...

    def run(self, traces: Sequence[Any], epochs: int = 1, *, wait: bool = True) -> list[SampleResult]:
        return self._run(traces, epochs=epochs, wait=wait)

    def _build_context(self, raw_trace, *, epoch, total_epochs, index, total,
                       global_sample_index) -> ACEStepContext:
        return ACEStepContext(
            skillbook=SkillbookView(self.skillbook),
            trace=raw_trace,
            metadata={...},                         # inferred trace identity for provenance
            epoch=epoch, total_epochs=total_epochs,
            step_index=index, total_steps=total,
            global_sample_index=global_sample_index,
        )

ACE

class ACE(ACERunner):
    """Live adaptive pipeline: Agent β†’ Evaluate β†’ Reflect β†’ Update β†’ Apply."""

    @classmethod
    def from_roles(cls, *, agent, reflector, skill_manager,
                   environment=None, skillbook=None, **kwargs) -> "ACE": ...

    def run(self, samples, epochs=1, *, wait=True) -> list[SampleResult]:
        return self._run(samples, epochs=epochs, wait=wait)

    def _build_context(self, sample, *, epoch, total_epochs, index, total,
                       global_sample_index, **_) -> ACEStepContext:
        return ACEStepContext(
            sample=sample,
            skillbook=SkillbookView(self.skillbook),
            metadata={...},
            epoch=epoch, total_epochs=total_epochs,
            step_index=index, total_steps=total,
            global_sample_index=global_sample_index,
        )

Integration runner pattern

class BrowserUse(ACERunner):
    """Browser-use agent with ACE learning pipeline."""

    @classmethod
    def from_roles(cls, *, browser_llm, reflector, skill_manager,
                   skillbook=None, **kwargs):
        skillbook = skillbook or Skillbook()
        steps = [
            BrowserExecuteStep(browser_llm),
            BrowserToTrace(),
            *learning_tail(reflector, skill_manager, skillbook, **kwargs),
        ]
        return cls(pipeline=Pipeline(steps), skillbook=skillbook)

    @classmethod
    def from_model(cls, browser_llm, *, ace_model="gpt-4o-mini",
                   ace_max_tokens=2048, ace_temperature=0.0, **kwargs) -> BrowserUse:
        return cls.from_roles(
            browser_llm=browser_llm,
            reflector=Reflector(ace_model),
            skill_manager=SkillManager(ace_model),
            **kwargs,
        )

    def run(self, tasks, epochs=1, *, wait=True):
        return self._run(tasks, epochs=epochs, wait=wait)

    def _build_context(self, task, *, epoch, total_epochs, index, total,
                       global_sample_index, **_):
        return ACEStepContext(
            sample=task,    # raw string β€” not wrapped in Sample
            skillbook=SkillbookView(self.skillbook),
            epoch=epoch, total_epochs=total_epochs,
            step_index=index, total_steps=total,
            global_sample_index=global_sample_index,
        )

ACELiteLLM

class ACELiteLLM:
    def __init__(self, model="gpt-4o-mini", *, skillbook=None, environment=None,
                 reflector=None, skill_manager=None, ...):
        self.agent = Agent(model)
        self.reflector = reflector or Reflector(model)
        self.skill_manager = skill_manager or SkillManager(model)
        self._skillbook = skillbook or Skillbook()
        self.environment = environment
        self._ace: ACE | None = None
        self._analyser: TraceAnalyser | None = None

    @classmethod
    def from_model(cls, model="gpt-4o-mini", *, max_tokens=2048,
                   temperature=0.0, **kwargs) -> ACELiteLLM:
        return cls(model, **kwargs)

    def ask(self, question, context="") -> str:
        """Direct Agent call β€” no pipeline. Stores interaction for learn_from_feedback()."""
        ...

    def learn(self, samples, environment=None, epochs=1, *, wait=True):
        """Delegate to lazy-init ACE runner."""
        return self._get_ace(environment).run(samples, epochs=epochs, wait=wait)

    def learn_from_traces(self, traces, epochs=1, *, wait=True):
        """Delegate to lazy-init TraceAnalyser."""
        return self._get_analyser().run(traces, epochs=epochs, wait=wait)

    def learn_from_feedback(self, feedback, ground_truth=None) -> bool:
        """Manual single-shot learning from last ask() call."""
        ...

    def load(self, path):
        """Load skillbook β€” invalidates cached runners (stale refs)."""
        self._skillbook = Skillbook.load_from_file(path)
        self._ace = None
        self._analyser = None

Role Implementations

Agent

Produces answers using the current skillbook. Formats the prompt, calls PydanticAI with AgentOutput as the structured result type, extracts cited skill IDs via extract_cited_skill_ids().

agent = Agent("gpt-4o-mini")
output = agent.generate(
    question="What is the capital of France?",
    context="Answer concisely",
    skillbook=skillbook,
)
# output.final_answer == "Paris"
# output.skill_ids == ["geography-00001"]

Reflector

Single-pass analysis. Builds a skillbook excerpt from cited IDs, formats the prompt, calls PydanticAI with ReflectorOutput.

reflector = Reflector("gpt-4o-mini")
reflection = reflector.reflect(
    question="What is 2+2?",
    agent_output=agent_output,
    skillbook=skillbook,
    ground_truth="4",
    feedback="Correct!",
)
# reflection.key_insight

SkillManager (agentic)

A RecursiveAgent subclass with atomic mutation tools. Tools operate on the real Skillbook directly; there is no staging and no downstream ApplyStep.

from ace import SkillManager
from ace.core.recursive_agent import AgenticConfig

sm = SkillManager("gpt-4o-mini", config=AgenticConfig(max_requests=20))
output = sm.update_skills(
    reflections=(reflection_output,),
    skillbook=skillbook,               # real Skillbook β€” mutated in place
    question_context="Math problem solving",
    progress="5/10 correct",
    source=source,
    injected_skill_ids=ctx.injected_skill_ids,
)
# skillbook has already been updated; `output` is the post-hoc audit log

Tool surface (implementations/sm_tools.py):

Tool Kind Purpose
add_skill(section, issue, keywords, insight?) mutate ADD a new skill
update_skill(skill_id, issue, keywords?, insight?) mutate UPDATE an existing skill
remove_skill(skill_id, reason) mutate REMOVE a skill (duplicate, vague, or harmful_count β‰₯ 3)
tag_skill(skill_id, delta) mutate Bump helpful_count / harmful_count / neutral_count (+1 / -1 / 0)
search_skills(query, top_k, section?, keywords?) read Hybrid retrieval lookup (check before ADD)
read_skill(skill_id) read Fetch full skill payload including counters
execute_code(code) read Inherited sandbox tool for verification

Each mutation tool appends an UpdateOperation to SMDeps.operations; update_skills() splices that list into the returned SkillManagerOutput.

Shared helpers (implementations/helpers.py)

Function Purpose
format_optional(value) Returns "(none)" for falsy values
make_skillbook_excerpt(skillbook, skill_ids) Builds issue / insight excerpts for listed skills

Prompt templates (implementations/prompts.py)

Constant Role
AGENT_PROMPT Agent prompt with strategic problem-solving protocol
REFLECTOR_PROMPT Reflector prompt with pure-analysis protocol (no tagging)
SKILL_MANAGER_SYSTEM SkillManager system prompt (tool rules + rejection criteria)
SKILL_MANAGER_PROMPT SkillManager user prompt (reflections, stats, workflow)
SKILLBOOK_USAGE_INSTRUCTIONS Shared text for skillbook usage guidance

Also exports wrap_skillbook_for_external_agent(skillbook) β€” the canonical function for injecting skillbook context into external agentic systems.


Integration Step Examples

Execute step pattern

class BrowserExecuteStep:
    requires = frozenset({"sample", "skillbook"})
    provides = frozenset({"trace"})

    def __init__(self, browser_llm, browser=None, **agent_kwargs) -> None:
        self.browser_llm = browser_llm
        self.browser = browser
        self.agent_kwargs = agent_kwargs

    async def __call__(self, ctx: ACEStepContext) -> ACEStepContext:
        task: str = ctx.sample

        # INJECT β€” prepend skillbook context
        enhanced_task = self._inject(task, ctx.skillbook)

        # EXECUTE β€” run browser-use agent
        agent = Agent(task=enhanced_task, llm=self.browser_llm, **self.agent_kwargs)
        history = await agent.run()

        result = BrowserResult(
            task=task, success=True, output=history.final_result(),
            steps_count=history.number_of_steps(),
            chronological_steps=..., raw_history=history,
        )
        return ctx.replace(trace=result)

ToTrace step pattern

class SomeToTrace:
    requires = frozenset({"trace"})
    provides = frozenset({"trace"})

    def __call__(self, ctx: ACEStepContext) -> ACEStepContext:
        r: SomeResult = ctx.trace
        trace = {
            "question": r.task,
            "reasoning": r.execution_trace,
            "answer": r.output,
            "skill_ids": r.cited_skill_ids,
            "feedback": f"Task {'succeeded' if r.success else 'failed'}",
            "ground_truth": None,
        }
        return ctx.replace(trace=trace)

Trace file pipeline composition

steps = [
    LoadTracesStep(),
    OpenClawToTraceStep(),
    *learning_tail(reflector, skill_manager, skillbook),
]

Custom pipeline with learning_tail

from ace.steps import learning_tail

skillbook = Skillbook.load_from_file("expert.json")
steps = [
    MyCustomExecuteStep(my_agent),
    MyValidationStep(),
    *learning_tail(reflector, skill_manager, skillbook, dedup_manager=dedup),
]
runner = ACERunner(Pipeline(steps), skillbook)

Provider Resolution

# ace/providers/pydantic_ai.py β€” resolve_model()
# Routes LiteLLM model strings to PydanticAI:

# 1. PydanticAI-native prefix β†’ pass through
#    "openai:gpt-4o" β†’ "openai:gpt-4o"

# 2. LiteLLM prefix matching native provider β†’ rewrite
#    "bedrock/model" β†’ "bedrock:model"

# 3. Fallback β†’ litellm: prefix
#    "ollama/llama3" β†’ "litellm:ollama/llama3"

Mapped prefixes: anthropic, azure, azure_ai, bedrock, cohere, deepseek, groq, mistral, openrouter, vertex_ai.

Install native provider extras for faster calls:

uv add "pydantic-ai-slim[anthropic]"    # uses ANTHROPIC_API_KEY
uv add "pydantic-ai-slim[openai]"       # uses OPENAI_API_KEY
uv add "pydantic-ai-slim[bedrock]"      # uses AWS credentials
uv add "pydantic-ai-slim[anthropic,openai,bedrock]"  # multiple

Config types

@dataclass
class ModelConfig:
    """Which model to use for a role. No secrets."""
    model: str
    temperature: float = 0.0
    max_tokens: int = 2048
    extra_params: dict[str, Any] | None = None

@dataclass
class ACEModelConfig:
    """Model selection per ACE role."""
    default: ModelConfig
    agent: ModelConfig | None = None
    reflector: ModelConfig | None = None
    skill_manager: ModelConfig | None = None

    def for_role(self, role: str) -> ModelConfig: ...

ace.toml example

[default]
model = "gpt-4o-mini"

[agent]
model = "claude-sonnet-4-20250514"
max_tokens = 4096

[reflector]
model = "gpt-4o-mini"

Registry (ace/providers/registry.py)

  • validate_connection(model, api_key?) β€” 3-token LLM call to verify auth
  • get_required_key(model) β€” returns (provider, env_var)
  • search_models(query?, provider?) β€” searches LiteLLM's model cost database
  • suggest_models(typo) β€” fuzzy match for typos
  • available_providers() β€” lists providers with key status

Usage Examples

TraceAnalyser β€” learn from browser-use history

from ace import TraceAnalyser, Reflector, SkillManager

traces = [
    {
        "task": "Find the cheapest flight to Tokyo",
        "output": "$450 on ANA, departing March 15",
        "feedback": "Correct price found in 8 steps",
        "reasoning": "Step 1: Navigate to Google Flights...",
    },
    {
        "task": "Book a hotel in Shibuya",
        "output": "Failed: could not find checkout button",
        "feedback": "Task failed after 15 steps β€” checkout button was behind a cookie modal",
        "reasoning": "Step 1: Navigate to Booking.com...",
    },
]

analyser = TraceAnalyser.from_roles(reflector=Reflector("gpt-4o-mini"), skill_manager=SkillManager("gpt-4o-mini"))
results = analyser.run(traces, epochs=2)
analyser.save("travel_agent.json")

ACE β€” live Q&A training

from ace import ACE, Sample, SimpleEnvironment, Agent, Reflector, SkillManager

samples = [
    Sample(question="Capital of France?", ground_truth="Paris"),
    Sample(question="Largest ocean?", ground_truth="Pacific"),
]

ace = ACE.from_roles(
    agent=Agent("gpt-4o-mini"),
    reflector=Reflector("gpt-4o-mini"),
    skill_manager=SkillManager("gpt-4o-mini"),
    environment=SimpleEnvironment(),
)
results = ace.run(samples, epochs=3)
ace.save("geography.json")

ACE β€” without environment

ace = ACE.from_roles(
    agent=Agent("gpt-4o-mini"),
    reflector=Reflector("gpt-4o-mini"),
    skill_manager=SkillManager("gpt-4o-mini"),
)
results = ace.run(samples, epochs=3)

ACE β€” with checkpoints and deduplication

from ace import ACE, Agent, Reflector, SkillManager, SimpleEnvironment
from ace.deduplication import DeduplicationManager
from ace.protocols.deduplication import DeduplicationConfig

ace = ACE.from_roles(
    agent=Agent("gpt-4o-mini"),
    reflector=Reflector("gpt-4o-mini"),
    skill_manager=SkillManager("gpt-4o-mini"),
    environment=SimpleEnvironment(),
    dedup_manager=DeduplicationManager(DeduplicationConfig(similarity_threshold=0.85)),
    checkpoint_dir="./checkpoints",
    checkpoint_interval=10,
)
# Pipeline: Agent β†’ Evaluate β†’ Reflect β†’ Update β†’ Apply β†’ Deduplicate β†’ Checkpoint
results = ace.run(samples, epochs=3)

Integration β€” browser-use runner

from ace import BrowserUse, Reflector, SkillManager
from langchain_openai import ChatOpenAI

browser_llm = ChatOpenAI(model="gpt-4o")

# Explicit construction
runner = BrowserUse.from_roles(
    browser_llm=browser_llm,
    reflector=Reflector("gpt-4o-mini"),
    skill_manager=SkillManager("gpt-4o-mini"),
)

# Or convenience construction
runner = BrowserUse.from_model(browser_llm, ace_model="gpt-4o-mini")

results = runner.run(["Find top HN post", "Check weather in Tokyo"])
runner.save("browser_expert.json")

Integration β€” LangChain runner

from ace import LangChain
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate

chain = ChatPromptTemplate.from_template("Answer: {input}") | ChatOpenAI(model="gpt-4o")

runner = LangChain.from_model(chain, ace_model="gpt-4o-mini")
results = runner.run([{"input": "What is ACE?"}, {"input": "Explain skillbooks"}])
runner.save("chain_expert.json")

Integration β€” Claude Code runner

from ace import ClaudeCode

runner = ClaudeCode.from_model(working_dir="./my_project", ace_model="gpt-4o-mini")
results = runner.run(["Add unit tests for utils.py", "Refactor the auth module"])
runner.save("code_expert.json")

ACELiteLLM β€” conversational agent with learning

from ace import ACELiteLLM, SimpleEnvironment, Sample

ace = ACELiteLLM.from_model("gpt-4o-mini")

# Direct Q&A (no pipeline)
answer = ace.ask("What is the capital of France?")

# Batch learning
samples = [
    Sample(question="Capital of France?", ground_truth="Paris"),
    Sample(question="Largest ocean?", ground_truth="Pacific"),
]
ace.learn(samples, environment=SimpleEnvironment(), epochs=3)

# Manual feedback learning from last ask()
ace.ask("What is 2+2?")
ace.learn_from_feedback("The answer should be 4", ground_truth="4")

ace.save("learned.json")

# With Recursive Reflector
from ace import RRStep, RRConfig
rr = RRStep("gpt-4o-mini", config=RRConfig(max_requests=20))
ace = ACELiteLLM("gpt-4o-mini", reflector=rr)

Fire-and-forget β€” results while learning continues

ace = ACE.from_roles(
    agent=Agent("gpt-4o-mini"),
    reflector=Reflector("gpt-4o-mini"),
    skill_manager=SkillManager("gpt-4o-mini"),
)

# wait=False: returns after foreground steps (Agent + Evaluate)
results = ace.run(samples, epochs=1, wait=False)

# Use agent outputs immediately
for r in results:
    print(r.output.agent_output.final_answer)

# Check learning progress
print(ace.learning_stats)
# {"active": 3, "completed": 12}

# Block when you need the skillbook finalised
ace.wait_for_background(timeout=60.0)
ace.save("learned.json")

Mixed workflow β€” batch then live

from ace import TraceAnalyser, ACE, Skillbook
from ace.implementations import Agent, Reflector, SkillManager

reflector = Reflector("gpt-4o-mini")
skill_manager = SkillManager("gpt-4o-mini")

# Phase 1: build skillbook from historical traces
skillbook = Skillbook()
analyser = TraceAnalyser.from_roles(
    reflector=reflector, skill_manager=skill_manager, skillbook=skillbook,
)
analyser.run(historical_traces, epochs=3)

# Phase 2: deploy with live learning (reuse the evolved skillbook)
ace = ACE.from_roles(
    agent=Agent("gpt-4o-mini"),
    reflector=reflector, skill_manager=skill_manager, skillbook=skillbook,
)
ace.run(live_samples, epochs=1)
ace.save("production.json")

Offline learning from integration traces

# Record browser executions
histories = [await agent.run(task) for task in tasks]

# Feed raw histories directly β€” Reflector analyses them as-is
analyser = TraceAnalyser.from_roles(
    reflector=Reflector("gpt-4o-mini"),
    skill_manager=SkillManager("gpt-4o-mini"),
)
analyser.run(histories, epochs=2)
analyser.save("browser_expert.json")