Skip to content

Lifecycle and state

Use this guide when embedding an Environment in a Host that retains work between Runs. A Provider creates adapters; an adapter connects to one target; state lets a later adapter identify that target. None is a durable worker lease.

For a first file operation without an Agent, start with Getting started.

Lifecycle at a glance

---
config:
  flowchart:
    useMaxWidth: false
  sequence:
    useMaxWidth: false
---
flowchart LR
    Host[Host policy and persistence] --> Definition[EnvironmentProviderDefinition]
    Definition --> Adapter[Fresh Environment]
    State[EnvironmentState or none] --> Adapter
    Runtime[Fresh runtime collaborators] --> Adapter
    Adapter --> Harness[Agent Harness Run]
    Harness --> Operations[Files, shell, processes, output, and ports]
    Adapter --> Latest[Detached cached state]
    Latest --> Host

A normal Run follows this sequence:

  1. The Host resolves an allowlisted Provider type from its catalog.
  2. The definition validates the account configuration, the credential, and the credential-free target recipe.
  3. The Host supplies the latest authoritative EnvironmentState.
  4. The definition acquires its runtime collaborator, then constructs one fresh adapter; everything before the runtime factory is pure. A runtime the Host passes in is borrowed; one the definition acquires belongs to the adapter.
  5. The Host prepares eagerly, or lets the first operation prepare lazily. Harness binds the local scope, uses operations, exports cached state, and closes it.
  6. The Host persists the latest state and applies retention policy separately.

close() releases the Session, temporary output, clients, and admission owned by that adapter, including a runtime create() acquired for it. It is idempotent and non-destructive. Shared Envd Device runtimes the Host passes in belong to the Host and are closed separately at Host shutdown. Harness never calls destroy().

When retention policy selects removal, the Host constructs a different fresh adapter from the exact current state and calls destroy() explicitly. Successful destruction clears that adapter's cached state. A failed or unknown outcome preserves the last validated state for inspection or retry.

Resolve and construct an Environment

A persisted EnvironmentProviderSpec contains only a Provider type and credential-free JSON recipe:

from a13n_harness.providers.catalog import ProviderCatalog
from a13n_harness.providers.environment.builtins import select_builtin_environment_providers
from a13n_harness.providers.environment.models import EnvironmentProviderSpec

spec = EnvironmentProviderSpec(
    provider_key="direct_local",
    configuration={
        "root": {"path": "/srv/agent-workspaces/current"},
    },
)

catalog = ProviderCatalog(select_builtin_environment_providers(("direct_local",)))
definition = catalog.require(spec.provider_key)
environment = await definition.create(
    spec.configuration,
    environment_id="workspace",
    state=None,
)

There is no configuration schema version: a Provider owns exactly one recipe model, and changing an input's meaning changes the Provider type. Selection, recipe validation, and adapter construction are inert. enter() also performs no target I/O. prepare() creates, resumes or connects the target; ensure_ready() triggers it on first use when preparation is lazy.

Pass the fresh adapter to Harness:

result = await executable.run(
    "Inspect the workspace",
    environment=environment,
)

Harness enters and closes the adapter exactly once. Construct another adapter for every independent Run, even when several Runs target the same workspace, container, VM, or remote sandbox.

Re-enter a stateful target

The Host supplies state before entry:

current_state = await state_store.load(environment_key)
environment = await definition.create(
    spec.configuration,
    configuration=backend_configuration,
    credential=current_credential,
    environment_id="workspace",
    state=current_state,
)

try:
    result = await executable.run(
        "Continue the task",
        environment=environment,
        previous_state=previous_harness_state,
    )
finally:
    await state_store.publish(environment_key, environment.dump_state())

dump_state() is synchronous and performs no target I/O. It returns a detached deep copy of the latest validated cached state, so caller mutation cannot alter the adapter's cache. Providers update that cache as soon as changed target identity is known, before later readiness work that might fail.

State is a soft reference, not proof that a target still exists. Construction validates its codec and compatibility; during preparation, the Provider inspects the exact target selected by that validated state. Merely entering the adapter does not perform that inspection. It may create a replacement only after authoritative absence and according to that Provider's contract. It never treats an incompatible target, ambiguous discovery, unavailable control plane, or unknown mutation outcome as absence.

EnvironmentState contains no bearer credential, live client, task, process-local handle, Harness mount policy, or destruction authority. Storage, authorization, retention, scheduling, and selection of the authoritative version remain Host responsibilities.

Explicit destruction

Destroy requires a fresh, not-yet-entered adapter:

