File size: 17,518 Bytes
116524e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
# Recursive Reflector (RR) Design

Design document for the Recursive Reflector (`ace/steps/rr_step.py`). The RR is a PydanticAI-powered trace analyser that uses tool calls to execute Python code in a sandbox, decompose complex inputs via recursive child sessions, and produce structured reflections from agent execution traces.

---

## Overview

The Recursive Reflector replaces the single-pass `Reflector` with an iterative tool-calling agent. Instead of asking the LLM for a one-shot analysis, RR gives the LLM two tools β€” `execute_code` and `recurse` β€” and lets it explore trace data programmatically and decompose large inputs into focused sub-problems.

**Key properties:**

- `RRStep` is a subclass of `RecursiveAgent` (`ace/core/recursive_agent.py`).
- Satisfies both `StepProtocol` and `ReflectorLike` β€” usable as a pipeline step or a drop-in reflector replacement.
- Uses a single tool-using PydanticAI agent with `PromptedOutput(ReflectorOutput)`.
- The same RR agent gathers evidence with tools, records intermediate observations, and returns the final structured `ReflectorOutput`.
- Two-tier compaction (microcompaction + full summarization) handles context-window pressure.
- Depth-based recursion via the `recurse` tool decomposes large/complex inputs.
- PydanticAI's `UsageLimits` enforces token and request budgets.
- Produces `ReflectorOutput` with an enriched `raw["rr_trace"]` dict for observability.

```python

from ace.steps.rr_step import RRStep, RRConfig



# Drop-in replacement for Reflector

ace = ACELiteLLM(llm, reflector=RRStep("gpt-4o-mini", config=RRConfig(max_requests=30)))



# Or as a pipeline step

pipe = Pipeline([..., RRStep("gpt-4o-mini"), ...])

```

---

## Architecture

### Inheritance

```

RecursiveAgent (ace/core/recursive_agent.py)

  β”œβ”€β”€ execute_code tool (generic)

  β”œβ”€β”€ recurse tool (generic, depth-based)

  β”œβ”€β”€ Two-tier compaction

  β”œβ”€β”€ Budget management (UsageLimits)

  β”œβ”€β”€ create_sandbox() helper

  └── on_compaction() callback



RRStep(RecursiveAgent) (ace/steps/rr_step.py)

  β”œβ”€β”€ RR-specific prompt building

  β”œβ”€β”€ Trace/sandbox setup

  β”œβ”€β”€ output_validator tool (ensure exploration before concluding)

  β”œβ”€β”€ Timeout/error fallback with ground-truth comparison

  └── Online mode skill evaluation

```

### Agent Loop

```

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”

β”‚  RRStep._run_reflection()                                     β”‚

β”‚                                                               β”‚

β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚

β”‚  β”‚  PydanticAI Agent (model, output_type=ReflectorOutput)  β”‚  β”‚

β”‚  β”‚                                                         β”‚  β”‚

β”‚  β”‚  Tools:                                                 β”‚  β”‚

β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                         β”‚  β”‚

β”‚  β”‚  β”‚ execute_code β”‚  β”‚ recurse  β”‚                         β”‚  β”‚

β”‚  β”‚  β”‚  (sandbox)   β”‚  β”‚ (child   β”‚                         β”‚  β”‚

β”‚  β”‚  β”‚              β”‚  β”‚  session) β”‚                         β”‚  β”‚

β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜                         β”‚  β”‚

β”‚  β”‚         β”‚               β”‚                               β”‚  β”‚

β”‚  β”‚         β–Ό               β–Ό                               β”‚  β”‚

β”‚  β”‚    TraceSandbox    Child RRStep                          β”‚  β”‚

β”‚  β”‚    exec() env      (own sandbox,                        β”‚  β”‚

β”‚  β”‚                     own budget)                          β”‚  β”‚

β”‚  β”‚                                                         β”‚  β”‚

β”‚  β”‚  Output:                                                β”‚  β”‚

β”‚  β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚  β”‚

β”‚  β”‚  β”‚  ReflectorOutput (structured, validated)          β”‚  β”‚  β”‚

β”‚  β”‚  β”‚  + output_validator enforces exploration depth    β”‚  β”‚  β”‚

β”‚  β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚  β”‚

β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚

β”‚                                                               β”‚

β”‚  UsageLimits(total_tokens_limit, request_limit)               β”‚

β”‚  β†’ compaction on context window pressure                      β”‚

β”‚  β†’ BudgetExhausted when total budget spent                    β”‚

β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

```

