Skip to main content

Firedrill Python client

For your first use, authorize and select scope, then run your actual callable. Native Python does not need Node, the CLI, synthetic Tools or a private source checkout. The operating reference below is optional depth, not an onboarding checklist.

Run/batch inspection honors the client's selected project/environment, including reports, events, exact values, assessment history, sequences and run-owned Tools. Comparisons carry the same selected pair. Wider credential rights do not override that selection. A service must confirm the read context before the client returns content; an older service that ignores it fails explicitly. Follow/recovery keeps the original scope and never substitutes cached data after a context rejection.

Discover capabilities and authoring schemas

client.discover() reads accepted recipe versions, supported capabilities, actual engineering bounds, current permission names and schema IDs without allocation. root = client.schema("recipe") reads its canonical structural input JSON Schema; client.schema("recipe", pointer="/properties/target", sha256=root["sha256"]) selects one exact node. Local $refs refer to that same root schema. Both methods exist on AsyncFiredrill with await, with no Node/CLI dependency. Use preview for semantic/cross-field/service validation: schema metadata is not an authorization grant, resource reservation or proof of execution. On FD_DISCOVERY_INCOMPATIBLE, update explicitly rather than silently downgrade a recipe or create work to probe compatibility. context() still reads the current authority/scope separately.

Population preparation and service inspection

client.materialize(recipe_or_path, axes=[{"path":"/priority", "values":["normal","urgent"]}], selection={"method":"matrix","seed":"release"}) returns an explicit recipe plus deterministic population provenance. Paths are relative to each scenario's input. sample requires an explicit count; it selects without replacement. No target, Tool instance or evaluator runs during preparation. Review the returned recipe, then pass population["recipe"] to the usual simulate call. The recipe's run limit still applies; up to 1,000 generated scenarios are allowed, not a promise of exhaustive behavior coverage.

client.services.observe(instance_id, epoch=epoch, cursor=0, evidence_cursor=0) returns service-attested state, pending work and activity. Follow the returned cursors with the same epoch; pending reads scheduled work only. These reads never invoke the agent. services.conform(build_id, suite, idempotency_key=key) runs explicit operation fixtures in disposable isolated instances; read the retained receipt with services.conformance(receipt_id). The suite uses schemaVersion: 1, cases, per-case id, actorOperations, optional records, and steps containing operationId, arguments, and expect path/value checks. Passing covers supplied cases only; it does not change the build's status.

For publication-time checks, pass the same suite as optional conformance=... to services.publish(...) or services.publish_directory(...). This gates publication on your explicit fixtures, not universal service fidelity. Inspect the retained publication result before creating instances.

services.create_observation_connection(instance_id, epoch=epoch, idempotency_key=key) returns a one-time protected read-only UI grant for an active run owned by your invocation actor. Explicitly call reveal_launch_url() only in a private delivery path; ordinary metadata omits it and retries never repeat it. All these operations also exist on AsyncFiredrill with await.

For compatible provider-shaped routes, an active target's context.tools[alias].export_connection("http") includes nativeHttpBaseUrl and scopeHeaders when supplied by the service. Pass those scope headers plus the derived bearer token to your HTTP client. Only authored routes exist, not a complete vendor API or unrestricted proxy. Do not substitute a Firedrill operator token or log the derived connection.

Recorded observations and target usage

Inside your native callback, context.observe("agent.decision", {...}) records bounded runner-reported evidence. For measurements you actually have:

context.record_metrics(
    latency_ms=response.elapsed_ms,
    model_usage={"provider": response.provider, "model": response.model,
                 "inputTokens": response.input_tokens, "outputTokens": response.output_tokens},
)

Include costUsd only when known. No model tariff or missing zero is invented. Each update is this invocation's cumulative value, not an additive increment. Native start events, observations and measurements are attributed to the runner, journaled before return and delivered in the background during execution, including sequence steps. Keep context.observe(...) synchronous in sync or async callbacks; no manual polling is needed. Sync callback threads marshal private journal writes to the coordinator loop. Delivery needs that loop to run; it does not keep callbacks alive after process exit. Interrupted delivery retries the same event IDs, never the target. These events do not prove external side effects or a passing assessment.

An explicit recipe target can carry optional metadata with schemaVersion: "2026-10-15-target-metadata.1", source and resetCoverage. For example, add "source": {"revision": actual_revision, "dirty": "unknown"} and "resetCoverage": {"applicationDatabase": "customer_managed", "cache": "not_reset"}. Optional source.contentDigest is sha256: plus 64 lowercase hex characters; dirty accepts clean, dirty or unknown. Coverage keys are applicationDatabase, cache, filesystem, conversationMemory, outstandingJobs, browserProfile, externalState; each accepts customer_managed, not_reset, not_used or unknown. Omitted values stay unknown. Native Python preserves this JSON metadata from a recipe file, inline dict or explicit target dict. For a callback, put metadata in its explicit {"kind": "native", "name": "...", "metadata": {...}} recipe target. Declarations neither implement reset hooks nor prove reset/source fidelity.

Large simulate()/result() responses automatically use the compact result path. result.assessment_counts, result.batch["counts"] and result.gate describe all runs; result.runs can be the first page. If result.pagination["complete"] is false, continue with client.list_runs(result.batch_id, cursor=result.pagination["nextCursor"]) (await with AsyncFiredrill). Keep outputs paged instead of loading the whole campaign into memory.

Native Python 3.11+ sync and async clients. No Node, CLI wrapper or world runtime. The one runtime dependency is certifi's certificate bundle. Install in your own agent project; no Firedrill source checkout is required.

python -m pip install firedrill-run==2.0.0

For an offline release wheel, verify its separately supplied checksum and install from its absolute path, preserving its real filename and version.

No API address is needed: Firedrill() and FiredrillLogin() use Firedrill production automatically. Development, staging, local and custom control API addresses are not accepted. Remove old URL overrides rather than changing them to another environment; existing recovery files remain bound to their original service and are never migrated implicitly. Redirects and environment HTTP proxies are not used. Customer application and synthetic Tool connection URLs remain separate from this production control-service policy.

This client requires the versioned production API contract. An older service is not a compatible fallback.

Request more execution capacity

request = client.capacity.request(
    resource="runs", requested_capacity=100000,
    message="Larger simulation campaign", idempotency_key="saved-key",
)
requests = client.capacity.list()

Both methods are awaitable on AsyncFiredrill. Requests use the client's selected project/environment. Resources are runs, services, storage or other; reply_to is optional. Do not include credentials or simulation data. Reuse the same key and input after an uncertain reply. Request receipt does not grant more capacity or start work. Notification sent means provider acceptance, not email delivery. Shared execution and payload controls are not per-account allowances.

HTTPS trust

Control/API calls, native WorkOS sign-in and HTTP targets use verified TLS with hostname checks. Normal Python/OS trust is preserved. When Python's default context has no loaded CA certificates and neither SSL_CERT_FILE nor SSL_CERT_DIR is explicitly set, the client adds the installed certifi roots to that request's context. It does not change global Python settings or download a trust store during a request.

Enterprise/custom trust remains explicit: set SSL_CERT_FILE to your approved CA bundle or SSL_CERT_DIR to your OpenSSL-compatible hashed certificate directory before starting Python. Those overrides suppress the certifi fallback, including when an override is empty or invalid; the client never replaces an explicit choice with public roots. A valid custom trust setup can still contain both enterprise and public CAs if your administrator deliberately supplies them.

Certificate or hostname failures return tls_certificate_untrusted; invalid trust setup returns tls_trust_configuration. Neither disables verification nor automatically retries a request. For a saved mutation, keep its existing operation/key and follow recovery instructions; a TLS error does not authorize starting the target again. For offline installation, provide the reviewed certifi==2026.7.22 wheel alongside the client wheel and install through an explicit --no-index --find-links directory. Do not omit the dependency.

Authorize the operator and choose scope

Firedrill login authorizes the coding-agent operator on the human's behalf—not its model provider or the application being simulated. Ordinary SDK calls never open a browser or create an account. CI supplies an authorized scoped FIREDRILL_TOKEN from its secret store and never runs interactive login.

If you already used the separately installed CLI's firedrill login and firedrill init, use Firedrill.from_config() to reuse its public scope and protected login. The CLI is optional; native Python sign-in needs no Node process. For a fresh Python-only setup, run this deliberately in a human's interactive terminal (not CI or captured model output):

import os
from firedrill import FiredrillLogin

if "FIREDRILL_TOKEN" in os.environ:
    raise SystemExit("An explicit CI token is selected; do not replace it with human login.")

def verify(value):
    # These are the human approval URL/code, never an access or refresh token.
    print("Open:", value["verificationUrl"])
    print("Approval code:", value["userCode"])

def choose_organization(value):
    active = [item for item in value["organizations"] if item["status"] == "active"]
    if active:
        for item in active:
            print(item["organizationId"], item["name"])
        selected = input("Choose an organization ID shown above: ").strip()
        if selected in {item["organizationId"] for item in active}:
            return {"organizationId": selected}
        raise ValueError("No available organization selected")
    if value["canCreate"]:
        name = input("Name your first Firedrill organization: ").strip()
        if name:
            return {"name": name}
    raise ValueError("Finish email verification/invitation or restore organization access, then retry")

login = FiredrillLogin()
status = login.login(on_verification=verify, on_organization=choose_organization)
print("Firedrill authorized:", status["signedIn"])

The human completes the displayed WorkOS approval; no token is pasted into chat or source. Organization choice is requested only when needed. A pending invitation or uncertain setup is not permission to create a replacement organization. Preserve the protected login state and follow the error's repair direction. login.status() reads current authorization; login.logout() ends this interactive session, not separately issued CI credentials. AsyncFiredrillLogin provides the same explicit flow with await login.login(...); its callbacks may be sync or async.

Sync and async SDK clients stay bound to their first resolved login credential and organization. Automatic renewal continues for that identity. Replacing the shared login stops an existing client with FD_SDK_IDENTITY_CHANGED before it sends a request under the replacement. Verify the intended account and scope, then construct a new client deliberately. Recover old work with its original identity, not a replacement target run.

