Replayable Autoresearch Ledger

July 17, 2026 ยท View on GitHub

The Rust SDK exposes Autohand's persisted autoresearch engine through typed JSON-RPC methods. A session proposes focused changes, evaluates each candidate, records immutable measurements and decisions under .auto/, and can replay or rescore earlier attempts without losing their original evidence.

Use a CLI build with autoresearch RPC support. Stopping a session pauses it; the persisted ledger remains available to resume, inspect, replay, or prune.

Start or resume

use autohand_sdk::{
    Agent, AutoresearchConstraint, AutoresearchConstraintOperator,
    AutoresearchOptimizationDirection,
    AutoresearchSamplingOptions, AutoresearchStartParams,
};

let started = agent
    .start_autoresearch(AutoresearchStartParams {
        objective: "Reduce checkout p95 latency without changing behavior".into(),
        max_iterations: Some(12),
        metric_name: Some("p95_ms".into()),
        metric_unit: Some("ms".into()),
        direction: Some(AutoresearchOptimizationDirection::Lower),
        measure_command: Some(
            "cargo test --release --bench checkout -- --sample-size 20".into(),
        ),
        checks_command: Some("cargo test".into()),
        files_in_scope: vec!["src/checkout/".into(), "src/cache/".into()],
        sampling: Some(AutoresearchSamplingOptions {
            min_samples: Some(3),
            max_samples: Some(7),
            confidence_threshold: Some(0.9),
        }),
        constraints: vec![AutoresearchConstraint {
            metric_name: "error_rate".into(),
            operator: AutoresearchConstraintOperator::LessThanOrEqual,
            threshold: 0.0,
        }],
        ..AutoresearchStartParams::default()
    })
    .await?;

if !started.success {
    return Err(autohand_sdk::Error::StructuredOutput(
        started.error.unwrap_or_else(|| "autoresearch could not start".into()),
    ));
}

Calling start_autoresearch for an existing paused session resumes its stored configuration. The returned instruction is the loop prompt. Pass it to Agent::send when your host owns each iteration. For the slash-command path, use agent.autoresearch(objective).await?.

Inspect persisted evidence

let status = agent.get_autoresearch_status().await?;
let history = agent.get_autoresearch_history().await?;

for attempt in &history.attempts {
    println!(
        "{} replayable={} pinned={} state={:?}",
        attempt.attempt_id,
        attempt.replayable,
        attempt.pinned,
        attempt.materialization,
    );
}

Each replayable attempt may contain its latest AutoresearchEvaluationRecord and AutoresearchDecisionRecord. Samples retain all configured metrics; aggregates use median and median absolute deviation.

Replay, rescore, compare, and Pareto

Replay evaluates a candidate in an isolated worktree:

let original = agent
    .replay_autoresearch(AutoresearchReplayParams {
        attempt_id: candidate.attempt_id.clone(),
        evaluator: Some(AutoresearchEvaluatorMode::Original),
    })
    .await?;

let current = agent
    .replay_autoresearch(AutoresearchReplayParams {
        attempt_id: candidate.attempt_id.clone(),
        evaluator: Some(AutoresearchEvaluatorMode::Current),
    })
    .await?;

AutoresearchRescoreParams is an enum with constructors that make invalid attempt/all combinations unrepresentable:

let one = agent
    .rescore_autoresearch(AutoresearchRescoreParams::attempt(&candidate.attempt_id))
    .await?;
let all = agent
    .rescore_autoresearch(AutoresearchRescoreParams::all())
    .await?;

Compare attempts and inspect the constraint-passing, non-dominated frontier:

let comparison = agent
    .compare_autoresearch(AutoresearchCompareParams {
        left_attempt_id: "attempt-1".into(),
        right_attempt_id: "attempt-2".into(),
    })
    .await?;
let pareto = agent.get_autoresearch_pareto().await?;

Pin and prune safely

agent
    .pin_autoresearch(AutoresearchPinParams {
        attempt_id: candidate.attempt_id.clone(),
        pinned: true,
    })
    .await?;

let preview = agent
    .prune_autoresearch(AutoresearchPruneParams {
        dry_run: Some(true),
        yes: None,
    })
    .await?;

let applied = agent
    .prune_autoresearch(AutoresearchPruneParams {
        dry_run: Some(false),
        yes: Some(true),
    })
    .await?;

Always show the preview and request explicit confirmation before applying.

Typed events and hooks

Autoresearch notifications use SdkEvent like every other stream event. Call event.autoresearch() to decode either AutoresearchEvent::Lifecycle or AutoresearchEvent::Operation:

if let Some(event) = event.autoresearch() {
    match event? {
        AutoresearchEvent::Lifecycle(event) => {
            println!("autoresearch {:?}: {}", event.phase, event.status_text);
        }
        AutoresearchEvent::Operation(event) => {
            println!("autoresearch {:?} {:?}", event.operation, event.phase);
        }
    }
}

AutoresearchHookEvent serializes to the CLI's canonical hook names from autoresearch:start through autoresearch:complete and autoresearch:error.

See examples/27-autoresearch-ledger.rs for a runnable end-to-end workflow.