DFAH quickstart¶
DFAH measures whether repeated executions of the same case reach the same decision and observable tool path. It does not measure correctness. The no-network example uses a deterministic toy agent and a registered local tool. Install the published package; no repository clone or API key is needed.
python -m pip install dfah-bench
Then run:
dfah check-agent --agent dfah.demo:toy_agent --episode-timeout-s 5
dfah run \
--agent dfah.demo:toy_agent \
--replays 3 \
--episode-timeout-s 5 \
--out .dfah/runs/quickstart
dfah analyze .dfah/runs/quickstart \
--report .dfah/runs/quickstart/report.html
The installed demo builds the public contract: a versioned Suite, a
Manifest, a ToolRegistry, an @agent adapter, AgentResult,
WireRequest, and Replay. Its implementation is also available as
examples/dfah_quickstart.py.
Before a replay begins, DFAH also writes an immutable run plan covering the manifest, selected cases/tasks, replay count, seed, sample rate, and shuffled schedule hash. Reusing the directory with a different design fails before an agent call.
The adapter and tool contract¶
An adapter exposes a manifest and returns AgentResult from one async call
per case. The manifest must match the selected suite's ID, version, fixture
hash, and tool-schema hash. Replay also checks that provider, model, adapter,
temperature, top-p, and seed in the recorded WireRequest match the manifest.
Register each tool before running. Its specs must exactly match
Suite.tools after stable name ordering; otherwise
Replay(..., tools=registry) fails closed. DFAH
creates one ToolSession per episode and injects it as context.tools.
Call the session, then return context.tools.trajectory(); a hand-built
trajectory that differs from the session record is a contract error.
import hashlib
from pathlib import Path
from dfah import (
AgentResult, Case, Replay, Suite, ToolRegistry, ToolSpec, Usage,
WireRequest, agent, build_manifest,
)
risk_tool = ToolSpec(
name="read_risk_tier",
input_schema={
"type": "object",
"properties": {"risk_tier": {"type": "string"}},
"required": ["risk_tier"],
"additionalProperties": False,
},
)
suite = Suite(
suite_id="toy-risk",
suite_version="1.0.0",
decisions=("escalate", "dismiss"),
cases=(
Case(
case_id="RISK-001",
artifact_case_id="CASE-001",
input={"risk_tier": "high"},
),
Case(
case_id="RISK-002",
artifact_case_id="CASE-002",
input={"risk_tier": "low"},
),
),
tools=(risk_tool,),
)
tools = ToolRegistry()
@tools.tool(risk_tool)
def read_risk_tier(*, risk_tier: str) -> str:
return risk_tier
parameters = {"temperature": 0.0, "top_p": 1.0, "seed": 42}
implementation_hash = hashlib.sha256(Path(__file__).read_bytes()).hexdigest()
manifest = build_manifest(
suite,
provider="my-provider",
model="my-model",
adapter="my_project.adapter",
adapter_version="1.0.0",
implementation_hash=implementation_hash,
request_parameters=parameters,
)
@agent(manifest=manifest, tools=tools, suite=suite)
async def my_agent(case, context):
assert context.tools is not None
risk = await context.tools.call("read_risk_tier", risk_tier=case.input["risk_tier"])
label = "ESCALATE" if risk == "high" else "DISMISS"
payload = {"model": "my-model", **parameters, "case_id": case.case_id}
return AgentResult(
output_text=f"DECISION: {label}",
trajectory=context.tools.trajectory(),
wire_request=WireRequest.from_payload(
provider="my-provider",
model="my-model",
payload=payload,
parameters=parameters,
adapter="my_project.adapter",
adapter_version="1.0.0",
),
usage=Usage(input_tokens=0, output_tokens=0),
cost_usd=0.0,
)
report = Replay(suite=suite, replays=3, out=".dfah/runs/toy-risk").run(my_agent)
A session returns raw tool values to the adapter but records only canonical
argument and output hashes by default. Its recorded ToolCall.arguments is
empty and arguments_redacted=True; arguments_hash remains available for
strong path agreement. Pass capture_tool_arguments=True to Replay only
when approved artifact policy allows retaining arguments. A requested,
rejected, or failed tool makes the episode a tool_error. Tool arguments are
validated against the declared Draft 2020-12 JSON Schema before the function
can execute; schema errors are recorded without retaining the rejected value.
The decision parser accepts exactly one line DECISION: LABEL, where LABEL
is in the suite ontology. It does not guess, use a modal fallback, or accept a
bare label. A malformed output becomes parse_failure and is ineligible.
A zero-tool session is ChannelState.OBSERVED_EMPTY, not UNAVAILABLE.
Use a registry whenever DFAH should be the source of trajectory capture; a
manually supplied trajectory remains useful only for integrations that do not
use injected tools. Manual calls are still checked against the suite's tool
names and argument schemas, then redacted before persistence.
Read a report¶
Case-level DAR is modal decision agreement. TAR.seq is modal exact ordered
tool-path agreement. Report-level DAR/TAR first average cases within each task,
then weight represented tasks equally; gates use those task-weighted values.
gap is DAR - TAR.seq, using the same retained replay groups.
flags_per_100_cases is 100 times the fraction of eligible case groups where
the decision is unanimous (DAR == 1.0) but the strong path identity changes.
That catches tool-set, multiplicity, order, argument, result-state, and hashed
result changes. sequence_flags_per_100_cases exposes the narrower tool-name
sequence trigger used in the historical study, and each CaseReport provides
path_variation_kind for queue triage. These are review-load signals, not a
percentage of all input cases or a correctness score.
If no replay group is eligible, aggregate metrics are unavailable:
Report.dar, Report.tar, Report.gap, and the flag-rate properties are
None; JSON contains null; and the CLI/HTML/Markdown render —. This is
different from a measured zero gap. Inspect ineligible_groups and the
privacy-safe ineligibility_reasons affected-group counts before changing an
adapter or suite. Gates continue to fail closed when a requested metric has no
eligible denominator.
Privacy and persistence¶
Use a new output directory such as .dfah/runs/...; never target historical
results/, traces/, or paper-artifact paths. The file store is
append-only and writes mode-700 directories, mode-600 artifacts, a SHA-256
sidecar, and a commit marker. Episode records contain the parsed decision,
trajectory, request metadata, usage, cost, latency, and sanitized errors—not
the full AgentResult.output_text.
Reports carry a canonical commitment to the verified episode set. Loading a
run directory rechecks its immutable run plan, start/dispatch boundaries,
episode metadata, wire echoes, ontology, commitment, case metrics, cost, and
latency before a gate can pass. A detached report JSON may be opened only with
allow_unverified=True and is never release-gate evidence by default.
Serialization redacts several common credential shapes. That is a safety net,
not a data-classification system: do not put prompts, customer data, raw tool
results, or secrets into manifest parameters, tool arguments, metadata, or
error text unless an approved artifact policy permits it. The current
capture_raw_content manifest field is recorded as provenance; Replay does
not make it capture raw output.
Resuming safely¶
Episode identity is (manifest_hash, artifact_case_id, replay_index), where
artifact_case_id is the case's explicit pseudonym or, if omitted, its raw
case_id. Set an artifact pseudonym before using production cases. Re-running
the same configuration reuses committed episodes byte-for-byte and does not
call the agent again.
One output directory also permits only one writer process. A second runner
fails before dispatch. A lease left by a dead local process is not displaced
unless the caller explicitly sets recover_stale_lease=True after confirming
that no runner remains active; the stale lease is archived for review.
A durable planned-only record proves no external dispatch boundary was reached,
so DFAH safely resumes it. Immediately before invoking the adapter, DFAH writes
a durable dispatching intent. A dispatching record without a terminal commit is
converted to an interrupted episode with unknown_after_dispatch and is not
reissued, because the provider may already have received the request. Change
the suite version, fixture/tool contract, manifest settings, or output location
deliberately when a new experiment is intended.
Before a paid run, use:
from dfah.testing import check_agent
check_agent(
my_agent,
max_cases=2, # at most four agent calls: two cases x two replays
budget_usd=0.20,
estimated_max_episode_cost_usd=0.05,
episode_timeout_s=30.0,
raise_on_error=True,
)
Pass out=".dfah/conformance/my-agent" when a failed preflight needs durable
local diagnostics. Without out, DFAH returns terminal status/error-kind
counts in the report and removes the temporary episode store.
The conformance report records the selected case count, maximum planned calls,
and estimated cost ceiling. Its replay_stability_smoke reports
replay-visible decision/path variation as an observational warning rather than
an adapter-contract failure; the deterministic-tool check is SKIP, not
PASS, when the agent makes no tool calls. Passing two repetitions does not
prove the absence of all wall-clock or ambient-state dependencies.