cleanup = await definition.create(
    spec.configuration,
    configuration=backend_configuration,
    credential=current_credential,
    environment_id="workspace",
    state=current_state,
    allow_create=False,
)
try:
    await cleanup.destroy()
finally:
    await state_store.publish(environment_key, cleanup.dump_state())
    await cleanup.close()

The Provider removes only the exact backing target and provider-owned bootstrap material represented by validated state. Shared Host directories, Docker bind sources, and external named volumes remain externally owned.

Do not use context exit, Harness completion, suspension, or cancellation as an implicit destruction signal. Those paths close process-local resources only.

Readiness, recovery, and maintenance

Operation What it does
enter() / async with Bind the one-use scope; no target preparation
prepare() Create, resume, or connect according to current validated state; requires entry
check_ready(operations) Check an entered connection without provisioning or recovery
ensure_ready(operations) Prepare on first use, check required families, and perform only Provider-supported recovery
recover() Explicitly authorized in-scope recovery where supported; not generic mutation replay
reconcile() Observe an abandoned preparation as running/stopped/absent without creating, starting, or replacing a target
stop() Resumable target stop where supported; distinct from close and destroy
keepalive(deadline=..., operation_id=...) Refresh retention for a supported target under Host maintenance ownership
dump_state() Detached cached state, with no target I/O
close() Idempotent local-resource cleanup
destroy() Remove the exact owned target through a fresh, unentered adapter

A supported readiness recovery can report environment_connection_refreshed or environment_rebuilt rather than silently continuing the originally requested operation. Same-target reconnection and replacement of a lost generation have different consequences. Recheck process observations and temporary files before continuing. Remote Envd preparation failures require fresh adapters rather than assumed in-scope recovery.

Provider capability declarations tell a Host which maintenance paths it may select:

Built-in Provider Managed selection Resumable stop Destroy Keepalive required
Direct Local Yes, stateless Declared, no-op over the Host directory Declared, no-op over the Host directory No
Local Envd Yes, stateless No No No
Docker Yes Yes Yes No
E2B Yes Yes Yes Yes
Daytona Yes Yes Yes No
Modal Yes Managed only, filesystem snapshot Yes Yes, fixed deadline only
Vercel Sandbox Yes Yes Yes Yes
Fly.io Sprites Yes No; automatic native sleep Yes No
Runloop Yes Yes Yes Yes
Remote HTTP / WebSocket Envd No, externally owned No No No

See the six-cloud comparison for filesystem, memory, and expiry differences. Managed selection does not mean every Provider creates storage or owns the Host directory. Unsupported base methods are not usable merely because they appear on the abstract interface. Schedule keepalive from the Provider's keepalive_horizon and actual target policy; do not treat the base default as a universal cloud TTL.

Handle typed failures

Configuration, catalog, lifecycle, and backend failures use EnvironmentProviderError. Operation failures use EnvironmentError. They are not interchangeable with a model-tool error envelope.

A Provider error has a stable code, category, certainty, recovery hint, bounded context, and richer local description/details. Categories are invalid, unsupported, missing, denied, conflict, unavailable, timeout, unknown_outcome, cleanup, and provider_failure.

Evidence Host response
Certainty not_dispatched No dispatch occurred; fix the stated input/runtime condition before deciding on another attempt
Certainty known Use the known outcome; do not infer that every known error is retryable
Certainty unknown Reconcile the original operation/target before any possible replay
Hint fix_input Correct validated configuration or request data
Hint refresh_runtime Reconstruct current authorized runtime collaborators
Hint retry_same_operation Preserve the owning operation identity and its retry contract
Hint reconcile Inspect durable Provider/Host evidence
Hint none No automatic recovery recommendation

Use error.safe_projection() for public presentation. It retains bounded correlation and a safe category message, excluding the rich local description/details. Keep local diagnostic access appropriate to the Host; blindly serializing the exception is not equivalent to the safe projection.

Always publish the latest validated cached state according to Host policy, including after failed readiness or unknown cleanup. Never convert control-plane unavailability into target absence, replace a target to hide incompatible state, or call destroy() as generic error handling.

Host checklist

  • Construct one fresh adapter per independent Run, including resumed Runs.
  • Persist the latest detached state after success or failure; publishing it remains a Host policy decision.
  • Treat an unavailable control plane or unknown mutation outcome as uncertainty, not target absence.
  • Close local resources independently of retention. Destroy only an exact validated target through a fresh adapter.
  • Keep credentials, authorization, and live clients out of both Environment and Harness continuation data.