Skip to content

Capture and resume

Capture saves application answers and their evidence before evaluation. Use it for expensive targets, product-owned Python adapters, or importing recorded application transcripts. A capture completes with status captured; this does not mean it passed an evaluation. Scorers, judges, and reporters are not invoked by capture or resume.

CLI

# Import recorded answers, without calling an application.
alignmenter capture --dataset conversations.jsonl --persona persona.yaml --out reports

# Generate answers through an installed/importable product adapter.
alignmenter capture --dataset conversations.jsonl --target my_eval.target:make_target

# Check the interrupted run before doing more work.
alignmenter resume reports/<run-directory> --target my_eval.target:make_target --check

# Continue missing capture using the frozen inputs and compatible target.
alignmenter resume reports/<run-directory> --target my_eval.target:make_target
alignmenter export-transcripts reports/<run-directory> --out captured.jsonl

Omit --target when resuming recorded inputs or when every generated answer is already committed. For an existing comparison run, pass --compare-target module:factory for the incomplete comparison stream. Every incomplete generated stream is checked before any stream dispatches new work. The standalone capture command creates a primary stream; comparison capture is available through the existing SDK runner configuration.

Resume uses verified source snapshots, so the original dataset and persona files may have moved or been deleted. Optional --dataset PATH and --persona PATH assert that current files still match; they do not replace the frozen inputs. Changed target identity, capabilities, configuration digest, or installed Alignmenter package version blocks continuation. Use a new run for an intentional target change.

--check acquires the coordinator lock and performs preflight without writing the database or calling the target. The target factory is still imported and constructed. Factories must configure their adapter without making application requests. A successful check does not reserve the run after it exits; resume checks again when it acquires the lock. Preflight and execution errors return a nonzero exit status for CI.

Python adapter contract

A target factory returns a CaptureTarget. This example wraps a product's existing answer function; replace the import and configuration with the actual application.

from alignmenter.providers.base import ChatResponse
from alignmenter.providers.callable import CallableProvider, CaptureTarget
from alignmenter.schemas.execution import RecoveryContract, content_digest
from my_app import answer_conversation


def make_target():
    # Include all behavior-affecting configuration, excluding credentials.
    configuration = {
        "app_revision": "your-build-or-commit",
        "model_revision": "your-pinned-model",
        "prompt_digest": "your-prompt-content-digest",
        "retrieval_revision": "your-index-and-retriever-revision",
        "generation": {"temperature": 0},
    }

    def chat(messages, *, request_id):
        # This example makes no idempotency promise and does not use request_id.
        response = answer_conversation(messages, **configuration["generation"])
        return ChatResponse(text=response.text, context=response.context)

    contract = RecoveryContract(
        configuration_digest=content_digest(configuration),
        session_state="stateless",
        interrupted_request="refuse",
        max_attempts=3,
    )
    return CaptureTarget("product:local", CallableProvider(chat, contract))

stateless promises that each request contains its complete conversation state; the adapter does not depend on an unrecorded application session. Resume reconstructs the messages from the committed conversation, including system instructions and prior generated answers. The default refuse policy permits continuation after a committed answer but blocks a previously dispatched request whose answer was not committed.

The configuration digest is an adapter-owned declaration. Include application and adapter code revisions, model revision, prompts, retrieval configuration/data, tools, generation settings, and other behavior-affecting inputs. The runner cannot discover omitted settings or verify that a running deployment matches a declared revision. It stores the digest, not a dump of the provider object or credentials. The Alignmenter package-version check does not fingerprint unversioned edits to an editable installation.

To declare interrupted_request="idempotent", the target must durably deduplicate the provided request_id, associate it with exactly those messages, and return the original response on repetition, including across process restarts. The wrapper only forwards the ID; it does not implement this guarantee. Ordinary repeated model calls or sending an otherwise unsupported request header do not establish idempotency.

Custom chat providers can expose the same typed recovery_contract attribute and accept chat(messages, request_id=...). Legacy providers without that declaration keep their existing chat(messages) interface. Their incomplete generated streams cannot be resumed automatically. session_state="opaque" explicitly records an unsupported session-recovery contract.

from pathlib import Path
from alignmenter.execution.recovery import resume_capture
from alignmenter.runner import RunConfig, Runner

target = make_target()
runner = Runner(
    RunConfig(model=target.model, dataset_path=Path("conversations.jsonl"),
              persona_path=Path("persona.yaml")),
    scorers=[], provider=target.provider,
)
run_dir = runner.capture()

# After interruption, reconstruct the target from the same configuration.
summary = resume_capture(Path("reports/<interrupted-run>"),
                         targets={"primary": make_target()})

Runner.run_dir identifies saved work when capture raises. resume_capture(..., check_only=True) exposes CLI preflight to Python callers. A fully captured run is a no-op; its previous scoring/reporting failure remains visible and is not reclassified as evaluation success.

Recovery guarantees and limits

Boundary Behavior
Answer committed Reuse the same observation, attempt, and transcript record
Missing turn never dispatched Call the compatible stateless adapter with a new request ID
Dispatched answer not committed Require idempotent recovery; reuse the request ID and exact messages
Retry Append a new attempt; retain failed/unknown attempts and enforce the frozen per-turn limit
Abandoned running attempt Reclassify as unknown_outcome when an accepted resume starts under the lock
Late response from an earlier attempt Cannot replace the active attempt or a committed answer
Scoring/reporting failed after capture Leave that failure recorded; make no evaluation calls
Competing coordinator Fail before dispatch while the other process holds the run lock

Each resume invocation makes at most one new attempt for each missing turn, stops on the first execution failure, and never resets the attempt limit. Preflight rejects incompatible or unsupported work before mutating the database. A rejected preflight can therefore leave an abandoned attempt's last recorded state as running; status continues to report saved state with liveness: "not_checked".

The coordinator lock covers fresh runs and resumed runs and is released by the OS on process death. Keep coordinator.lock in place. This is a lease on one run directory on a local filesystem. It is not a cross-machine lease or an exclusive physical-device lease across different runs. Process-kill acceptance tests currently qualify this path on macOS; the Windows lock branch has not been exercised in this environment.

The acceptance tests use an independent target database to distinguish transport attempts from accepted target requests. They kill a runner before acceptance, after acceptance, and after commit, then resume through a fresh CLI process. Additional tests cover changed configuration, full conversation reconstruction, primary/comparison preflight, retry limits, corrupt saved data, and stale attempt rejection.

New run databases use database version 2, allowing multiple attempts per turn. The JSON record schema remains version 1 with optional recovery/plan-identity fields. Version 1 databases remain readable through status, export-transcripts, and RunStore; they are not silently migrated or reopened. Export their saved transcripts into a new recorded run when needed. Unknown database versions are rejected. Export/import does not claim to preserve the original run's complete attempt history.

Stateful reset/replay, physical-device leases, and portable run migration remain planned. Durable rubric evaluations now provide shared judge budgets and saved verdicts through a separate evaluation command. Atlas's current device adapter has not yet established the session and request-identity guarantees needed for automatic recovery. Its captured answers remain available for inspection/export.

To evaluate a completed capture with the current scoring pipeline, export it and run alignmenter run --dataset captured.jsonl --persona persona.yaml without --generate-transcripts. This explicitly creates a new evaluation and can call its configured judges. For versioned rubric results and reusable raw judge replies, use alignmenter evaluate directly on the saved run instead.