### Tools

| Tool | Signature | Defined in | Description |
|------|-----------|------------|-------------|
| `execute_code` | `(code: str) -> str` | `RecursiveAgent` | Run Python in the `TraceSandbox`. Variables persist across calls, so the tool owns working state for evidence gathering: define variables, extract slices, compute checks, and verify contradictions. Tool output should stay terse and factual. It must not be used to print reflections, summaries, lessons, insights, analysis, or final reflection prose; those belong in `ReflectorOutput`. Raises `ModelRetry` on exceptions. |
| `think` | `(thought: str, evidence_refs: list[str] \| None) -> dict` | `RRStep` | Scratch prose channel for short working notes during the run (e.g. "mismatch confirmed, one more passenger-count check"). Notes are surfaced in `output.raw["thoughts"]` for inspection but **do not** propagate to the SkillManager. Conclusions, root cause, and key insight must therefore go in `ReflectorOutput`, not here. Persistent state for handoff to a sub-`recurse` belongs in a sandbox variable, not in `think`. |
| `recurse` | `(prompt: str, context_code: str) -> str` | `RecursiveAgent` | Spawn a child session with its own sandbox. Child inherits data and helpers. Use `context_code` to prepare the child's data. Not available at max depth. |
| `output_validator` | (on output) | `RRStep` | Ensures the RR agent has used `execute_code` at least once before producing its final `ReflectorOutput`. |

RR uses one tool-capable structured-output agent. It may call `execute_code`,
`think`, skillbook inspection tools, and `recurse`, then stops using tools and
returns `ReflectorOutput` directly. There is no second conversion agent.
RR specializes the generic `execute_code` tool description for this step so the
model sees it as an evidence workbench rather than a prose-reporting channel.
RR also defaults to `temperature=0.0` for deterministic evidence analysis unless
the caller passes explicit `model_settings`.
For small traces, the generated data summary tells RR to use only a few focused
code checks and avoid transcript walkthroughs.

### Dual Protocol Support

```python

class RRStep(RecursiveAgent):

    # StepProtocol β€” place in any Pipeline

    requires = frozenset({"trace", "skillbook"})

    provides = frozenset({"reflections"})



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



    # ReflectorLike β€” use as drop-in reflector in runners

    def reflect(self, *, question, agent_output, skillbook, ...) -> ReflectorOutput: ...

```

---

## Configuration

### AgenticConfig (base)

Defined in `ace/core/recursive_agent.py`. All fields inherited by `RRConfig`.

| Parameter | Default | Description |
|-----------|---------|-------------|
| `max_tokens` | `500_000` | Total token budget per agent run. When exhausted β†’ `BudgetExhausted`. |
| `max_requests` | `50` | Safety cap on LLM requests per agent run. When hit β†’ `BudgetExhausted`. |
| `context_window` | `128_000` | Model context window size. |
| `max_depth` | `2` | Max recursion depth. At max depth, `recurse` tool is not registered. |
| `child_budget_fraction` | `0.5` | Fraction of remaining token budget given to each child session. |
| `max_compactions` | `3` | Safety cap on full summarization rounds per session. |
| `microcompact_keep_recent` | `3` | Number of most recent tool results preserved during microcompaction. |
| `timeout` | `60.0` | Seconds per sandbox `execute()` call. Uses `signal.SIGALRM` on Unix. |
| `max_output_chars` | `20_000` | Per-execution stdout/stderr truncation limit. |
| `usage_callback` | `None` | Optional `(RequestUsage, model_id) -> None` hook fired once per completed pydantic-ai request (orchestrator turn, child session, compaction summary). Callback exceptions are swallowed, so a broken meter never crashes a run. Implemented via `ace.core.metered_model.MeteredModel`. |