After authorization, discover authorized projects and their environment pair:

from firedrill import Firedrill

account = Firedrill()
for project in account.projects()["items"]:
    print(project["id"], project["name"], project["environment"]["id"])

If you deliberately need a new project, save one key before calling project = account.create_project("My application", idempotency_key="my-application-first-project"). It returns project["id"] and project["environment"]["id"] (the new Default environment). Reuse the exact key/name after uncertain delivery, not a new key. Use that actual pair as FIREDRILL_PROJECT_ID and FIREDRILL_ENVIRONMENT_ID, or pass project_id and environment_id to the constructor. Never invent IDs or put credentials in config/recipes. An explicit token takes precedence over saved login and never silently falls back to another identity.

First tool-free run

from firedrill import Firedrill

client = Firedrill()  # explicit environment-selected scope; constructor args also work
result = client.simulate({
    "name": "Greeting variation",
    "scenarios": [{"id": "greeting", "name": "Greeting", "input": "Hello"}],
}, target=lambda value, context: {"reply": value.upper()})
print(result.report_url)

This example executes your callable, not a model substitute. Use your existing application in the callback to exercise its actual behavior. Zero checks is valid; no universal score or passed flag is inferred. The local service retains the recipe, run, output, attributed evidence and any requested assessment.

For an awaitable application, use the native async client in your existing loop:

from firedrill import AsyncFiredrill

client = AsyncFiredrill()

async def target(value, context):
    context.raise_if_cancelled()
    return await existing_application(value)  # your application's existing callable

result = await client.simulate("firedrill/scenario.json", target)

Recipe limits.maxRuns optionally caps the expanded run count for this batch. It accepts a positive integer or JSON null (Python None); null is the default and means no caller-selected run-count cap. A numeric cap is an execution choice, not a cumulative account usage quota. Per-request payload, scenario/repetition/concurrency, output and execution safeguards still apply.

simulate(recipe, target=None, *, operation_id=None, allow_parallel=False) accepts an inline recipe or JSON file. Callback targets need no serialized source; a missing wire target becomes a named native target. schemaVersion defaults to the canonical 2026-10-14 contract; an explicit other version fails locally, before a request or journal is created. Remaining recipe validation and canonical defaults stay with the service. Both one and many runs return the same BatchResult with batch, runs, report_url and canonical server gate; to_dict() preserves wire spellings. reportUrl and url are aliases for the same retained report_url, not additional reports. Results from simulate, run and resume also carry operation_id, serialized as operationId by to_dict(). Preserve it even for a partial result: it selects the same protected journal on resume, without creating replacement work. The client's last_operation_id remains available. result(batch_id) reads the API record only and does not invent a local operation handle. With an explicitly configured service portal origin, the report URL opens the retained batch with its exact organization/project/environment context. Normal portal sign-in and authorization still apply. A local service without that configuration returns an authenticated API resource instead. Neither link is a public share link or an access grant.

Individual report(run_id) dictionaries also preserve optional reportLink (REPORT_LINK_VERSION, ReportLink): kind == "portal" names the exact retained run's scoped overview; kind == "api" names its authenticated API fallback. inspect_report preserves it inside the operator document's value. Older services may omit it. Never infer an origin, credential or access from a link.

Command and HTTP recipes need no callback. Commands execute literal argv without a shell and receive the versioned target envelope on stdin. FIREDRILL_INPUT_JSON contains only JSON input; run/attempt IDs have their own environment variables. The client removes FIREDRILL_TOKEN and FIREDRILL_CREDENTIAL from child environments while retaining customer target model configuration. HTTP targets receive the same envelope from your runner, without the control token, redirects or automatic retries.

The envelope's schemaVersion is 2026-10-17-target-invocation.1. It contains separate input, scenario: {"id": ...}, configuration, runId, attemptId, target-only tools and optional current step: {"id": ..., "ordinal": ...}. Read client.schema("target-invocation") for its structural transport schema. The runner keeps checks, rubrics, metric selectors, dataset references and future sequence steps; they are not target input. Explicit task input should contain only the knowledge the application actually needs.

Callback context includes schema_version (TARGET_INVOCATION_VERSION), run_id, attempt_id, ID-only scenario (TargetScenario), configuration, a Unix-seconds deadline, remaining_seconds, cancelled, raise_if_cancelled() and observe(kind, json_payload). Observations are bounded runner-reported evidence, not service-attested side effects. Never put secrets in output/evidence. Target exceptions retain their type, not arbitrary potentially secret-bearing exception text. Callback input/configuration are detached JSON values, not references to retained coordinator data. This is least-disclosure, not a hostile-code sandbox: native callbacks share the customer's process and commands still have its filesystem.

Optional task measurements

Use scenario metrics to retain up to 32 named finite numeric values from frozen run evidence. They are observations, not assertions or evaluator scores. This native arithmetic application demonstrates the actual recipe shape; replace the callback with your agent before drawing conclusions about that agent. It needs your configured origin, authentication and selected project/environment.

from firedrill import Firedrill, TASK_METRIC_VERSION, TaskMetricDefinition

net_revenue: TaskMetricDefinition = {
    "schemaVersion": TASK_METRIC_VERSION,
    "id": "net-revenue", "label": "Net revenue", "path": "/output/netRevenueUsd",
    "unit": "USD", "direction": "neutral",
}
recipe = {
    "schemaVersion": "2026-10-14", "name": "Refund outcome exploration",
    "target": {"kind": "native", "name": "revenue-summary"},
    "scenarios": [{"id": "refund-exceeds-sales", "name": "Refunds exceed sales",
                   "input": {"grossUsd": 100, "refundsUsd": 107}, "metrics": [net_revenue]}],
    "repetitions": 3, "concurrency": 1,
    "limits": {"timeoutMs": 10000, "maxRuns": 3, "maxOutputBytes": 4096},
}
client = Firedrill()
result = client.simulate(recipe, target=lambda value, context: {
    "netRevenueUsd": value["grossUsd"] - value["refundsUsd"],
})
print([group.get("taskMetrics") for group in client.summary(result.batch["id"])["groups"]])

Zero and negative task values are valid. Missing evidence, missing selected values and non-numeric values stay unavailable, never coerced to zero. TaskMetricResult and TaskMetricSummary preserve canonical wire names, definition digests, coverage, provenance and measured/unavailable states. Summaries keep median/range and bounded run samples separate per exact definition and scenario/configuration population; absent historical fields remain absent. direction is only a declared preference, not a score, CI rule or automatic winner. Units are not converted, and this USD task value is not an inference-cost estimate. AsyncFiredrill uses the same recipe and retained shapes without a Node bridge or client-side grading implementation.

Without source, JSON Pointers select from {input, output, metrics}. /input/... is scenario-declared data; /output/... and /metrics/... are runner-reported. An explicit canonical service source selects frozen attested Tool state/activity/ pending work, not a later live instance. Read the task-metric authoring schema for exact fields. record_metrics still accepts only its documented telemetry fields: put custom task measurements in your actual JSON output. These docs describe the source contract, not a deployment or verification receipt.

Optional rubric and gate

Numeric exact constraints can include "numeric": {"schemaVersion": "2026-10-16-numeric-check.1", "unit": "USD", "absoluteTolerance": 0.01}. Use only numeric expected values with equals, lte or gte. Equality compares absolute difference; lower/upper bounds expand by the declared tolerance. The tolerance is finite and nonnegative, defaulting to zero; missing/non-numeric evidence stays unknown. This client exports NUMERIC_CHECK_VERSION. Units are labels, not inferred or converted. Supply the same JSON recipe as other clients; the service validates/evaluates the canonical check.

Observation-only runs do not need scoring. When the user wants quality assessment, this self-contained example requests a separate platform evaluator budget: three sequential trials, not unlimited evaluation. Save it as firedrill/scored.json. The native renderer below uppercases supplied notice text; it is a real teaching application, not an AI agent or a promised passing score. Replace it with your actual application before drawing conclusions about that application.

{
  "schemaVersion": "2026-10-14",
  "name": "Delivery notice quality",
  "target": {"kind": "native", "name": "delivery-template"},
  "scenarios": [{
    "id": "delay", "name": "Explain a delivery delay",
    "input": {
      "task": "Give a brief delivery update using only these facts.",
      "facts": {"status": "delayed", "deliveryDay": "Friday", "customerAction": "none"},
      "label": "Your shipment is delayed until Friday. No action is needed."
    },
    "checks": [{"id": "label-present", "description": "Return the label field",
      "path": "/output/label", "comparator": "exists", "required": true}],
    "rubric": {
      "id": "delivery-facts", "version": "1",
      "policyVersion": "2026-10-15-assessment.1",
      "dimensions": [{
        "id": "facts",
        "applicability": {"state": "applicable"},
        "missingEvidence": "not_assessable",
        "description": "Judge output.label against input.facts. Accept equivalent wording; do not require the input label's exact phrasing.",
        "anchors": [
          "Contradicts the delivery facts or invents a harmful instruction.",
          "Mostly omits the facts or adds unsupported promises.",
          "Preserves some facts but misses an important fact or is materially ambiguous.",
          "Preserves all three facts without unsupported claims, with minor clarity issues.",
          "Clearly and concisely preserves the delay, Friday delivery and no action needed, without unsupported claims."
        ]
      }]
    }
  }],
  "repetitions": 3,
  "concurrency": 1,
  "limits": {"timeoutMs": 10000, "maxRuns": 3, "maxOutputBytes": 4096},
  "gate": {"requireCompleted": true, "requireExactChecks": true, "minimumLevels": {"facts": 3}}
}

The five anchors correspond to levels0–4, not probabilities. Task facts live in input and the dimension description; do not rely on a scenario display label as evaluator context. The exact check protects output shape while the rubric permits valid wording variation.

from firedrill import Firedrill

