Architecture for ACE, a pipeline-based framework for building self-improving AI agents. Roles are backed by PydanticAI agents; the pipeline engine handles composition and concurrency.
For full code examples and API reference, see ACE_REFERENCE.md. For design decisions and rejected alternatives, see ACE_DECISIONS.md. For the pipeline engine, see PIPELINE_DESIGN.md.
ACE (Agentic Context Engine) builds AI agents that learn from their own executions. It combines:
- A pipeline engine (
pipeline/) with typed step contracts, concurrent execution, and structured error handling - Roles (Agent, Reflector, SkillManager) backed by PydanticAI agents for structured LLM interactions
- A Skillbook — an evolving knowledge base of strategies that agents read from and learning loops write to
- Integration steps for external frameworks (browser-use, LangChain, Claude Code, Anthropic SDK)
- Observability via Logfire auto-instrumentation of all PydanticAI agent calls
The LLM interaction layer uses PydanticAI exclusively. Three legacy hand-rolled LLM clients (LiteLLM, Instructor, ClaudeCode) were replaced — PydanticAI handles structured output, retries with error feedback, and multi-provider support as maintained infrastructure. The pipeline engine and skillbook/learning loop are untouched.
| Kept (core IP) | Replaced (commodity plumbing) |
|---|---|
Pipeline engine (requires/provides, async_boundary, max_workers) |
LLM client abstraction (3 implementations → PydanticAI agents) |
| Skillbook & learning loop (Reflect → Update → Apply) | Structured output parsing + retries (→ PydanticAI native validation) |
Step composition (learning_tail, pipeline nesting) |
RR iteration loop, code extraction, budget tracking (~2,500 lines → PydanticAI agent + tools) |
| Domain-specific prompts | Sub-agent call management (CallBudget → UsageLimits) |
| Legacy | Current | What it does |
|---|---|---|
OfflineACE |
TraceAnalyser |
Analyse pre-recorded traces → evolve a skillbook |
OnlineACE |
ACE |
Live execution → feedback → learning loop |
ACEBase |
ACERunner |
Shared runner infrastructure (composition, not inheritance from Pipeline) |
ACEStepResult |
Removed — use SampleResult from the pipeline engine |
Unified result type |
The framework separates concerns into four layers:
| Layer | Location | Responsibility | Example |
|---|---|---|---|
| Protocols | ace/protocols/ |
Interface contracts | ReflectorLike.reflect() |
| Roles | ace/implementations/ |
Business logic (LLM calls) | Reflector, RRStep |
| Steps | ace/steps/ |
Context plumbing (extract → call role → put back) | ReflectStep |
| Runners | ace/runners/ |
Orchestration (sample loop, epoch management) | ACELiteLLM |
Protocols define what a role must look like. Roles implement the logic. Steps adapt between the pipeline's context-based data flow and the role's parameter-based API. Runners compose steps into pipelines and iterate over inputs.
Roles are interchangeable anywhere their protocol is expected — both Reflector (simple single-pass) and RRStep (recursive multi-iteration) satisfy ReflectorLike. The runner and pipeline don't know or care which one is in use.
The input unit for ACE. A question with optional context and ground truth:
@dataclass
class Sample:
question: str
context: str = ""
ground_truth: str | None = None
metadata: dict = field(default_factory=dict)
id: str | None = NoneSteps access ctx.sample.question uniformly. A Protocol makes this duck typing explicit and type-safe. Sample satisfies it structurally — no inheritance required.
The Skillbook is mutable — steps add, update, and remove skills. Placing it directly on a frozen=True context would allow mutation through the reference, breaking the immutability guarantee.
SkillbookView wraps a Skillbook and exposes only read methods (as_prompt(), get_skill(), skills(), stats()). Write methods don't exist on the class — calling them raises AttributeError at runtime and a type error at check time.
Enforcement:
- Type checker — mypy/pyright flags
ctx.skillbook.add_skill(...)becauseSkillbookViewhas no such method. - Runtime —
AttributeErrorif someone calls a write method anyway. - Convention — the underlying
_sbis underscore-prefixed. Accessing it is a deliberate violation.
Steps that only read the skillbook (ReflectStep) access ctx.skillbook — the view. Steps that write the skillbook (AgentStep, UpdateStep, DeduplicateStep, CheckpointStep) receive the real Skillbook via constructor injection and use self.skillbook. AgentStep bumps used_count; UpdateStep invokes the agentic SkillManager whose tools apply ADD / UPDATE / REMOVE / TAG directly.
Subclass of the pipeline engine's StepContext. Carries all step-to-step data for the ACE pipeline. The pipeline engine only knows about sample and metadata; all ACE-specific fields live here.
Key fields:
| Field | Type | Source |
|---|---|---|
mode |
Literal["online", "offline"] |
"online" (default) — reserved for downstream steps |
sample |
ACESample | None |
Set by runner's _build_context() |
skillbook |
SkillbookView | None |
Read-only projection of the real Skillbook |
trace |
object | None |
Raw execution record — any type, no enforced schema |
agent_output |
AgentOutput | None |
Produced by AgentStep |
reflections |
tuple[ReflectorOutput, ...] |
Produced by ReflectStep / RRStep |
skill_manager_output |
UpdateBatch | None |
Produced by UpdateStep (audit log of mutations the SM already applied) |
injected_skill_ids |
tuple[str, ...] |
Produced by AgentStep — skill IDs rendered into the agent prompt; downstream attribution scope |
epoch, total_epochs |
int |
Runner bookkeeping |
step_index, total_steps |
int |
Runner bookkeeping |
global_sample_index |
int |
Runner bookkeeping (used by interval steps) |
The trace field holds the raw execution record from any external system — a browser-use AgentHistoryList, a LangChain result dict, a Claude Code transcript, or any arbitrary Python object. The Reflector receives the raw trace and is responsible for making sense of it.
The reflections field is a tuple. In single-trace mode, it's a 1-tuple. In batch mode, it holds one ReflectorOutput per trace. Downstream steps iterate uniformly — no special-casing.
| On the context | Injected via constructor | |
|---|---|---|
| Nature | Step-to-step data + read-only dependencies | Mutable shared state |
| Lifetime | Per-sample (born in _build_context, dies after pipeline) |
Per-runner (created once, shared across samples) |
| Immutable? | Yes — frozen fields, read-only views | No — mutable by design |
| Examples | agent_output, reflections, skillbook (view) |
skillbook (real), environment, dedup_manager |
| Validated by engine? | Yes — requires/provides |
No — runtime error if missing |
Steps depend on protocols, not concrete classes. Each protocol defines the minimal interface a step needs. Concrete implementations satisfy them structurally — no inheritance required.
| Protocol | Method | Used by | Satisfied by |
|---|---|---|---|
AgentLike |
generate(question, context, skillbook, reflection, **kwargs) → AgentOutput |
AgentStep |
Agent |
ReflectorLike |
reflect(question, agent_output, skillbook, ground_truth, feedback, **kwargs) → ReflectorOutput |
ReflectStep |
Reflector, RRStep |
SkillManagerLike |
update_skills(reflections, skillbook, question_context, progress, **kwargs) → SkillManagerOutput |
UpdateStep |
SkillManager |
DeduplicationManagerLike |
get_similarity_report(skillbook) → str | None |
DeduplicateStep |
DeduplicationManager |
Roles take a model string directly (e.g. Agent("gpt-4o-mini")). Internally each role creates a PydanticAI agent that handles structured output natively — no separate LLM client protocol is needed.
Why protocols, not ABC: Protocols use structural typing (duck typing checked by mypy). A class satisfies a protocol if it has the right methods — no class Agent(AgentLike) inheritance needed. Users can pass any object with a matching method, mocks satisfy protocols without ceremony, and steps are decoupled from implementations at the type level.
Concrete LLM-based implementations of the role protocols. Live in ace/implementations/ — fully self-contained.
| Class | Protocol | Method | What it does |
|---|---|---|---|
Agent |
AgentLike |
generate() |
Produces answers using the current skillbook of strategies |
Reflector |
ReflectorLike |
reflect() |
Single-pass analysis of agent outputs to extract lessons |
RRStep |
ReflectorLike + StepProtocol |
reflect() / __call__() |
Recursive multi-iteration reflection via PydanticAI agent with tools |
SkillManager |
SkillManagerLike |
update_skills() |
Transforms reflections into actionable skillbook updates |
All three share the same constructor pattern: __init__(self, model: str, *, prompt_template=..., max_retries=3). The model parameter is resolved via resolve_model() to a PydanticAI agent.
RRStep is both a StepProtocol[ACEStepContext] (composable in any pipeline) and ReflectorLike (usable as a drop-in reflector). It is a subclass of RecursiveAgent with execute_code and recurse tools, plus two-tier compaction and depth-based recursion. See RR_DESIGN.md for the full Recursive Reflector architecture.
Reusable step implementations in ace/steps/. Each satisfies StepProtocol[ACEStepContext]. Each step does exactly one thing.
Design principle: steps are stateless. A step's __call__ is a pure function of its constructor arguments and the incoming ACEStepContext. No internal counters, no accumulated state between invocations. Run-scoped information (like a global sample index for interval logic) comes from the context.
| Step | Requires | Provides | Side effects | max_workers |
|---|---|---|---|---|
| AgentStep | sample, skillbook |
agent_output |
None | 1 |
| EvaluateStep | sample, agent_output |
trace |
None | 1 |
| ReflectStep | trace, skillbook |
reflections |
None | 3; async_boundary = True |
| UpdateStep | reflections, skillbook |
skill_manager_output |
Agentic SkillManager mutates skillbook directly via ADD / UPDATE / REMOVE / TAG tools; output is an audit log | 1 |
| DeduplicateStep | global_sample_index |
— | Consolidates similar skills | 1 |
| CheckpointStep | global_sample_index |
— | Saves skillbook to disk | 1 |
| LoadTracesStep | sample |
trace |
None | 1 |
| PersistStep | skillbook |
— | Writes skillbook to external file | 1 |
| ExportSkillbookMarkdownStep | skillbook |
— | Exports skillbook as markdown | 1 |
Requires vs Injected: Requires lists context fields (validated by the pipeline engine at construction time). The skillbook on the context is a SkillbookView (read-only). Steps that write to the skillbook receive the real Skillbook via constructor injection.
trace as the universal learning input: The learning tail's entry point (ReflectStep) requires only trace and skillbook. In the standard ACE pipeline, EvaluateStep bundles structured fields into a trace dict. In TraceAnalyser, _build_context places the raw trace directly. In integrations, the execute step provides trace from its framework's native output. The learning tail is agnostic to trace format.
Steps with empty provides are pure side-effect steps — they mutate shared state (skillbook) or write to external systems (disk) but add no new fields to the context.
ACERunner (shared infrastructure: epoch loop, delegates to Pipeline.run())
├── TraceAnalyser — [Reflect → Update → Apply]
├── ACE — [Agent → Evaluate → Reflect → Update → Apply]
├── BrowserUse — [BrowserExecute → BrowserToTrace → learning_tail]
├── LangChain — [LangChainExecute → LangChainToTrace → learning_tail]
├── ClaudeCode — [ClaudeCodeExecute → ClaudeCodeToTrace → learning_tail]
└── OpenClaw (script) — [LoadTraces → OpenClawToTrace → learning_tail]
ACELiteLLM (standalone convenience wrapper — not an ACERunner subclass)
├── ask() — direct Agent call, no pipeline
├── learn() — delegates to lazy-init ACE runner
├── learn_from_traces() — delegates to lazy-init TraceAnalyser
└── learn_from_feedback()— runs learning_tail from last ask()
RRStep (RecursiveAgent subclass — composable iterative step)
├── __call__() — StepProtocol entry; usable in any runner's pipeline
├── reflect() — ReflectorLike entry; drop-in reflector for runners
└── _run_reflection() — PydanticAI agent with execute_code and recurse tools
All runners compose a Pipeline rather than extending it.
Encapsulates everything runners have in common: the epoch loop and Iterable validation. Per-sample iteration, error handling, background execution, and checkpoints are all delegated to Pipeline.run().
Subclasses only override run() (public signature) and _build_context() (input mapping).
Responsibilities:
| Concern | Owner |
|---|---|
| Epoch loop + Iterable validation | ACERunner._run() |
| Per-sample iteration + error isolation | Pipeline.run() |
| Foreground/background split | Pipeline.run() (via async_boundary) |
| Concurrent workers | Pipeline.run(workers=N) |
| Checkpoints | CheckpointStep (in the pipeline) |
| Background drain | ACERunner.wait_for_background() → Pipeline.wait_for_background() |
| Skillbook I/O | save(path) on the runner |
Each sample is independent — no state persists across samples. The skillbook is the only cross-sample coupling.
Eventual consistency: SkillbookView is a thin delegation wrapper, not a snapshot — it reads from the live Skillbook at call time. When background learning is active, concurrent samples may observe partially-updated skillbook state. This is by design: steps see a best-effort view rather than a point-in-time snapshot. The trade-off is acceptable because (1) the skillbook is LLM prompt context where a few missing or extra skills have negligible impact, (2) serialising reads would eliminate the concurrency benefit, and (3) write steps already run with max_workers = 1.
Analyses pre-recorded traces without executing an agent. Runs the learning tail only. Accepts raw trace objects of any type.
When to use: You have execution logs from an external system and want to build or refine a skillbook from historical data. Multi-epoch mode re-processes all traces with the evolving skillbook.
Pipeline:
[ReflectStep] → [UpdateStep] (SkillManager mutates the skillbook directly)
No AgentStep, no EvaluateStep. The trace already contains the agent's output and the evaluation feedback.
Multi-epoch semantics: Each epoch re-processes all traces with the current skillbook. Early epochs extract obvious patterns; later epochs refine and consolidate.
The full live adaptive pipeline. An agent executes, the reflector analyses, the skill manager updates. Optionally evaluates against a TaskEnvironment for feedback-driven learning.
When to use: Building a new agent, or running closed-loop learning where the agent improves in real time.
Pipeline:
[AgentStep] → [EvaluateStep] → [ReflectStep] → [UpdateStep] (SkillManager mutates the skillbook directly)
A single class handles both single-pass (epochs=1) and multi-epoch batch training (epochs > 1). The environment is optional — when provided, EvaluateStep generates feedback. When omitted, the Reflector learns from ground-truth comparison or the agent's reasoning alone.
ACELiteLLM is not an ACERunner subclass. It wraps two different runners (ACE and TraceAnalyser) and exposes a fundamentally different API:
| Method | What it does |
|---|---|
ask(question, context) |
Direct Agent call — no pipeline. Stores interaction for learn_from_feedback() |
learn(samples, environment, epochs) |
Delegates to lazy-init ACE runner |
learn_from_traces(traces, epochs) |
Delegates to lazy-init TraceAnalyser |
learn_from_feedback(feedback, ground_truth) |
Manual single-shot learning from last ask() call |
Runners are cached and invalidated on load() (new skillbook object means stale references).
All runners provide a from_roles factory that takes pre-built role instances. Integration runners also provide from_model() that auto-builds PydanticAI-backed roles from a model string.
Common parameters on from_roles:
| Parameter | Default | Description |
|---|---|---|
skillbook |
Skillbook() |
Starting skillbook |
dedup_manager |
None |
Appends a DeduplicateStep |
dedup_interval |
10 |
Deduplication frequency |
checkpoint_dir |
None |
Appends a CheckpointStep |
checkpoint_interval |
10 |
Checkpoint frequency |
extra_steps |
None |
Additional steps appended after the learning tail |
Every integration assembles the same [Reflect → Update → Apply] suffix. learning_tail() returns this standard step list, with optional dedup and checkpoint steps. If the provided reflector already exposes provides = {'reflections'} (e.g. RRStep), it's inserted directly instead of being wrapped in ReflectStep.
External frameworks integrate via composable pipeline steps in ace/integrations/. Each integration provides:
- Result type — an integration-specific dataclass (e.g.
BrowserResult,ClaudeCodeResult) - Execute step — INJECT skillbook context + EXECUTE the framework, writes to
ctx.trace - ToTrace step — converts the integration-specific result into the standardised trace dict
Standard ACE: [Agent → Evaluate] → [Reflect → Update → Apply]
╰── execute (built-in) ──╯ ╰──────── learn (shared) ──────╯
provides: trace (dict) ─────────────────► requires: trace
Browser-use: [BrowserExecute] → [BrowserToTrace] → [Reflect → Update → Apply]
╰── execute ────╯ ╰── convert ──╯ ╰──────── learn (shared) ──────╯
provides: trace rewrites trace requires: trace
(BrowserResult) (BrowserResult → dict)
TraceAnalyser: [_build_context] → [Reflect → Update → Apply]
╰── sets ctx.trace (raw object) ───────╯ ╰──────── learn (shared) ──────╯
The standardised trace dict keys match what ReflectStep expects: question, reasoning, answer, skill_ids, feedback, ground_truth.
| Integration | Result type | Key fields |
|---|---|---|
| Browser-use | BrowserResult |
task, success, output, error, steps_count, duration_seconds, cited_skill_ids, chronological_steps, raw_history |
| Claude Code | ClaudeCodeResult |
task, success, output, execution_trace, returncode, error |
| Claude SDK | ClaudeSDKResult |
task, success, output, error, model, stop_reason, input_tokens, output_tokens, tool_calls, cited_skill_ids |
| LangChain | LangChainResult |
task, output, result_type, success, error, intermediate_steps, messages, raw_result |
Splitting execute from trace conversion gives independent testability, reusability (execute step usable standalone), and separation of concerns (framework interaction vs trace formatting).
| Integration Runner | TraceAnalyser | |
|---|---|---|
| When | Live execution | Post-hoc analysis |
| Agent | Framework runs it | Already ran |
| Feedback | Generated live | Baked into trace |
| Use case | Production deployment | Historical batch learning, debugging |
Both update the same skillbook. A common workflow: TraceAnalyser builds an initial skillbook from historical data, then an integration runner refines it during live deployment.
MCP Server is a different pattern. It does not add pipeline steps — it's a thin async layer over
ACELiteLLMthat exposes ACE as an MCP tool provider. See MCP Server docs.
- API keys never appear in ACE APIs — no
api_keyparameter anywhere. Keys are resolved from the environment by LiteLLM at call time. - Per-role model selection — Agent, Reflector, and SkillManager can each use different models.
- Validate before running —
ace setupandvalidate_connection()make a tiny LLM call to verify auth before writing code.
ModelConfig— which model to use for a role (model string, temperature, max_tokens). No secrets.ACEModelConfig— model selection per ACE role. Serialises to/fromace.toml(committable, no secrets).
| Constructor | Input | Use case |
|---|---|---|
ACELiteLLM.from_setup() |
ace.toml + .env |
Teams, CI, guided setup |
ACELiteLLM.from_config(config) |
ACEModelConfig object |
Per-role model selection in code |
ACELiteLLM.from_model("gpt-4o") |
Model string | Quick start, single model |
ACELiteLLM("gpt-4o-mini", ...) |
Model string + overrides | Full control |
| Command | What it does |
|---|---|
ace setup |
Interactive wizard: model name, API key, validate, assign per-role models. Saves .env + ace.toml. |
ace models [query] |
Search LiteLLM's model registry (2,600+ models). Filter by --provider. |
ace providers |
List providers with env var names and key status. |
ace validate <model> |
Test a model connection with a tiny LLM call. |
| File | Secrets? | Committable? | Purpose |
|---|---|---|---|
.env |
Yes | No (gitignored) | API keys only |
ace.toml |
No | Yes | Model names + parameters per role |
API Key: .env → os.environ → LiteLLM reads OPENAI_API_KEY / ANTHROPIC_API_KEY / etc.
Model: ace.toml → ACEModelConfig.for_role("agent") → resolve_model(model) → PydanticAI agent
ACE model strings follow LiteLLM convention (provider/model). The resolver in ace/providers/pydantic_ai.py routes them through three paths:
- PydanticAI-native prefix — strings like
openai:gpt-4opass through unchanged - LiteLLM prefix → native provider — when the first path segment matches a PydanticAI native provider,
/is rewritten to:(e.g.bedrock/model→bedrock:model) - Fallback — everything else is prefixed with
litellm:for the proxy provider
LiteLLM string → PydanticAI string
───────────────────────────────────────────────── ──────────────────────────────────────────
gpt-4o-mini → litellm:gpt-4o-mini
bedrock/eu.anthropic.claude-haiku-4-5-v1:0 → bedrock:eu.anthropic.claude-haiku-4-5-v1:0
groq/llama-3.1-70b-versatile → groq:llama-3.1-70b-versatile
openrouter/anthropic/claude-3.5-sonnet → openrouter:anthropic/claude-3.5-sonnet
anthropic/claude-3-5-sonnet-20241022 → anthropic:claude-3-5-sonnet-20241022
ollama/llama3 → litellm:ollama/llama3
together_ai/meta-llama/Llama-3-70b → litellm:together_ai/meta-llama/Llama-3-70b
Mapped LiteLLM prefixes: anthropic, azure, azure_ai, bedrock, cohere, deepseek, groq, mistral, openrouter, vertex_ai. All others fall through to litellm:.
Native providers are faster (no proxy hop) and use the provider's own API key env vars directly. Install with extras: uv add "pydantic-ai-slim[anthropic,openai,bedrock]".
Skill deduplication subsystem in ace/deduplication/ — fully self-contained.
| Class | Role |
|---|---|
SimilarityDetector |
Computes embeddings, detects similar pairs via cosine similarity |
DeduplicationManager |
Coordinates detection and consolidation |
Embedding providers: LiteLLM (remote) or sentence-transformers (local, lazy-loaded).
Consolidation operations:
| Operation | Effect |
|---|---|
MergeOp |
Combine skills — accumulate counters, soft-delete others |
DeleteOp |
Soft-delete a redundant skill |
KeepOp |
Store a similarity decision so the pair is not flagged again |
UpdateOp |
Refine content to differentiate, clear embedding |
Pipeline integration: Deduplication runs as a separate DeduplicateStep at a configurable interval, not inside the SkillManager. Appended by factory methods when a DeduplicationManagerLike is provided.
PydanticAI has first-class Logfire integration. One call auto-instruments everything:
logfire.configure()
logfire.instrument_pydantic_ai()This automatically captures agent runs, tool calls, model requests, structured output validation, and sub-agent delegation — the entire RR execution appears as a structured trace. No custom span-building code.
Pipeline integration: Logfire is OpenTelemetry-based, so pipeline-level spans coexist. Steps that don't use PydanticAI can use logfire.span() / logfire.info() directly.
Setup: ace/observability/configure_logfire() auto-instruments all PydanticAI agents. Opt-in via ACELiteLLM(logfire=True). Config is purely env-based (LOGFIRE_TOKEN), no changes to ace.toml.
Both TraceAnalyser and ACE inherit async capabilities from the pipeline engine. No custom async machinery is needed.
ReflectStep.async_boundary = True means everything before it (Agent, Evaluate) runs in the foreground, and everything from ReflectStep onwards runs in a background thread pool:
sample 1: [AgentStep] [EvaluateStep] ──fire──► [ReflectStep] [UpdateStep]
sample 2: [AgentStep] [EvaluateStep] ──fire──► ...
↑
async_boundary
| Knob | Where | Effect |
|---|---|---|
ReflectStep.max_workers = 3 |
Step class attribute | Up to 3 reflections in parallel |
UpdateStep.max_workers = 1 |
Step class attribute | Serialises skill manager LLM calls AND skillbook writes (SM tools mutate in place) |
wait_for_background(timeout) |
Runner method | Blocks until background threads drain |
ACE inherits cancellation from the pipeline engine. Pass a CancellationToken to run(). The pipeline checks it before each foreground step. Within a step, PydanticAI's async runtime handles cancellation natively. The token flows via contextvars.ContextVar — no parameter changes needed across layers. See PIPELINE_DESIGN.md § Cancellation.
Follows the pipeline engine's error model without additions.
- Per-sample isolation: A failing sample does not abort the run. The exception is recorded in
SampleResult.errorandSampleResult.failed_at. - Background failures: Captured and attached to
SampleResultby the pipeline engine. - No retry logic in the runner. Retries are the responsibility of individual steps (e.g., PydanticAI's built-in retry with error feedback).
ace/
__init__.py ← Public API re-exports
core/
context.py ← ACEStepContext, SkillbookView, ACESample
insight_source.py ← TraceIdentity, TraceReference, InsightSource
outputs.py ← AgentOutput, ReflectorOutput, SkillManagerOutput
skillbook.py ← Skill, Skillbook, SimilarityDecision
environments.py ← Sample, TaskEnvironment, SimpleEnvironment
protocols/ ← Role protocols (one file per protocol)
agent.py, reflector.py, skill_manager.py, deduplication.py
implementations/ ← PydanticAI-backed role implementations
agent.py, reflector.py, skill_manager.py, helpers.py, prompts.py
steps/ ← Pipeline steps (one file per class)
__init__.py ← learning_tail() helper
agent.py, evaluate.py, reflect.py, update.py,
apply.py, deduplicate.py, checkpoint.py,
load_traces.py, persist.py, export_markdown.py, observability.py
runners/ ← Runner classes
base.py ← ACERunner
trace_analyser.py, ace.py, browser_use.py, langchain.py,
claude_code.py, litellm.py
integrations/ ← Integration steps (execute + result + converter)
browser_use.py, langchain.py, claude_code.py, claude_sdk.py
openclaw/ ← OpenClaw trace converter
mcp/ ← Optional MCP server
providers/ ← PydanticAI model resolution
pydantic_ai.py, config.py, registry.py
deduplication/ ← Skill deduplication subsystem
detector.py, manager.py, operations.py, prompts.py
rr/ ← Recursive Reflector (PydanticAI agent)
observability/ ← Logfire configuration
| Module | Contents |
|---|---|
ace/core/ |
ACEStepContext, SkillbookView, Skillbook, AgentOutput, ReflectorOutput, InsightSource |
ace/protocols/ |
AgentLike, ReflectorLike, SkillManagerLike protocols |
ace/implementations/ |
PydanticAI-backed Agent, Reflector, SkillManager |
ace/steps/ |
All pipeline steps + learning_tail() |
ace/runners/ |
ACERunner, TraceAnalyser, ACE, BrowserUse, LangChain, ClaudeCode, ACELiteLLM |
ace/providers/ |
resolve_model, ACEModelConfig, validate_connection |
ace/steps/rr_step.py |
RRStep (RecursiveAgent subclass), RRConfig, TraceSandbox |
ace/core/recursive_agent.py |
RecursiveAgent, AgenticConfig, AgenticDeps, compaction, recursion, usage_callback hook |
ace/core/metered_model.py |
MeteredModel — pydantic-ai WrapperModel that fires the usage_callback once per request |
ace/integrations/ |
Execute steps, result types, ToTrace converters; MCP server |
ace/deduplication/ |
Dedup subsystem (detector, manager, operations) |
ace/observability/ |
Logfire configuration (configure_logfire()) |
Issues acknowledged but deferred.
Streaming / lazy iteration: _run() eagerly materializes the full iterable before passing to Pipeline.run(). True streaming would require the pipeline to accept an iterator. Deliberate simplification — revisit if memory pressure from large single-pass runs becomes real.
Builder API for custom pipelines: The current API offers two extremes: factory methods that hide the pipeline, and manual construction that requires understanding step contracts. A builder could bridge this gap, but learning_tail() covers the most common customisation (custom execute step + standard learning). Worth pursuing when users hit friction with manual wiring.
Skillbook rollback and versioning: Currently the skillbook is mutated in place with no undo. A lightweight versioning mechanism (snapshotting at epoch boundaries, rollback(to_version)) would enable automatic revert when metrics degrade. Deferred because checkpoints cover the common recovery scenario.
LiteLLM proxy base URL support: Users running a LiteLLM proxy may need api_base configuration. Deferred because all current users connect directly to providers.