Spaces:
Sleeping
Sleeping
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,
)
```
|