client = Firedrill()  # approved origin and selected project/environment from above
result = client.simulate(
    "firedrill/scored.json",
    target=lambda value, context: {"label": value["label"].upper()},
)
print(client.summary(result.batch["id"]))
print(client.report(result.runs[0]["id"]))

Inspect execution, exact checks, dimension level/state/explanation/evidence and gate separately. A dimension can be not_assessable with level: null: that quality evidence is unknown, not level0 or proof of application failure. The gate is inconclusive unless another known requirement already blocks it; exact checks and completion still apply independently. Pending assessment stays pending. Retain all three trials and failed/interrupted/unassessed counts, not just the best result.

Applicability is authored case policy, not a grader's judgment. Explicit applicability or missingEvidence requires rubric "policyVersion": "2026-10-15-assessment.1" (also exported as firedrill.ASSESSMENT_POLICY_VERSION). Omitted applicability means applicable. A dimension that does not apply to the case can instead declare "applicability": {"state": "not_applicable", "reason": "This case is information-only; no action is authorized."}. It retains state not_applicable and level: None in native Python, separate from missing or unsupported evidence (not_assessable/None). The JSON wire value is null. The evaluator cannot invent this exemption. "missingEvidence": "not_assessable" is the only supported missing-data policy: missing evidence never becomes a score, a pass or an automatic exclusion. An excluded dimension cannot satisfy its required minimumLevels entry; that gate remains inconclusive unless another known requirement blocks it. Other dimensions are not reweighted. Changing this authored policy changes the pinned rubric; past assessments are not rewritten.

For an optional descriptive composite, add this composite field inside the scenario's rubric before submitting the recipe above:

"composite": {
  "schemaVersion": "2026-10-16-composite.1",
  "method": "weighted_mean_ordinal",
  "weights": [{"dimensionId": "facts", "weight": 1}]
}

Select only dimension IDs declared in that rubric, once each, with positive integer weights from 1–1000. This explicitly authored index uses the 0–4 ordinal coding; it is not a probability, a universal quality score or a gate. The service derives it from retained assessment evidence. Python does not calculate a grade or invoke Node. Inspect the optional service result as an ordinary dictionary:

from firedrill import CompositeResult

report = client.report(result.runs[0]["id"])
composite: CompositeResult | None = report["assessment"].get("composite")
print(composite)  # None when no policy was requested

With AsyncFiredrill, await client.report(run_id) and inspect the same dictionary. COMPOSITE_VERSION, CompositePolicy, CompositeComponent and CompositeResult are exported native types/constants; wire keys remain camelCase. Read state, reason, components and requiredExactChecks, not only value. Missing/unassessable or selected not-applicable dimensions do not become zero or silently reweight the remaining dimensions. A required exact violation yields constraint_violation; unknown required checks yield not_assessable. Both retain value: None (null on the wire). The optional CI gate is unchanged, and these report reads invoke neither the target nor an evaluator.

This evaluator reads text observations. For a dimension that requires pixels, audio or video, declare "evidenceKinds": ["image"], ["audio"] or ["video"] (combine kinds when all are required). Omitted kinds mean text. Unsupported required kinds remain not_assessable/None; captured-file metadata cannot produce a visual/audio grade. Text-compatible dimensions can still be assessed. A recipe whose dimensions are all unsupported makes no evaluator request. Declare these requirements explicitly; they are not inferred from criterion prose.

After an intentional application change, run the same recipe again and compare the two actual batch IDs. Keep scenario/rubric, configurations, repetitions and any selected service seeds consistent; record the application revision and its own randomness/reset policy. This tool-free recipe declares no service seed. Comparisons describe retained populations, not a causal effect or automatic winner. summary() keeps notApplicable counts separate from notAssessable and missing counts. A paired dimension reports notApplicablePairs separately from missingOrNotAssessablePairs; neither contributes a numeric observation. Quality pooling also requires the same known evaluator configurationDigest, not only the same provider/model/protocol. Differences are reported as evaluator_configuration or evaluator_configuration_unavailable. Historical missing configuration remains unknown and excludes paired quality pooling; individual retained reports stay readable. These wire keys remain camelCase in the native result dictionaries. A reassessment explicitly evaluates frozen evidence again; it does not run the application. Do not request one implicitly to repair an unknown result. Target provider credentials/costs remain separate from operator login and evaluation.

Use client.compare(baseline_batch_id, candidate_batch_id) for those retained batches. Deliberate reassessment is client.reassess(run_id, idempotency_key=saved_reassessment_key), which reuses the retained rubric. Supplying rubric=selected_rubric changes assessment criteria, not the application's past output. Async clients use the same data with awaited methods. No Node process or CLI is needed for this native path.

Inspect each assessment's evaluationAttempts for the frozen nonsecret configuration/configurationDigest, state and separate provider usage. Configuration includes request model, protocol/instructions digest, input/output bounds and transport limits; it never contains a provider key. The evaluator configuration digest is distinct from the target configuration digest. Known provider usage remains retained when structured scores are rejected; missing or unparseable provider usage remains unknown, not zero. Native sync and async reads preserve these dictionaries without Node. Reading history does not create an assessment or replay a target.

Optional run-bound Tools

Tool-free simulations remain valid. To allocate fresh Tool state for each run, select immutable builds, explicit datasets/seeds and operation grants in the recipe:

recipe = {
    "name": "Application with a controlled inbox",
    "services": [{
        "alias": "inbox", "buildId": selected_build_id,
        "datasetId": selected_dataset_id, "seed": "42",
        "operations": ["list"],
    }],
    "scenarios": [{"id": "one", "name": "One", "input": {}}],
}

def target(value, context):
    response = context.tools["inbox"].invoke(
        "list", {}, idempotency_key=f"{context.run_id}:list-once",
    )
    return existing_application(value, response)  # your own application

result = client.simulate(recipe, target)

Async callbacks use await context.tools[alias].invoke(...). The returned value is the unchanged semantic operation response, including result.outcome; an operation error is not an agent assessment verdict. Every invocation requires an explicit stable key. Unknown acknowledgement is not retried automatically: while the run still has authority, reconcile the same key and unchanged arguments.

scenario.serviceDatasets can override a binding's dataset by alias, for example {"inbox": another_dataset_id}. Each expanded scenario/configuration/repetition receives its own instance. An omitted dataset means no seed records, never starter data. Tool instances are run-managed: preparation, closure and retained evidence belong to Firedrill, not target lifecycle/reset calls. Finish, cancellation, expiry or revoked authority retires that run's grants and preserves service evidence. Binding a request to a run/attempt does not prove it was authored by an AI agent.

tool.instance_id, tool.epoch, tool.operations, tool.to_dict() and normal repr(context)/repr(tool) omit grant secrets. context.to_dict() is also metadata-only. For an existing protocol integration, tool.export_connection("http") or tool.export_connection("mcp") is an explicit secret export returning the exact URL, token and scoped grant metadata. The HTTP invoke body is {"operationId": ..., "arguments": ...} and the stable key belongs in Idempotency-Key. Do not substitute the operator token or copy a grant to another run, instance, project or environment.

Command stdin and HTTP target JSON include an explicit tools binding map, with HTTP/MCP connections carrying only these derived target credentials. They contain no operator token or lease token. FIREDRILL_INPUT_JSON remains input alone; no grant is added to child environment variables. This intentional secret-bearing envelope is never journaled by the helper. Do not log it or include it in captures. Known exact derived tokens echoed in target output/events/stdout/stderr are redacted before journaling and evidence submission, including before log chunking. This is not a general detector for transformed secrets or arbitrary credentials.

Preparation heartbeats run before target entry. A claim remains claimed in its private journal until exact returned aliases/builds/datasets/seeds/grants/routes are checked. Unconfirmed preparation remains replayable on resume without inventing target execution. Confirmed setup rejection records runner.preparation_failed and interrupted execution, with no runner.started or target duration. Authority/transport uncertainty preserves the claim for reconciliation. The target deadline starts after preparation. A journal entry becomes running immediately before invocation; recovery never invokes that target again if its outcome is uncertain. Already terminal claims are reconciled without preparing replacement instances.

Optional bounded sequences

Use scenario.sequence with schemaVersion: "2026-10-14-sequence.1" for ordered target calls and explicit service operations. The service owns conditions, bounded polling, virtual-time advancement and service snapshot/restore; Python does not reimplement their semantics. The same simulate, run and resume entry points work for sync/async callbacks, literal commands and HTTP targets.

from firedrill import SEQUENCE_VERSION

result = client.simulate({
    "name": "Two explicit application calls",
    "scenarios": [{
        "id": "follow-up", "name": "Follow-up", "input": "default input",
        "sequence": {"schemaVersion": SEQUENCE_VERSION, "steps": [
            {"id": "first", "kind": "target", "input": None},
            {"id": "second", "kind": "target", "input": "Follow up"},
        ]},
    }],
}, target=existing_application)
retained_steps = client.sequence(result.runs[0]["id"])

Inside each target callback context.step is {"id": step_id, "ordinal": index}; the command/HTTP envelope has the same step field. Explicit input: None is JSON null, not a request to reuse the scenario input. An omitted step input uses the scenario input. Each target step receives freshly checked run-bound Tool grants, including new epochs after an explicit service-only restore. Restoring service state does not reset your process, model memory or application database.

The service supplies one absolute sequence deadline. The helper recomputes the remaining time after grant refresh and starts no target after it expires. Each target output is at most 128 KiB (or the lower recipe bound); a complete step observation is at most 256 KiB. Larger captures belong in context.attach. Native shared-memory and cooperative-cancellation limitations still apply.

Recovery journals persist sequence phase, then a step's running marker just before invocation. Output/events are saved as complete before acknowledgement; lost acknowledgements replay those exact records without rerunning the target. A saved running step with unknown output raises RecoveryRequired until its retained run is terminal. resume(id) without a native callable can reconcile a saved completion but does not invoke the next native target. Journal reads require the operation lock. Derived grants and target envelopes are never journaled. sequence(run_id) returns retained service records in both sync and async clients; reading it does not dispatch any step or create an assessment.

Captured artifacts