### RRConfig (alias for RecursiveConfig)

Defined in `ace/implementations/rr/config.py`. Extends `AgenticConfig`.

| Parameter | Default | Description |
|-----------|---------|-------------|
| `max_output_chars` | `50_000` | Override: larger limit for trace analysis output. |

All other fields are inherited from `AgenticConfig` with the same defaults.

```python

from ace.steps.rr_step import RRConfig



config = RRConfig(

    max_requests=20,

    max_depth=2,

    timeout=60.0,

    max_output_chars=50_000,

)

```

---

## Dependencies

### AgenticDeps (base)

Defined in `ace/core/recursive_agent.py`.

| Field | Type | Description |
|-------|------|-------------|
| `config` | `AgenticConfig` | Configuration |
| `sandbox` | `Any` | TraceSandbox or compatible (used by `execute_code` and `recurse` tools) |
| `depth` | `int` | Current recursion depth |
| `max_depth` | `int` | Maximum recursion depth |
| `iteration` | `int` | Number of `execute_code` calls (incremented by the tool) |
| `run_session_fn` | `Callable` | Callback for spawning child sessions (wired by `RecursiveAgent.run()`) |
| `parent_usage_tokens` | `int` | Token usage from parent (for child budget computation) |

### RRDeps

Defined in `ace/implementations/rr/tools.py`. Extends `AgenticDeps`.

| Field | Type | Description |
|-------|------|-------------|
| `trace_data` | `dict[str, Any]` | The canonical traces dict |
| `skillbook_text` | `str` | Skillbook text |

---

## TraceSandbox

Lightweight `exec()`-based sandbox for running LLM-generated Python code. Located in `ace/core/sandbox.py`.

**Not a security sandbox.** Restricts builtins as defence-in-depth but relies on trusting the LLM not to generate malicious code.

### Pre-loaded Namespace

| Variable | Type | Description |
|----------|------|-------------|
| `traces` | `Any` | Raw trace payload (injected by `RRStep`) |
| `skillbook` | `str` | Skillbook text (injected by `RRStep`) |
| `helper_registry` | `dict` | Metadata for registered reusable helper functions |
| `register_helper` | `Callable` | Define and persist helper code for later calls and child sessions |
| `list_helpers` | `Callable` | Return registered helper names and descriptions |
| `run_helper` | `Callable` | Invoke a registered helper by name |
| `SHOW_VARS` | `Callable` | Print available variables (debugging) |
| `json`, `re`, `math`, `collections` | module | Standard library modules |
| `datetime`, `timedelta`, `date`, `time`, `timezone` | class | datetime classes |

### Blocked Builtins

`open`, `eval`, `exec`, `compile`, `input`, `globals`, `locals`, `breakpoint`, `memoryview` β€” all set to `None`. `__import__` is replaced with a safe import that only allows pre-loaded modules.

### ExecutionResult

```python

@dataclass

class ExecutionResult:

    stdout: str = ""

    stderr: str = ""

    final_value: Any = None

    exception: Optional[Exception] = None



    @property

    def success(self) -> bool:

        return self.exception is None

```

### Timeout Behaviour

- **Unix (main thread):** Uses `signal.SIGALRM`. Raises `ExecutionTimeoutError` after `config.timeout` seconds.
- **Windows / non-main thread:** No timeout enforcement.

### Runtime Helper Registry

- `register_helper(name, source, description)` executes helper source code, stores it, and records metadata.
- Registered helpers persist across `execute_code` calls within the same session.
- Child sessions (via `recurse`) inherit registered helpers automatically.

---

## Compaction

When the agent's context window fills up, two-tier compaction kicks in:

