Spaces:
Sleeping
Sleeping
| # ACE Code Reference | |
| > Full code examples, API signatures, step implementations, and usage patterns. | |
| For architecture and concepts, see [ACE_ARCHITECTURE.md](ACE_ARCHITECTURE.md). | |
| For design decisions and rejected alternatives, see [ACE_DECISIONS.md](ACE_DECISIONS.md). | |
| --- | |
| ## Public API | |
| All pipeline primitives, ACE steps, and context types are importable from `ace`: | |
| ```python | |
| # 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): | |
| ```python | |
| 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 | |
| ```python | |
| @dataclass | |
| class Sample: | |
| question: str | |
| context: str = "" | |
| ground_truth: str | None = None | |
| metadata: dict = field(default_factory=dict) | |
| id: str | None = None | |
| ``` | |
| ### ACESample protocol | |
| ```python | |
| 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 | |
| ```python | |
| 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 | |
| ```python | |
| @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`). | |
| ```python | |
| 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 | |
| ```python | |
| 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`. | |
| ```python | |
| 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`. | |
| ```python | |
| 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. | |
| ```python | |
| 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. | |
| ```python | |
| 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. | |
| ```python | |
| 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. | |
| ```python | |
| 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). | |
| ```python | |
| 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. | |
| ```python | |
| 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 | |
| ```python | |
| # 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` | |
| ```python | |
| @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` | |
| ```python | |
| @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 | |
| ```python | |
| 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`) | |
| ```python | |
| 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 | |
| ```python | |
| 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 | |
| ```python | |
| 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 | |
| ```python | |
| 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 | |
| ```python | |
| 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()`. | |
| ```python | |
| 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`. | |
| ```python | |
| 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`. | |
| ```python | |
| 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 | |
| ```python | |
| 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 | |
| ```python | |
| 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 | |
| ```python | |
| steps = [ | |
| LoadTracesStep(), | |
| OpenClawToTraceStep(), | |
| *learning_tail(reflector, skill_manager, skillbook), | |
| ] | |
| ``` | |
| ### Custom pipeline with `learning_tail` | |
| ```python | |
| 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 | |
| ```python | |
| # 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: | |
| ```bash | |
| 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 | |
| ```python | |
| @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 | |
| ```toml | |
| [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 | |
| ```python | |
| 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 | |
| ```python | |
| 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 | |
| ```python | |
| 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 | |
| ```python | |
| 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 | |
| ```python | |
| 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 | |
| ```python | |
| 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 | |
| ```python | |
| 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 | |
| ```python | |
| 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 | |
| ```python | |
| 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 | |
| ```python | |
| 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 | |
| ```python | |
| # 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") | |
| ``` | |