context.attach({"path": local_path, "artifact": metadata}) declares and uploads an exact capture under this run/attempt. Call it inside the callback before returning. Sync callbacks receive an available reference directly; async callbacks use await context.attach(...). The operator credential and lease are not exposed in the context or returned reference.

import hashlib
from datetime import datetime, timezone
from pathlib import Path

# An actual capture produced by your application; retain it for interrupted uploads.
capture = Path("captures/trace.zip")  # owned by you, mode 0600, regular file, no links
content = capture.read_bytes()
reference = context.attach({
    "path": capture,
    "artifact": {
        "id": "trace-1", "name": "trace.zip", "kind": "trace",
        "mediaType": "application/zip", "bytes": len(content),
        "sha256": hashlib.sha256(content).hexdigest(),
        "capturedAt": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
        "metadata": {},
    },
})

The helper validates the original private file and records its absolute path, metadata, digest and exact declaration key before network writes. Files are bounded to 16 MiB; the service enforces its per-run count/byte bounds. Supported formats are PNG, JPEG, WebP, ZIP, JSON, text and WebM—not inline HTML/SVG. An acknowledged attachment is retained evidence of those bytes, not proof that its claims are true or that an external side effect happened.

A lost declaration/upload acknowledgement preserves the pending intent. Resume the same operation, with the original capture unchanged. The helper reconciles exact intent before finishing the run and does not reinvoke the target. If the failed attachment call prevented your callback from returning, recovery cannot invent that return value: the retained target error remains an error. Pending new uploads need the original active lease; already accepted exact content can be reconciled without new target execution. Local capture files are never automatically deleted.

Retrieve retained captures

The sync and async clients expose artifacts(run_id), artifact_content(run_id, artifact_id) and download_artifact(run_id, artifact_id, output). These are scoped evidence reads, not target execution or evaluation:

files = client.artifacts(run_id)
available = [item for item in files["items"]
             if not item.get("metadataOnly") and item.get("state") == "available"
             and item.get("mediaType") == "image/png"]
if available:
    # Choose an extension matching the selected artifact's mediaType.
    saved = client.download_artifact(run_id, available[0]["id"], "captures/review.png")
    print(saved["path"], saved["artifact"]["mediaType"], saved["verified"])

Await these methods on AsyncFiredrill. The download verifies retained size, SHA-256 and media type and confirms the selected project/environment. It rejects redirects and publishes a mode 0600 file at an explicit new path in an existing directory, never overwriting a file or symlink. No automatic open or extraction occurs. artifact_content instead returns verified bytes and their metadata. Metadata-only expired/deleted entries remain visible as history, with no names or payload; pending uploads are not available evidence. Retry an interrupted download, not the original simulation. Cancelling an async download's await can leave its verified file saved because the bounded download runs in a worker thread. Check the chosen filename before retrying; no existing file is replaced.

Documents use the same retained evidence path: kind: "file" with plain text, JSON, text/markdown, application/pdf or application/octet-stream. Opaque files are download-only attachments, never interpreted or executed. Attach the original private file with its exact byte count and SHA-256 while the invocation is active; the browser helper does not manufacture documents. The authenticated API owns storage and retention. An available manifest plus verified download establishes returned bytes, not a client claim about the deployed storage provider, replicas or backups. No direct bucket URL or cross-origin redirect is accepted by these download methods.

Recipes have no fixed client scenario/configuration/service item quota. Repetitions and desired concurrency must be positive safe integers. Finite JSON, 8 MiB recipe input and canonical expanded-payload bounds remain, as do timeouts, actual service capacity and explicit caller budgets (limits.maxRuns: None means no caller run cap). Sync and async runners allocate workers using the lesser of desired concurrency, admitted population and reported operations.resources.runSlotLimit. Older services without that disclosure remain serially recoverable. Recovery reads the admitted batch instead of requiring a new admission preview or silently submitting replacement work. The runner_capacity progress notification/default stderr discloses requested concurrency, local worker ceiling and reported service slots. This is not a reservation or proof of execution. Python native execution supports command, HTTP and Python callbacks; submission can queue an isolated Node recipe, but executing it still requires the explicitly prepared TypeScript isolated adapter.

Coordination and recovery

Private compilation failures preserve FiredrillError.build_id, operation_id and failure; to_dict() uses wire keys buildId, operationId and failure. services.build(build_id) returns an optional failure with versioned category diagnostics, repair directions and selected relative file/one-based line/column locations. The public ServiceBuild, ServiceBuildFailure and ServiceBuildDiagnostic types describe this response. Compiler stderr, source lines and host paths are not returned. A null failure means no structured diagnostic was recorded, not success. Fix source and publish with a new operation key; the old failed key recovers its retained failure without rebuilding. Partial Tool setup receipts retain these diagnostics and existing instances.

Canonical server definition rejections keep FiredrillError.code (normally FD_INVALID_INPUT), HTTP status, repair and request_id. The bounded error.validation dictionary gives exact field paths with array indices, issue codes/messages, totalIssues and omittedIssues; at most 20 issues and 20 path parts per issue are returned. str(error) includes the server's short human summary. error.to_dict() uses wire spellings inside {"error": ...}, including the current operationId/batchId/runId when available. Missing, malformed or legacy raw diagnostics are not exposed as validation. Python does not duplicate the service's definition validator: preview/save_scenario may send invalid definitions and receive these errors without creating a simulation journal; simulate can already have a protected create_intent journal when the service rejects the definition, with an operation ID but no batch or target invocation. An unchanged retry/resume repeats that rejection; fix the definition and use a new deliberate operation. Do not edit or discard an uncertain execution journal.

The helper owns scope checks, durable intent, runner claims, 15-second heartbeats, bounded stdout/stderr/output, retained evidence and finish reconciliation. It does not bundle or reimplement simulation state or assessment logic. Journals default to ~/.local/state/firedrill/python. Set FIREDRILL_JOURNAL_DIRECTORY for a selected CI recovery location; an explicit state_dir takes precedence. Python and TypeScript execution journals have different formats, so retain separate locations for their executions; their protected sign-in store is shared. Directories must be owner-private (0700), files 0600. Journals contain sensitive case/evidence and lease state, but never the operator token. Keep them outside source control. A process lock prevents two local owners of the same operation.

from firedrill import TransportError, RecoveryRequired

try:
    result = client.simulate(recipe, target, operation_id="my-deliberate-operation")
except (TransportError, RecoveryRequired) as error:
    print(error.code, error.operation_id, error.batch_id, error.repair)
    # After connectivity/authority is restored:
    result = client.resume(error.operation_id, target=target)

Only resume the original operation after a lost response. A new simulate call with a new ID creates a new batch. last_operation_id also exposes the local handle if the caller cancels its foreground wait. resume(id) without a callback can reconcile completed output/finish requests; it cannot execute more native work. A saved running invocation without a local result is never re-invoked: the client reads retained state and raises RecoveryRequired while execution is uncertain. Lease expiry becomes interrupted in the service, not a target retry. Exact event IDs/content and finish idempotency keys survive lost responses.

Actual native callback entry and successful command process creation emit runner.started. An HTTP request emits only runner.http_dispatch, which is not proof of remote application invocation. The helper retains measured monotonic metrics.durationMs for the target adapter, including awaited callback/upload work; this is runner-reported elapsed time, not service-attested or inference-only latency. Cancellation before invocation produces no invented start or target duration.

run(batch_id, target=None, ...) attaches your customer-owned runner to an existing batch, validates its selected scope, and uses the same journal/claims without creating a second batch. Useful when an operator prepared work through the API or MCP. The control service cannot run a laptop callback on its own.

Native callbacks share process memory. Parallel native, command and HTTP targets require an explicit retained targetExecution declaration when recipe concurrency exceeds one. Fresh simulate(..., allow_parallel=True) can record it after review; existing run/resume cannot change an admitted recipe to add it. This acknowledges shared-state risk, not isolation. Sequential execution is unchanged. No adapter silently resets your database, application memory or borrowed service. Timeouts/cancellation cannot guarantee that arbitrary threads, cancellation- suppressing async code or remote HTTP effects stop. Such outcomes are interrupted with an explicit stop-unconfirmed reason and never automatically retried. POSIX command groups receive bounded TERM/KILL followed by an exact owned-group absence observation, not just a leader-exit check. The retained runner.command_stop receipt distinguishes group_absent from unconfirmed; escaped descendants are not observed and this is not a container sandbox. Windows can stop only the owned child here and always reports process-tree stop as unconfirmed. Captured output prefixes remain evidence. An adapter-observed unconfirmed stop is retained in the private operation/sequence journal, stops new coordinated claims and sequence steps, and remains fenced on resume even if the service lease is already terminal. Resume can retain existing completion/evidence, not establish process cleanup or authorize more execution; inspect the customer runner separately.

request_timeout (default 10 seconds) bounds an individual control exchange. Recipe limits.timeoutMs bounds one target invocation, or the whole declared sequence rather than each step separately; follow_timeout (default 300 seconds) bounds execution coordination and queue following, not an invented target duration. assessment_timeout (default 300 seconds, or 0 to return without waiting) is a separate wait after execution reaches a terminal state. Set these options on Firedrill(...) or AsyncFiredrill(...). Waiting for an assessment never claims more work or invokes the target again. Active invocations retain their separate target bound. A bound/cancellation can return the retained partial result with pending assessment and its operation/batch handles; assessment is never fabricated as complete. resume(operation_id) without a native callback can inspect retained work, including a compact large-batch result, without restarting that callback. Cancellation prevents new work and is observed by active runners on heartbeats.

Small control surface

Both clients expose context(), projects(), create_project(name, idempotency_key=...), batch(id), result(id), list_runs(batch_id, cursor=None, limit=100), report(run_id, cursor=None, limit=100), scenarios(project_id=None) and cancel(batch_id, idempotency_key=...). Async methods are awaited. Use explicit idempotency keys for retryable project/cancellation mutations.