```

agent running

    ↓

PydanticAI: UsageLimitExceeded

    ↓

Budget exhausted? β†’ YES: raise BudgetExhausted β†’ fallback output

                  β†’ NO: context window hit, continue ↓

    ↓

Tier 1: microcompact(messages, keep_recent=3)

    - Clear old execute_code tool results

    - Keep last 3 tool results intact

    - Keep all model messages (reasoning chain)

    ↓

Changed? β†’ YES: retry with compacted history

         β†’ NO: fall through to tier 2 ↓

    ↓

Tier 2: summarize_and_compact()

    - compaction_count++ (cap at max_compactions=3)

    - LLM summarizes progress (1 request from budget)

    - Save pre-compaction context to sandbox `history` variable

    - Replace history with [summary + continuation prompt]

    - Retry with compacted history

```

### Compaction Callback

`RecursiveAgent.on_compaction()` saves compaction metadata to the sandbox's `history` variable so the agent can reference prior context after compaction.

---

## Recursion

The `recurse` tool enables depth-based decomposition:

- Root agent runs at `depth=0` with `recurse` available (if `max_depth > 0`)
- Each `recurse` call spawns a child at `depth + 1` with its own sandbox and budget
- At `depth == max_depth`, `recurse` is not registered β€” the agent must analyze directly
- Child sandbox inherits all non-internal, non-callable variables from parent
- Registered helpers are rehydrated in child sandboxes
- Child budget: `remaining_tokens * child_budget_fraction`

---

## Timeout / Fallback

When `BudgetExhausted` is raised (token or request budget spent):

1. `RRStep._build_budget_exhausted_output()` constructs a `ReflectorOutput` with `raw["timeout"] = True`.
2. If `agent_output` and `ground_truth` are available, `_build_timeout_output()` includes a simple correct/incorrect assessment.

When any other exception occurs, a minimal `ReflectorOutput` is returned with `raw["error"]`.

---

## Online Mode Skill Evaluation

When `ctx.mode == "online"` and the skillbook is non-empty, `RRStep` appends skill evaluation instructions to the prompt. The agent:

1. Scans trace text for skill ID citations (`[section-NNNNN]`)
2. Verifies each cited ID exists in the skillbook
3. Classifies each as `helpful`, `harmful`, or `neutral`
4. Includes results in the `skill_tags` output field

In offline mode, skill evaluation is skipped (traces may be from external agents with no skill IDs).

---

## Traces Input

The `traces` variable in the sandbox contains the raw data structure:

```python

{

    "question": str,              # The question/task

    "ground_truth": str | None,   # Expected answer

    "feedback": str | None,       # Environment feedback

    "steps": [                    # Agent execution steps

        {

            "role": "agent",

            "reasoning": str,

            "answer": str,

            "skill_ids": list[str],

        }

    ],

}

```

For arbitrary trace inputs, the agent discovers the structure via `execute_code` and decomposes via `recurse` if needed.

---

## rr_trace Output Schema



`RRStep` enriches `ReflectorOutput.raw` with execution metadata:



```python

{

    "rr_trace": {
        "total_iterations": int,   # Number of execute_code calls

        "subagent_calls": list,    # Reserved for future use

        "timed_out": bool,         # Whether budget was exhausted

        "compactions": int,        # Number of compaction rounds

        "depth": int,              # Recursion depth of this session

    },

    "usage": {

        "input_tokens": int,

        "output_tokens": int,

        "total_tokens": int,

        "requests": int,

    },

}

```


---

## Observability

Logfire auto-instruments PydanticAI agents, providing:

- Per-agent-run traces with spans for each LLM request and tool call
- Token usage tracking
- Latency metrics
- No explicit opt-in step required in the pipeline

The `rr_trace` dict in `ReflectorOutput.raw` provides programmatic access to iteration counts and metadata.

---

## Public API

```python

from ace.steps.rr_step import (

    RRStep,              # Main entry point (RecursiveAgent subclass)

    RRConfig,            # Configuration (alias for RecursiveConfig)

    RRDeps,              # PydanticAI RunContext dependencies

    TraceSandbox,        # Sandbox for code execution

    ExecutionResult,     # Result of sandbox.execute()

    ExecutionTimeoutError,

)

```