Stepfork v0.2 introduces automatic failure minimization using dependency-aware delta debugging. When an AI agent execution fails, minimization helps identify the smallest reproducible trace that preserves the original failure.
Minimization takes a failing trace and systematically removes events while verifying that the failure still reproduces. The result is a minimal trace that:
- Preserves the original failure signature
- Maintains all dependency relationships (parent-child event links)
- Includes only the events necessary to reproduce the failure
- Passes structural validation
This makes debugging easier by eliminating irrelevant execution paths and focusing attention on the core failure.
# Record a failing execution
python my_agent.py # Creates failure.sftrace
# Minimize the trace
stepfork minimize failure.sftrace \
--entrypoint my_agent:run \
--output minimal.sftrace
# The minimal trace reproduces the same failure with fewer events
stepfork replay minimal.sftrace --entrypoint my_agent:run --mode frozenMinimization begins by replaying the original trace to detect its failure signature. The signature captures:
- Exception type and message pattern (e.g.,
ValueError: invalid flight ID) - Replay divergence type (e.g.,
ReplayMismatchError: tool call order changed) - Output mismatch (when expected output is provided)
The signature is used throughout minimization to verify that candidate traces preserve the same failure.
Stepfork uses a dependency-aware variant of delta debugging:
- Start with the full trace
- Try removing subsets of events (tool calls, LLM requests, state changes)
- For each subset, add back required parent events automatically
- Replay the candidate trace and check if it matches the failure signature
- If the failure still reproduces, keep the smaller trace and continue
- Repeat with progressively smaller subsets until no further reduction is possible
Events are connected through parent_id relationships. When minimizing:
- If event B has
parent_id = A.id, removing A would break the trace structure - The minimizer automatically includes all ancestor events when testing a subset
run_startandrun_endevents are always preserved for structural integrity
After minimization completes, the minimized trace is replayed one final time to confirm:
- The failure signature matches the original
- The trace passes structural validation
- No additional events can be removed without losing the failure
stepfork minimize <trace> --entrypoint MODULE:FUNCTION [OPTIONS]| Argument | Description |
|---|---|
<trace> |
Path to the .sftrace bundle to minimize |
--entrypoint, -e |
Agent entrypoint as MODULE:FUNCTION |
| Option | Description | Default |
|---|---|---|
--expect-output FILE |
JSON file with expected output for comparison | None |
--mode, -m |
Replay mode: frozen, live, forbidden, manual, or derived |
frozen |
--output, -o |
Path for the minimized .sftrace bundle |
<trace>-minimized.sftrace |
--overwrite |
Overwrite the output bundle if it exists | false |
--timeout SECONDS |
Timeout for each replay attempt | 30.0 |
--attempts N |
Number of attempts per subset test (for nondeterminism detection) | 1 |
Let's minimize a failing agent that books the wrong flight:
# agent.py
from stepfork import record, trace_tool
@trace_tool(name="search_flights")
def search_flights(destination: str) -> list[dict]:
return [
{"id": "FL123", "dest": "KTM", "price": 800},
{"id": "FL456", "dest": "NYC", "price": 600},
]
@trace_tool(name="book_flight")
def book_flight(flight_id: str) -> dict:
return {"id": flight_id, "status": "confirmed"}
def run_agent(destination: str = "KTM") -> dict:
flights = search_flights(destination)
# BUG: books the second flight instead of checking destination
booked = book_flight(flights[1]["id"])
return {"booking": booked, "destination": destination}
if __name__ == "__main__":
with record("booking-agent", output="failure.sftrace") as session:
result = run_agent()
session.set_output(result)The original trace has 8 events:
run_starttool_call(search_flights)tool_result(search results)tool_call(book_flight)tool_result(booking confirmation)state_change(internal state update)error(failure detected)run_end
Minimize the trace:
# Create expected output file
echo '{"booking": {"id": "FL123", "status": "confirmed"}, "destination": "KTM"}' > expected.json
# Run minimization
stepfork minimize failure.sftrace \
--entrypoint agent:run_agent \
--expect-output expected.json \
--output minimal.sftraceOutput:
Stepfork Minimize
Original trace: failure.sftrace
Agent: booking-agent
Events: 8
Entrypoint: agent:run_agent
Mode: frozen
Detecting failure signature...
Minimizing trace...
╭─ Minimization Report ─────────╮
│ Original Events 8 │
│ Minimized Events 6 │
│ Reduction 25.0% │
│ Replay Attempts 12 │
│ Failure Signature Output Mismatch │
│ Minimal Yes │
╰────────────────────────────────╯
✓ Minimized trace saved to: minimal.sftrace
The minimized trace removed the state_change event (not relevant to the failure) while preserving all dependency calls needed to reproduce the wrong-booking bug.
When the replayed agent raises an exception:
# Trace raises ValueError during replay
Failure Signature: Exception: ValueErrorThe minimizer preserves traces that raise the same exception type with similar message patterns.
When the agent's execution diverges from the recorded trace:
# Agent tries to call a different tool than recorded
Failure Signature: Replay Divergence: ReplayMismatchErrorDivergence failures include:
- Wrong tool called
- Tool called with different arguments
- Extra or missing dependency calls
- Different model used for LLM request
When the agent produces incorrect output (requires --expect-output):
# Agent output doesn't match expected output
Failure Signature: Output MismatchOutput comparison uses canonical JSON serialization, so key order and whitespace don't matter.
Some traces fail in multiple ways:
Failure Signature: Exception: ValueError + Divergence: ReplayMismatchError + Output MismatchAll components must match for the signature to be preserved.
If your agent has nondeterministic behavior (even with frozen replay), use --attempts:
stepfork minimize failure.sftrace \
--entrypoint agent:run \
--attempts 3Each candidate trace is tested 3 times. Only subsets that fail consistently are accepted. Nondeterministic failures are reported in the minimization report with a warning.
- Minimization is not a sandbox. The entrypoint runs with your code and privileges.
- Only instrumented boundaries are frozen. Uninstrumented code may behave differently across replay attempts.
- Timeouts may truncate search. Increase
--timeoutif traces take longer to replay. - Cannot minimize successful traces. The trace must fail during replay to be minimized.
- Manual reduction may be needed. Delta debugging finds a minimal trace, but not necessarily the unique minimal trace. Manual inspection may reveal further simplifications.
Minimization fits into the standard Stepfork workflow:
- Record a failing execution →
failure.sftrace - Minimize the trace →
minimal.sftrace - Inspect the minimal trace →
stepfork inspect minimal.sftrace - Replay to verify →
stepfork replay minimal.sftrace --entrypoint agent:run - Diff against a fix →
stepfork diff minimal.sftrace fixed.sftrace - Export as a regression test →
stepfork export minimal.sftrace --pytest
Using a minimized trace for regression tests makes the test suite faster and easier to understand.
- Replay - How frozen replay works
- CLI Reference - Complete command documentation
- Trace Format - Understanding
.sftracestructure - Examples - Runnable examples with minimization