For separate preparation/execution/observation, use submit(recipe, idempotency_key=...), resume_submission(key), run(batch_id, target=...) and follow(batch_id, timeout=...). The ordinary simulate remains the one-call path. Submission keys reconcile creation only; runner operation IDs reconcile execution. Follow requests no target, cancellation or assessment, and settled does not mean passed. Both native classes expose these methods; await them on AsyncFiredrill.

For an explicitly selected repository config, both classes expose a synchronous factory; creating the client does not send a request:

client = Firedrill.from_config("firedrill.config.json")  # FIREDRILL_TOKEN supplies authority
async_client = AsyncFiredrill.from_config("firedrill.config.json")  # no await for construction

The public config needs only projectId and environmentId, each a nonempty string of at most 2,048 characters. Select both or neither. Credentials and unknown settings are rejected, not ignored. from_config() rejects conflicting project/environment selections across config, explicit options and environment.

Legacy apiUrl, api_url, base_url and FIREDRILL_API_URL declarations are accepted only when they identify https://api.firedrill.run. Remove those unnecessary settings from new integrations. Every supplied address is checked, including an environment value shadowed by an explicit option. Another origin, credentials, query, fragment or path fails with FD_API_ENDPOINT_LOCKED before sign-in, credential storage or network access. Hostname case, an explicit HTTPS default port and a trailing slash normalize to the same production origin; there is no switch to enable development endpoints. Null/empty scope options are not valid selections. Direct constructors retain explicit-argument precedence for project/environment only, not for control API addresses.

Config files are bounded to 64 KiB and recipe files to 8 MiB, read as strict UTF-8 JSON through a regular-file descriptor. Final-component symlinks, FIFOs, devices, directories and malformed inputs fail with a typed FiredrillError without echoing source contents. This does not prohibit symlinked parent directories or claim a filesystem sandbox. There is no ancestor search, executable config, automatic credential-file discovery or implicit browser authorization.

Tool-free command/HTTP/native execution remains valid. Service authoring and explicit lifecycle controls are available below; run-bound Tools are selected explicitly as described above. Arbitrary application-state reset and browser execution are not inferred or performed by the helper.

Scoped machine credentials

Both clients expose client.credentials for an identity explicitly authorized to manage its own machine credentials. The service—not the client—checks that the exact project/environment and requested capabilities narrow the issuer's authority. Machine credentials cannot create projects or delegate credentials.

issued = client.credentials.create(
    label="CI inspection", capabilities=["read"],
    expires_in_seconds=3600, idempotency_key=saved_issuance_key,
)
print(issued)  # metadata plus [redacted], never the token
metadata = issued.to_dict()
secret = issued.export_secret()  # intentional export to your protected credential store
# Do not print secret or put it in a recipe, target envelope, model prompt or source.
page = client.credentials.list(limit=25)
revoked = client.credentials.revoke(issued.credential["id"])

AsyncFiredrill uses the same namespace/signatures with await. Methods use native Python transport; no CLI or Node process is launched. create uses the client's selected scope unless both project_id and environment_id are explicitly supplied. Capabilities must be a distinct nonempty subset of read, runs:write, runs:execute, assessments:create, services:publish, services:create, services:invoke, and services:control; none is silently added. Lifetime is 60–7,776,000 seconds, default 2,592,000. WorkOS-member-owned keys survive normal interactive sign-out, but remain limited by expiry, revocation and the owner's current membership and permissions. Development bootstrap-derived keys remain parent-bound. Retained expiresAt is authoritative, not proof of current access.

Creation requires a caller-saved visible-ASCII operation key (1–200 characters, no spaces). After an uncertain response, use the exact key and unchanged input; the service can recover the same secret without creating a second credential. No automatic retry occurs. Replay after revocation/expiry does not resurrect it. IssuedCredential redacts normal repr, str and to_dict; export_secret() is the explicit secret path. Do not serialize private internals. Keep the returned metadata ID for revocation rather than using the secret as an ID.

list(cursor=None, limit=50) returns metadata only; follow nextCursor unchanged until null. revoke(credential_id) is an exact-ID idempotent operation; repeat that same revocation after uncertain acknowledgement, never issue a replacement as an implicit recovery. These methods keep no secret-bearing credential journal.

Retained inspection and explicit reassessment

Both sync and native async clients expose:

list_batches(filters={...}) and list_runs(batch_id, filters={...}) use the same wire-named filters: q (literal case-insensitive name/ID prefix), createdAfter / createdBefore (inclusive/exclusive UTC submission bounds, year 0001–9999 and at most three fractional digits), execution, origin, scenarioId, configurationId, and issue. Issue values are incomplete, exact_violation, assessment_unavailable, and below_level. The latter requires dimensionId and numeric minimumLevel 0–4. Unknown/N/A/missing assessments are not zero. Conditions match the same run, not unrelated rows. Clear the cursor when changing filters; otherwise reuse it unchanged.

page = client.list_batches(filters={"origin": "ci", "issue": "incomplete"})
low = client.list_runs(batch_id, filters={"issue": "below_level", "dimensionId": "answer", "minimumLevel": 3})

New simulations default to immutable client-declared sdk origin. In ordinary CI, use simulate(recipe, target, submission_origin="ci") (also native async), or set FIREDRILL_SUBMISSION_ORIGIN=ci. Only with that resolved origin and GITHUB_ACTIONS=true does the SDK automatically record GitHub server, repository, run ID, attempt, commit, ref, workflow, job and event metadata. Optional FIREDRILL_CI_PR_NUMBER records the PR number; an empty value is omitted. It reads no git commands, event files or environment dumps. Malformed advertised metadata is rejected before new admission.

Alternatively pass ci_context matching exported CiContext and CI_CONTEXT_VERSION; this requires the ci origin. The first local create journal freezes the declaration. run(batch_id, target, ci_context=...) and resume(operation_id, target, ci_context=...) preserve the original batch's submission; an explicit different context is rejected. Omit the context to recover without deriving a replacement from the current job. Historical records without metadata remain unknown. Context is caller-declared provenance, not execution-host attestation, and does not grant access.

Set FIREDRILL_CI_REPORT=github_summary for an optional compact job summary when simulate, run or resume returns. Native sync and async paths support this without Node. The summary contains retained execution/assessment counts, the separate gate and authenticated report/original workflow links—not inputs, outputs, names or credentials. The current reporting job is validated independently; it can report a batch submitted by an earlier workflow. Only the existing regular GITHUB_STEP_SUMMARY file is used, with a 16 KiB append and 1 MiB total limit; no fallback file is created. result.ci_report (CiReportReceipt) says written or unavailable with a failure code. An uncertain write is not retried and never changes simulation or gate state. result.to_dict() and batch_receipt(result) retain it as ciReport. Ordinary result/report/summary inspection never writes a job summary.

  • list_batches(cursor=None, limit=25, filters=None), summary(batch_id) and compare(baseline_batch_id, candidate_batch_id).
  • compare_pairs(baseline_batch_id, candidate_batch_id, cursor=None, limit=25) pages all paired cases with exact run references/differences; use await with AsyncFiredrill. Omit cursor first, then follow opaque nextCursor (1–512 characters) unchanged until null. On FD_COMPARISON_PAGE_CHANGED, restart the first page. Summary samples are not the complete paired-case list.
  • list_scenarios(project_id=None, environment_id=None, q=None, cursor=None, limit=100) for project-owned definitions, with nullable lastExecution in the selected environment. Unused revisions stay visible. scenarios() is a first-page alias.
  • scenario(scenario_id, revision, project_id=None) and event(run_id, event_id) for exact retained resources. These additive methods are source/compile-checked here; endpoint qualification belongs to the coordinator receipt.
  • assessments(run_id) and assessment(run_id, assessment_id) for retained history.
  • reassess(run_id, idempotency_key=saved_key, rubric=None) as a separate explicit evaluator action. None reuses the retained rubric; it never invents one.

Read methods return the service's wire dictionaries and never run targets, request evaluation or recalculate gates. Reassessment uses frozen evidence, may consume platform evaluator budget and never reruns the customer's target. Customer target-model costs remain separate. A lost reassessment response requires the same saved key and unchanged rubric, not a new action. This slice qualified missing-rubric rejection and read-only history without spending evaluator budget.

Follow nextCursor until null, even if a page contains fewer than the requested limit. SDK result reads automatically fall back to the compact result when the full aggregate is too large; continue with its pagination cursor and never create another execution to repair a large report. Large exact-check operands may be omitted: null with omittedValues is not an observed null. The metadata retains byte count/digest and run-output path or scenario check reference. Ordinal distributions and paired observations are not calibrated probabilities, causal claims or automatic winners.

Read reports without filling your coding agent's context

Opt in to a compact execution receipt rather than printing every output and the whole recipe:

import json
from firedrill import batch_receipt

result = client.simulate(recipe, target)
print(json.dumps(batch_receipt(result)))

After asynchronous execution, use the same pure batch_receipt(result) helper without await. It preserves batch identity, actual execution counts, assessment counts when known, gate, report URL and the real recovery operationId when present. At most ten small run references are included; sample.complete says whether they cover the population. This is not a run page or success verdict, and no cursor is invented. Use its reads paths or the inspection methods below for the full retained population and evidence. Unknown whole-population assessment counts remain None unless the existing result covers every run. No request, evaluation or execution occurs, and the original SDK result remains unchanged.

Use inspect_batch(batch_id) and inspect_report(run_id, limit=10) for operator views. Large values become references with their source, JSON Pointer, byte count and digest. Existing batch() and report() keep their original response shapes. Operator report event pages accept limit 1–20, cursor and the separate check_cursor; follow each returned cursor unchanged.

Both sync and async report / inspect_report accept flat optional event_kind (exact, case-sensitive, 1–120 characters) and event_producer (literal, case-sensitive prefix, 1–200 characters). Choose values from retained events; no trimming, case folding or search-expression parsing occurs. These filters select activity only, not checks, scores or gates. Invalid controls or Unicode are rejected before a request.

page = client.report(run_id, event_kind="runner.started", limit=25)
if page["nextCursor"] is not None:
    next_page = client.report(run_id, event_kind="runner.started", limit=25,
                              cursor=page["nextCursor"])
    print(next_page["events"])

Use await for the same calls on AsyncFiredrill. Raw pages retain their 1–100 limit (default 100); compact operator pages use 1–20 (default 10). Omit cursor for the first page. Filtered continuation cursors are opaque, at most 1024 characters and bound by the service to the run, scope and exact filters. Keep the same filter arguments on continuation; clear the cursor when changing them. Unfiltered decimal cursors remain supported. Never invent or decode a filtered cursor. Additive activity metadata retains wire names eventKind and eventProducer in filters, plus filtered and the exclusive afterSequence anchor; exported ActivityPageMetadata / ActivityFilters describe that wire shape. Operator views retain it in document["value"]["activity"]. Sequence gaps from filtering are not missing evidence. check_cursor still pages checks independently.

document = client.inspect_report(run_id)
print(document["value"])
reference = next((item for item in document["references"] if item["source"] == "output"), None)
if reference is not None:
    part = client.read_value(
        run_id, source="output", pointer=reference["pointer"],
        sha256=reference["sha256"], limit=4096,
    )
    print(part["text"])  # A bounded JSON fragment, not necessarily complete JSON.
    # Fetch another chunk only if the investigation needs it.
    if part["nextOffset"] is not None:
        next_part = client.read_value(
            run_id, source="output", pointer=reference["pointer"],
            offset=part["nextOffset"], sha256=part["sha256"], limit=4096,
        )
        print(next_part["text"])

AsyncFiredrill accepts the same arguments; await each method. read_value supports output, input, scenario, configuration, target, assessment and event, plus check_actual/check_expected with the exact check_id; event requires its exact event_id, including literal service/sequence separators. For example: client.read_value(run_id, source="check_actual", check_id=check_id). For an omitted check operand, omit sha256 on the first read: its canonical assessment digest is a different representation. Continue with the returned serialization digest. These reads select pinned data or frozen closure evidence, not live Tool state. Missing observations return unavailable rather than null. Read a recipe subtree with read_batch_value(batch_id, pointer="/scenarios/0"). Pointers use JSON Pointer escaping (~0 for ~, ~1 for /); empty selects the whole source.

Each call returns only one chunk: 8192 bytes by default, with a permitted limit of 256–32768. Continue only with the returned nextOffset and sha256, keeping the source, pointer and event/check ID unchanged. Offsets count UTF-8 bytes, not Python characters. Parse JSON only after retrieving its complete serialization. Changed or unavailable content produces an error, never a successful empty value. These reads do not rerun your target, request evaluation or automatically fetch the remainder. RetainedValueChunk, RetainedValueReference and OperatorDocument are exported typed dictionaries; their response keys preserve wire camelCase.

Content availability and removal

Both clients expose client.retention. policy() reads the active new-resource policy; metadata(kind=..., id=..., cursor=..., limit=...) returns {"records": [...], "nextCursor": ...} with fixed noncontent facts only. Omit filters you do not need and follow the opaque cursor unchanged. Existing history is not assigned retroactive expiry. A list row with metadataOnly: True has no scenario/name/recipe payload. Full expired, deleted or deletion-pending reads raise a typed 410 instead of returning empty successful content.

Removal requires the freshly authorized human owner; CI/machine credentials do not gain deletion authority:

preview = client.retention.preview(targets=[{"kind": "batch", "id": batch_id}])
# Review resources, requiredCascade, survivingDefinitions and exact scope.
# Save this exact input and key before dispatch; this choice consents to the cascade.
removal = client.retention.remove(
    targets=preview["targets"], graph_digest=preview["graphDigest"],
    confirmation_digest=preview["confirmationDigest"],
    cascade=preview["requiredCascade"], idempotency_key="remove-selected-batch-1",
)
print(client.retention.get(removal["id"]))

The async equivalent awaits the same methods and keyword arguments. Cascade is mandatory; [] is an explicit empty choice. If dependencies changed, review a new preview before confirming. No method retries deletion automatically. On an uncertain response, inspect the known removal ID or explicitly resend only the original saved commit/key. reconcile(removal_id) deliberately continues that same intent with human-owner authority; it creates no replacement operation. Pending/blocked is not success. livePayloadDeleted is not instant backup erasure: backup retention is separate and restoration reapplies tombstones.

Optional service bindings may declare "control": {"schemaVersion": SERVICE_CONTROL_VERSION, "operations": [...]} for authored scenario-controller interventions. Those IDs must be distinct and disjoint from target operations; malformed or overlapping choices fail before admission. Declared service sequence steps use control authority separately. The target's context.tools, exported grants and callable operations retain only its target operation set. This is authored synthetic behavior, not a claim that every external dependency or fault can be controlled.

For a privately published build that actually declares both operations, a binding and ordered service-step fragment can look like:

binding = {
    "alias": "dependency", "buildId": build["id"], "seed": "31", "operations": ["lookup"],
    "control": {"schemaVersion": SERVICE_CONTROL_VERSION, "operations": ["set_availability"]},
}
intervention = {
    "id": "outage", "kind": "service", "service": "dependency",
    "operationId": "set_availability", "arguments": {"available": False},
}

Place the binding in recipe["services"] and the intervention between real target steps in the scenario sequence. End it with an explicit later control step or supported service-only restore. Controller and target observations are separate evidence; an outage is not automatically an agent failure. This does not intercept arbitrary networks or extend target connection authority.

Advance a managed Tool clock during a live simulation

For a new run, explicitly declare the binding's control as {"schemaVersion": SERVICE_CONTROL_CLOCK_VERSION, "operations": [], "clock": {"advance": True}}. Import SERVICE_CONTROL_CLOCK_VERSION from firedrill; its value is 2026-10-15-service-control.2. Operation-only version 1 remains supported and does not authorize live clock changes. The target receives only its separate ordinary operation grants.

controls = client.interventions.controls(run_id)
tool = next(item for item in controls["bindings"] if item["alias"] == "dependency")
if (not tool["interventionAvailable"] or not tool.get("clockAdvance")
        or tool["instanceId"] is None or tool["epoch"] is None
        or tool.get("virtualTimeUs") is None):
    raise RuntimeError("Clock control is unavailable")
input = {
    "action": "clock", "service": tool["alias"], "instanceId": tool["instanceId"],
    "epoch": tool["epoch"], "toUs": tool["virtualTimeUs"] + 60_000_000,
    "maxEvents": 100, "reason": "Observe one minute of managed Tool time",
}
# Save this exact input and key before sending; do not recompute after uncertainty.
receipt = client.interventions.apply(run_id, input, operation_key="dependency-clock-1")
print(receipt["intervention"]["result"])

The async client exposes the same methods with await. toUs is an absolute microsecond timestamp; maxEvents is required, 1–1000. Inspect the result's reachedUs, scheduledEventsProcessed, failures and stoppedEarly rather than assuming that recorded completion means the requested time was reached. An exact-key replay returns the original receipt; any further bounded advancement is a new deliberate action. Backwards advancement is rejected. This never advances your agent's clock, extends wall-clock leases, reruns the target, or rewinds its memory. The run and Tool history record operator attribution. Manual Tool clocks and run-owned interventions both reject a backward destination with FD_SERVICE_CLOCK_REWIND_UNSUPPORTED before clock movement or event dispatch. That definite rejection does not fence the Tool as uncertain. Read its current clock before submitting a corrected destination with a new operation key.

Optional built-in Tools

For optional manual use, start one or many named independent instances with the authenticated, scoped client. This path does not start your agent:

release = client.services.catalog_release("gmail")
setup = client.services.start({
    "tools": [{
        "alias": "mailbox",
        "source": {"kind": "catalog", "catalogId": release["id"],
                   "releaseDigest": release["releaseDigest"]},
        "data": {"kind": "starter"},  # or explicit empty state
        "operations": [operation["id"] for operation in release["operations"]],
        "seed": "0", "ttlSeconds": 1800,
    }],
})
print(setup["operationKey"], setup["status"], setup["items"])
# After partial completion, recover the same journaled intent:
recovered = client.services.resume_start(setup["operationKey"])

Review the declared operations; add distinct aliases for additional Tools or copies. Saved data uses source:{"kind":"build","buildId":...} with data:{"kind":"saved","datasetId":...}. Discover exact selections with catalog(), builds(), build(id), datasets(build_id=...) and dataset(id, include_records=True). All first-start selections are checked before allocation, including exact dataset compatibility. Empty means empty; starter records are an explicit catalog choice.

The client generates a durable private operation key unless you supply operation_key. wait_timeout_ms bounds acquisition following; it does not change instance lifetime. Partial success keeps its instances; recovery uses the retained build, not a newer catalog release, and never replaces an ended instance. on_progress is advisory; notification errors cannot interrupt setup. Ready receipts contain token-free endpoints, not automatically issued grants. Create connection credentials explicitly and deliver them privately.

item["instanceObservation"] uses schemaVersion:"2026-10-16-tool-start-observation.1", state and lastConfirmedAt. Only confirmed follows a fresh authorized instance GET; cached create responses are not current availability. A failed read leaves any retained instance explicitly unconfirmed/last-observed, with null time for historical unknown confirmation. Access hints are absent for unconfirmed or inactive instances. Authorization rejection stops the compound operation rather than continuing allocations or returning cached instance data. Preserve its original key/private journal; prior confirmed progress remains historical, not proof of continuing access or availability. Sync and async share this behavior.

Use instances(lifecycle="active") or instances(lifecycle="history") for newest-first pages and follow their opaque cursor. history(instance_id) continues to expose retained lifecycle events after termination. Existing snapshot/reset/rewind/clock/extend/pause/resume/destroy methods retain explicit epoch/key controls. Async clients use the same APIs with await; no Node process or event-loop wrapping is required. Tool-free simulations remain valid, and recipe-bound Tools allocate independently for each case.

Manual start plans have no fixed instance-count quota. The normalized plan has a 4 MiB serialized boundary; creation remains sequential under actual service capacity. Response/private-journal byte bounds remain. Preserve the original key and retained service receipts after a partial or uncertain setup. The CLI keeps its smaller 256 KiB selected-file/interactive-plan input bound.

services.rewind(instance_id, snapshot_id, epoch=current_epoch, idempotency_key=saved_key) aliases the manual Tool reset operation. It restores the snapshot's modeled state, virtual clock, pending work and random generator state, increments epoch, fences old grants and preserves history. Use the same exact input/key after an uncertain response; changing alias is not a new restore. Neither operation resets customer agent memory, application databases, wall clocks or external providers. Run-owned Tools retain their own run/sequence lifecycle.

For simulation definitions without manual allocation, acquire a build/dataset:

Inspect the qualified catalog before selecting a controlled dependency:

release = client.services.catalog_release("gmail")
acquired = client.services.acquire(
    release["id"], release_digest=release["releaseDigest"],
    initial_data="starter", idempotency_key="acquire-mailbox-1",
)
if acquired["phase"] != "ready":
    print(acquired["id"], acquired["phase"], acquired["error"])
    print(client.services.acquisition_by_key("acquire-mailbox-1"))
else:
    print(acquired["buildId"], acquired["datasetId"])

Save that intended key/input before sending. Choose initial_data="empty" when you want no starter records. catalog() lists metadata; acquisition(id) and acquisition_by_key(key) inspect the original scoped receipt. Async clients offer the same methods with await, without Node or a subprocess.

Acquisition creates project-owned immutable build/dataset IDs, not an instance, credential or target execution. Once ready, pin the IDs in your existing recipe's services binding with your chosen alias, seed and explicit operations; normal simulation preparation allocates independent instances. Manual exploration can use services.create instead. Starter datasets retain their declared modeled time. A pending, failed or uncertain acquisition is not a ready Tool; inspect the same key rather than silently selecting another release or repeating under a replacement key.

Catalog releases expose a bounded semantic API/MCP subset and read-only record view, not complete vendor REST APIs or vendor application clones. Private service authoring and tool-free simulations remain equally valid.

Optional service authoring and lifecycle

Both clients expose client.services. Sync calls return the retained wire dictionary; async calls are awaited, with network and selected-file I/O outside the event loop. All compilation, data/state, permissions and history remain in Firedrill. There is no Node dependency or local service runtime in the package.

services = Firedrill().services
build = services.publish_directory(
    "./my-tool", manifest_path="manifest.json", entry_point="service.ts",
    files=["service.ts", "formatting.ts"], view_path="view.json",
    idempotency_key=saved_build_key,
)
dataset = services.seed(build["id"], records, idempotency_key=saved_dataset_key)
instance = services.create(
    build["id"], dataset_id=dataset["id"], seed="42", actor_operations=["read"],
    idempotency_key=saved_create_key,
)
response = services.invoke(
    instance["id"], "read", {}, epoch=instance["epoch"],
    idempotency_key=saved_call_key,
)

view_path is optional, explicit data-only JSON, not customer JavaScript. Only selected source files are read: 1–128 visible relative .ts/.js/.mjs files, at most 1 MiB each and 4 MiB total, including the entrypoint. The manifest is at most 1 MiB, the view declaration 64 KiB. Traversal, hidden path segments, symbolic links, hard links, nonregular files and invalid UTF-8 are rejected. Whole-directory upload is never implicit. publish(manifest=..., entry_point=..., files=[{"path": ..., "content": ...}], view=..., idempotency_key=...) accepts explicit source text instead. The service validates the canonical manifest/view schema and compiles it.

publish(..., dataset_policy=policy) and publish_directory(..., dataset_policy=policy) also accept explicit finite JSON containing schemaVersion: "2026-10-15-dataset-policy.1" and declared unique/reference/count constraints. The service validates the canonical contract; use client.schema("dataset-policy") to discover it. No Node process or local engine is needed. Policy is pinned to the published build, separate from its namespace JSON Schemas; relationships are never inferred from IDs.

seed(build_id, records, metadata=metadata, idempotency_key=saved_key) accepts optional schemaVersion: "2026-10-15-dataset-metadata.1" metadata. Its optional provenance declares kind (authored, generated, imported) and sensitive; imported data requires authorizedImport: True, a caller assertion rather than verified consent. Optional mappings pin each target dataset's exact ID/version and namespace, using literal JSON Pointers. Read client.schema("dataset-metadata") for the full shape. Keep input/key unchanged after uncertain delivery. Both sync and async clients preserve these fields.

Retained build details include datasetPolicy and datasetPolicyDigest; lists contain only its digest/constraint count. Dataset dictionaries retain metadata, validation and validationStatus, distinguishing schema/domain/mapping checks from caller-declared provenance. Missing or null older fields mean not recorded, not clean or synthetic data. A successful validation receipt explicitly does not establish real-world fidelity; no provenance is invented when omitted.

Additional methods (also native async):

  • get(instance_id), list(cursor=None, limit=100) and history(instance_id, cursor=None, limit=100) inspect retained resources/events.
  • snapshot(instance_id, epoch=..., idempotency_key=...) returns the snapshot ID.
  • reset(instance_id, snapshot_id, epoch=..., idempotency_key=...) restores this instance's snapshot and returns its advanced epoch.
  • clock(instance_id, epoch=..., to_us=..., max_events=None, idempotency_key=...) explicitly advances virtual time with an optional bounded event count.
  • pause, resume and destroy take the instance ID and explicit epoch and idempotency_key. Destroy stops use while retaining state and history.
  • extend(instance_id, epoch=..., expected_expires_at=..., expires_at=..., idempotency_key=...) changes a manual Tool's real-world expiry only. Read the current epoch and exact expiresAt first; both timestamps must be canonical UTC ISO strings including milliseconds. Persist the observed/new timestamps and key before sending and reuse them unchanged after an uncertain response. The new deadline must be later and within the service's configured maximum. This does not advance simulated time, resume paused compute or renew connection credentials; expired and run-managed Tools cannot be extended this way.

seed(build_id, [], ...) is intentionally empty. Omitting dataset_id creates no seed records. The explicit decimal seed and actor_operations are required; no sample data or broad permissions are inferred. Use selected authored operations to read or mutate domain state; get(id)["runtime"] is an observed runtime summary, not an unrestricted state database. history is the service lifecycle/evidence page, not a fabricated client log. Follow returned nextCursor until null.

Mutations need a caller-saved stable key and exact input. The client never retries automatically or advances an epoch after failure. After uncertain delivery, inspect get/history and replay only that same key/input. A new key means a new action. These explicit controls do not maintain an automatic service-action journal; simulation journals remain separate. Engineering bounds protect each operation. Native source-file guards are qualified on POSIX, not Windows.

Scoped HTTP, MCP and UI instance connections

client.services exposes create_connection(instance_id, *, epoch, audience, purpose, allowed_operations, ttl_seconds, idempotency_key), connections(instance_id, cursor=None, limit=100) and revoke_connection(instance_id, connection_id, idempotency_key=...). Use audience="http", "mcp" or "ui" and an explicit purpose="manual" or "target"; purpose records intent, not execution attestation. Grants can only narrow the existing actor permissions and belong to the selected project/environment/instance/epoch. Async service methods are awaited directly; no CLI/Node subprocess is used.

ConnectionGrant has safe repr() and metadata-only to_dict(). grant.reveal_token() explicitly accesses the one-time credential for protected storage or authorized target configuration. grant.reveal_launch_url() explicitly returns a UI launch URL whose fragment is also secret; normal representations omit both. A UI grant requires an actual available declared view and its permitted operation subset. Do not serialize internal attributes, include credentials in evidence or pass the operator token to the target. Identical-key replay returns metadata with token_delivery="not_repeated" and no secret. Lost delivery requires inspecting/revoking that exact connection and creating a new grant with a new saved key. An unrevoked grant can still be expired, epoch-stale or parent-revoked. UI launch uses a one-time exchange for an isolated browser session, not the operator credential. HTTP compatibility is limited to the selected service's declared service.httpRoutes; it is not an unrestricted provider proxy. The same seeded instance and operation permissions apply.

Provenance

The orchestration and adapters here are newly authored for the root-owned v0.1 wire contract. No legacy source was copied or adapted, and no legacy checkout is part of this package's dependency graph. Runtime dependencies: Python standard library plus certifi==2026.7.22 (Mozilla CA bundle, MPL-2.0). The dependency's own licence/notices remain with its separately installed package; its certificate data is not copied into this client wheel. Packaging build dependencies: setuptools and wheel. Artifact wire fields and recovery ordering follow the root-owned artifact contract and sibling TypeScript attachments.ts; file I/O, transport, sync/async bridging and journaling are native Python implementations, without a TypeScript/Node runtime. Service wire fields, request bounds and selected-file policy follow the sibling TypeScript services.ts; transport and descriptor-relative reads are new native Python code. The qualification reuses the successor CLI's explicit room fixture; that fixture is replaceable and is not bundled as product defaults. Run-bound bindings and grant URLs follow ServiceBindingSchema and the control coordinator's /v1/runs/:id/services response. Native context bridging, exact response validation, in-memory-only secret delivery and recovery are newly authored Python code. No private service-host/runtime implementation is embedded. Sequence wire fields/recovery ordering follow the root-owned SequenceReply and SequenceCompletion contracts and sibling sequences.ts. The native Python step coordinator is newly authored; service-clock and restore semantics are not copied into the client. Credential wire/version and recovery semantics follow the canonical credential contract and sibling credentials.ts. The native implementation does not embed the service's credential derivation or authorization logic.

Prepare without execution

Both native clients expose preview and save_scenario; use await with AsyncFiredrill. No Node/CLI bridge is involved. With client already authenticated and scoped to the selected project/environment:

scenario = {"id": "route", "name": "Choose a supplied route", "input": {"destination": "library"}}
estimate = client.preview({
    "name": "Route preview", "scenarios": [scenario],
    "target": {"kind": "native", "name": "choose_route"}, "repetitions": 3,
})
print(estimate["admission"], estimate["expansion"], estimate["limitations"])
saved = client.save_scenario(scenario, expected_revision=None, idempotency_key="save-route-1")
retained = client.scenario(saved["id"], saved["revision"])

Preview accepts an inline recipe or selected JSON path, requires a target declaration but never a callable, and invokes no target/provider/Tool. It creates no batch or saved definition and reserves no capacity. Current admissibility is an estimate, not guaranteed admission; runner readiness, reset/capture setup, customer inference/runtime costs and remaining assessment budget are not proved.

Save requires runs:write, but no target binding. None explicitly creates a new scenario ID; an intentional append supplies the current revision and a new key. Stale bases conflict without overwriting history. Reconcile an uncertain response with the same key and unchanged input/base revision. Reuse retained["scenario"] inside a later recipe. Environment-restricted definition reads require an authorized environment context, supplied from the client or environment_id. The definition remains project-owned; saving does not admit a simulation.

Queue now, attach a runner later

queued = client.submit(recipe, idempotency_key="release-2026-10-16-1")
retained = client.resume_submission("release-2026-10-16-1")
# Later, when execution is intended, in the prepared customer process:
result = client.run(retained["batch"]["id"], target=my_application_adapter)
# AsyncFiredrill exposes the same methods with await.

Submit accepts an explicit JSON recipe or path, including a native target descriptor without a callable. Any parallel non-isolated recipe must already declare targetExecution before submission; queue-only admission cannot defer that acknowledgement to a later runner. Command/HTTP targets attach without a callback. Python may queue an isolated_node descriptor, but executing it requires the existing TypeScript isolated adapter; Python does not silently select another boundary. The service canonically validates before admission; no Node/CLI subprocess is needed to submit. The client requires read and runs:write in its selected scope.

Save a unique printable ASCII key (1–200 characters, no whitespace) before dispatch. Exact recipe, API/scope/identity and original origin/CI context are durably saved under state_dir/submissions before admission. Repeating the key checks the frozen intent. resume_submission(key, submission_origin=None, ci_context=None) reads that original intent; it never relabels it with today's workflow environment. Explicit changed declarations conflict. Keep the private state directory; absent journals do not cause replacement work. A key reused under a different local scope conflicts rather than replacing its original intent.

SubmissionReceipt is a dictionary with schemaVersion, compact current batch snapshot, submissionKey and scoped runner/inspection continuation. It omits the frozen recipe; use scoped retained reads if those values are needed. It confirms creation/reconciliation, not that the batch remains queued or passed; every replay reads authorized cloud state. SubmissionError.to_dict() adds the saved submission key and selection to the typed error, without credentials. Submit never invokes/claims targets, cancels, follows execution, requests assessment or writes CI summaries. Cancelling an async submission wait may leave its bounded admission request in flight: reconcile the same key, do not assume nothing was admitted. Submission journals are rejected by execution recovery. After a runner begins, use its real execution operation ID and journal to reconcile uncertain execution.

Observe the retained batch

observation = client.follow(batch_id, timeout=30, poll_interval=1)
print(observation.observation["state"], observation.result.gate)
# AsyncFiredrill: observation = await client.follow(batch_id, timeout=30)

Follow needs the original selected project/environment and reads context and compact results only. It does not claim work, invoke a callable, cancel targets, request assessment, change journals or write a CI summary. Another local/CI runner may own execution. Read endpoints may reconcile expired leases server-side.

timeout is seconds, default 300, maximum 3600; poll_interval is .25–30 seconds, default 1. timeout=0 attempts one snapshot with a shared 30-second observation ceiling across context and result reads, subject to each transport timeout. Optional stop=threading.Event() or async task cancellation stops observation, not execution. One bounded authenticated GET may finish in its daemon read thread after observation stops; it cannot dispatch another request or update the saved snapshot. There is no nested event loop or Node dependency.

FollowResult.to_dict() uses 2026-10-16-follow.1 with result and observation. States are settled, timeout, interrupted, and read_error. Settled requires terminal execution and whole-population assessment coverage with no pending entries, not a passing result. Missing coverage stays unsettled. Other states identify the last-confirmed snapshot, client-read completion timestamp and exact batch/scope continuation. No confirmed snapshot raises FOLLOW_UNCONFIRMED; access, identity, scope and retention rejections raise instead of serving cached content. Retained result/gate are never replaced by observation status. Read remaining runs with list_runs and their evidence with inspect_report.

Declare the population's purpose

recipe["populationPurpose"] = {
    "schemaVersion": "2026-10-16-population-purpose.1",
    "kind": "exploration",
    "rationale": "Explore behavior across explicit input variations",
}

exploration, fixed_suite, held_out, and failure_selected are caller-declared intent, not independent attestations. Native JSON recipe loading retains the optional field. Preview/summary/comparison expose populationDisclosure, distinct from matrix/sample materialization metadata. Missing metadata is not_recorded, not an inferred purpose. Held-out does not prove unseen cases; failure-selected scores do not estimate production incidence. Exported PopulationPurpose, PopulationDisclosure and PopulationProvenance types only describe wire fields.

Preparation, cleanup and diagnostics

Both native clients accept on_progress on simulate, run and resume. Fresh simulate reads the canonical non-reserving preview before admission and notifies {"phase": "preexecution", "preexecution": preview, "operationId": id}. The preview includes expansion, target boundary, configured resource ceilings and separate customer costs. Notifications are advisory: async callbacks are scheduled but not awaited, errors do not change execution, and callbacks are not an approval barrier. Use preview separately for an explicit application review step. Without on_progress, a bounded JSON progress.preexecutionSummary is written to stderr before admission, leaving stdout untouched. It discloses selected scope, counts, numeric bounds, isolation and separate unknown customer costs, not inputs, Tool credentials, target URLs or command arguments. A supplied callback replaces that presentation. Neither mode prompts or waits for approval in CI. Numeric assessmentPolicy limits come from the service; explicit null is preserved, while an older service omitting that policy is reported as "unknown". Recovery uses the original key; possibly dispatched admissions never depend on a new preview. Replayed saved estimates use preexecution_retained.

Tool start progress and results include the existing policy read; its optional resourceEnvelope describes enforced ceilings, not measured use or reservations. Older-service absence is unknown, not unlimited capacity.

BatchResult.lifecycle, to_dict() and batch_receipt preserve the service's optional lifecycle object, including additive fields. Evidence completeness is limited to declared captures and run-owned Tool closure records. It does not prove every intended observation exists; missing lifecycle is unknown. Native command execution cleans up the owned process group even on normal completion. Leader exit starts cleanup independently of inherited output pipes; draining those pipes has its own short bound rather than consuming the target deadline. Recovery of a saved running native, command or HTTP invocation fences further automatic dispatch even after its service lease becomes terminal, because a terminal lease does not establish that the customer-local or remote invocation stopped. Escaped descendants remain unobserved. Windows child-only cleanup cannot confirm group absence, so it reports unconfirmed cleanup instead of success.

Native wrapper exceptions can explicitly carry stop_unconfirmed = True (stopUnconfirmed = True is also recognized) when observing the wrapper's completion does not establish that its inner work stopped. Both sync/async adapters preserve that flag through bounded exception cause/context/group chains, record runner.target_stop, and persist the coordinator stop fence. Ordinary exceptions and returned metrics do not imply this flag. Preserve the original exception chain; no SDK can infer arbitrary detached work from a wrapper that silently returns a normal value.

Both clients expose result_lifecycle(batch_id) for authorized metadata-only BatchLifecycleRead access without reading expired recipe/output payloads. result and follow preserve the original content-unavailable error and attach this separately read receipt as error.retained_lifecycle when available; to_dict() uses error.retainedLifecycle. Every read keeps the original selected project/environment. Metadata expiry or a secondary read failure does not replace the original error, fabricate a BatchResult, or return a stale follow snapshot. Follow's observation deadline bounds this extra read too. When child metadata has been removed, lifecycle.evidence.metadataComplete: false identifies that unknown coverage; it is not a claim that evidence or cleanup was unnecessary. The optional lifecycle.cleanup.targetStopsUnconfirmed counts retained native or HTTP stop observations separately from command process-group cleanup.

FiredrillError.to_dict() uses 2026-10-17-client-diagnostics.1 with category, workStarted, uncertain, retry and retained IDs. workStarted includes operation side effects; possibly sent admissions remain "unknown", even without a batch response. same_request means read-only retry safety; reconcile requires the retained intent/key; never forbids automatic unchanged retries. These fields do not perform a retry or authorize a new target invocation.

Private journal directory, lock, read, decode and atomic-save failures are typed FiredrillError diagnostics (JOURNAL_IO_ERROR, JOURNAL_INVALID, or the precise unsafe/ownership code). They retain the original execution operation_id, known batch and single active-run handles. Positively unsent work reports workStarted: false; a sent or unreadable intent remains "unknown", not a claim that nothing happened. Preserve the original private journal and any temporary file, repair local storage, and reconcile the same operation ID. Submission and Tool-setup errors retain their original submission/setup keys, not the journal's internal hashed filename. Catch FiredrillError to handle these storage failures as well as transport/recovery errors. No automatic replacement execution or deletion of partial recovery files is performed.

Metadata

Release files for firedrill-run 2.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distribution (wheel)

Table of built distributions (wheels) for firedrill-run 2.0.0
File Interpreter ABI Platform
firedrill_run-2.0.0-py3-none-any.whl Python 3 none any Details

Release files / firedrill_run-2.0.0-py3-none-any.whl

Download URL firedrill_run-2.0.0-py3-none-any.whl
Size 158.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7a74e7b0d153c6e7689c1b4378a915f9c6eb1181a0c288cb49f9a7d7c4068b98
BLAKE2b-256 checksum
How to use checksums
89ebdb42cdb5981e144f0d7ddb9584424ca4a7fa92b6b4b6e1e0cf6ef186d236
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.6

Release history Release notifications | RSS feed

2.1.0

1 release file

This release

2.0.0 This release

1 release file

1.0.1

5 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page