Metadata-Version: 2.4
Name: agent-framework-durabletask
Version: 1.0.0b261029
Summary: Durable Task integration for Microsoft Agent Framework.
Author-email: Microsoft <af-support@microsoft.com>
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Classifier: License :: OSI Approved :: MIT License
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Typing :: Typed
License-File: LICENSE
Requires-Dist: agent-framework-core>=1.19.0,<2
Requires-Dist: durabletask>=1.11.0,<2
Requires-Dist: durabletask-azuremanaged>=1.4.0,<2
Requires-Dist: opentelemetry-api>=1.39.0,<2
Requires-Dist: pydantic>=2.11,<3
Requires-Dist: python-dateutil>=2.8.0,<3
Requires-Dist: durabletask[azure-blob-payloads]>=1.11.0,<2 ; extra == "azure-blob-payloads"
Requires-Dist: aiohttp>=3.13.4,<4 ; extra == "azure-blob-payloads"
Project-URL: homepage, https://aka.ms/agent-framework
Project-URL: issues, https://github.com/microsoft/agent-framework-durable-extension/issues
Project-URL: release_notes, https://github.com/microsoft/agent-framework-durable-extension/releases
Project-URL: source, https://github.com/microsoft/agent-framework-durable-extension/tree/main/python
Provides-Extra: azure-blob-payloads

# Get Started with Microsoft Agent Framework Durable Task

[![PyPI](https://img.shields.io/pypi/v/agent-framework-durabletask)](https://pypi.org/project/agent-framework-durabletask/)

Please install this package via pip:

```bash
pip install agent-framework-durabletask --pre
```

## Optional Blob Payload Offloading

This extra is part of the current unreleased stack. The matching package release must
include it before using the installation command against PyPI.
Install the optional storage dependencies through the shared extra:

```bash
pip install "agent-framework-durabletask[azure-blob-payloads]" --pre
```

The Functions package's `azure-blob-payloads` extra forwards to this extra. The dependency
list has one owner; installation alone does not enable offloading. The shared extra uses the
same `durabletask>=1.11.0,<2` baseline as both hosts, including non-blocking asynchronous Blob
compression. Keep SDK versions locked across clients and workers. When upgrading from older
SDKs, finish affected nested-race runs on pinned old workers or use a separate hub for new
instances. Already-stuck runs may require deliberate recovery. Existing migration and
fresh-instance requirements still apply; see the [SDK upgrade guidance](#upgrading-the-durable-task-sdk).

Configure a compatible `payload_store` on each SDK worker and client before wrapping them:

```python
import os

from durabletask.client import TaskHubGrpcClient
from durabletask.extensions.azure_blob_payloads import BlobPayloadStore, BlobPayloadStoreOptions
from durabletask.worker import TaskHubGrpcWorker

from agent_framework_durabletask import DurableAIAgentClient, DurableAIAgentWorker

store = BlobPayloadStore(
  BlobPayloadStoreOptions(
    connection_string=os.environ["PAYLOAD_STORAGE_CONNECTION_STRING"],
    container_name="durable-payloads",
    threshold_bytes=256 * 1024,
  )
)
worker = TaskHubGrpcWorker(host_address=os.environ["DURABLE_TASK_ENDPOINT"], payload_store=store)
client = TaskHubGrpcClient(host_address=os.environ["DURABLE_TASK_ENDPOINT"], payload_store=store)
agent_worker = DurableAIAgentWorker(worker, deployment_mode="isolated_v2")
agent_client = DurableAIAgentClient(client)
```

`DurableTaskSchedulerWorker` and `DurableTaskSchedulerClient` accept the same `payload_store`
argument alongside their DTS endpoint, task hub and credential settings. The MAF wrappers retain
it. Separate processes can use different store objects but must agree on the backing storage and
reference format. Keep each store open while its worker or client uses it. Blob offloading does
not require Functions. Functions configures its root app instead of constructing these objects;
see the [Functions setup](../azurefunctions/README.md#optional-blob-payload-offloading).

The configured credentials need permission to create the upload container and to read
and write payload blobs. The SDK attempts container creation on the first upload, even
if the container already exists. Pre-provisioning it does not bypass this request.

Supported serialized payloads above the threshold, including entity state and operation results,
are stored in blobs and hydrated by configured SDK clients/workers. The default stored-payload
limit is 10 MiB and is configurable. Offloading does not reduce decoded state or model context,
and does not change MAF state budgets or retention. Reset, response expiry, entity deletion and
orchestration purge do not clean up SDK payload blobs. Retain blobs while referenced state/history
may be read or replayed, including across deployments, and manage lifecycle separately.

Store-recognized whole strings are reserved transport references rather than ordinary data.
Wrap literal references in an object and retain that wrapper across durable boundaries.
References do not authorize reads; the upload container does not constrain which containers
credentials can read. Use least-privilege credentials and validate or reject references from
untrusted callers before durable API calls. Blob transport failures and missing references can
prevent execution or reads; storage retries are not a guarantee of durable recovery.

## Durable Task Integration

The durable task integration lets you host Microsoft Agent Framework agents using the [Durable Task](https://github.com/microsoft/durabletask-python) framework so they can persist state, replay conversation history, and recover from failures automatically.

### Upgrading the Durable Task SDK

Both Python hosts require `durabletask>=1.11.0,<2`; standalone still supports Python 3.10+.
SDK 1.11 fixes nested races such as `when_any([cancel, when_all(tasks)])`. Replaying affected
older histories can now select a different branch and fail with `NonDeterminismError`.
Finish affected runs on the previous SDK, or keep their hub on pinned old workers and start
new instances in a separate hub. Already-stuck runs need deliberate recovery and may not drain.
Do not mix old and new workers on the same hub to achieve isolation. The framework's fresh
instance and isolated-deployment requirements below still apply.
See the [SDK release warning](https://github.com/microsoft/durabletask-python/releases/tag/v1.11.0).

Coordinate SDK locks across clients and workers. A minimum version does not enforce identical
resolved versions. Failure type names are now qualified, for example `builtins.ValueError`.
Use `FailureDetails.is_caused_by(ValueError)` to also recognize older recorded bare names.

<a id="current-runtime-contract-on-this-branch"></a>

### Current Runtime Contract On This Unreleased Stack

This README describes the current unreleased schema-v2 runtime, not a released compatibility
promise. It is the common runtime contract for both Python hosts. See the
[Azure Functions guide](../azurefunctions/README.md) for host-specific routes and settings.

The public mutable state model is now canonical `DurableAgentState` schema `2.0.0`.
`read_agent_state(raw)` remains explicitly backward compatible with legacy `1.x` payloads.
It accepts a dictionary or JSON string and returns:

- `LegacyDurableAgentState` for exact `1.0.0`, `1.1.0`, and `1.2.0` inputs
- `SharedAgentStateReader` for exact `2.0.0` inputs

Other schema versions are rejected. Within an accepted `2.0.0` snapshot, unknown fields and
unknown protocol content remain preserved in detached raw JSON until a caller explicitly asks for
a narrower projection.

Named shared counters are bounded to signed Int64. Usage token counts and ingested
positions accept `0..9223372036854775807`. A present `evictedMessageCount` accepts
`1..9223372036854775807`. Readers and writers reject invalid counts, and eviction
increments reject overflow before changing state. These bounds also apply when
loading legacy snapshots. Legacy nullable usage fields remain readable, and nullable
Core usage counts retain their existing metadata representation. Other invalid known
counts cannot be moved into metadata to bypass validation.

Integral JSON numbers such as `1.0` remain accepted. Exact integer tokens are not
rounded through floating point. These bounds do not restrict opaque user payloads,
provider metadata, session data, or unknown fields.

The v2 reader preserves the original JSON, including unknown fields, shared-value metadata,
session state, receipts, and protocol details. `to_dict()` returns detached original JSON.
`try_get_agent_response(correlation_id)` reads canonical results and receipts without writing
back lifecycle changes. If the response is expired or otherwise unavailable, polling consumers
retain the known completion outcome instead of deleting or rewriting the receipt. Durable Task
storage reads use bounded polling retries.

Known response snapshots preserve the explicit shared-value policy. Typed projections require a
present `value` field and a JSON-preserving decode. Consumers do not infer values from text,
coerce missing payloads, or discard unknown structured fields. The Core transport snapshot carries
the versioned `_durable_value_policy` marker so the matching loader can preserve the same rules
after JSON transport. It does not prove execution authority, duplicate the value, or identify a
runtime-native class by itself.

Host construction requires an explicit deployment strategy. The opt-in
[two-phase entity migration path](ENTITY_MIGRATION.md) uses versioned names and DTS
backend filtering, then permits in-place historical upgrades after old workers stop.
For an isolated deployment, pass
`deployment_mode="isolated_v2"` or set `DURABLE_AGENTS_DEPLOYMENT_MODE=isolated_v2`.
There is no automatic probe. This is an operator
acknowledgement that the task hub is isolated and the clients are upgraded. It is not runtime
proof of isolation and it cannot detect peer workers. A new isolated hub with matching upgraded
workers and clients is the recommended way to keep old workers and histories off this deployment.

**The following inherited replay limitations apply to affected older workflow builds, not to entity address upgrades.**
Workflow protocol `2` is unchanged, but start admission is stricter and checkpointed HITL admission
and mixed parent/child scheduling change the replay action graph, including generated child IDs.
Existing v2 in-flight instances and recorded histories are not supported by this runtime and may
fail. The version marker checks start-envelope admission, not feature or replay compatibility.
Older v2 envelopes can still pass that check. There is no replay migration or compatibility fallback.
If old runs must finish, keep them on their original workers and hub, including published histories
with concatenated child IDs. Do not resume them here.

#### Migration

For on-demand upgrades at an existing address, see [in-place entity migration](ENTITY_MIGRATION.md).
The export/import API below remains a separate explicit transfer operation, not the
two-phase rollout path.

The current layer includes canonical transaction state, session capture, the workflow
protocol boundary, and a privileged manual migration path. Migration is a backend entity
operation only. There is no generated HTTP or MCP migration endpoint. The helper stages a
detached result with no model, tool, provider, storage, or transcript I/O. The operator must
quiesce the legacy owner, fence it, authorize ownership transfer, and commit the staged result
exactly once into a separate empty isolated v2 destination. The migration evidence cannot prove
that authority or completeness on its own.

The request contract is explicit and complete. `migrate(request)` requires `source`,
`sourceDigest`, `sourceSessionId`, `destinationSessionId`, `migrationId`, and
`ownershipTransferId`. It optionally accepts `deliveryEvidence`, `completionEvidence`, and
`requireKnownOutcomes`. The destination must match the current entity, must be separately
addressed, and must still be empty. This is never an in-place rewrite and never a replay into a
used destination. The same exact request may be retried idempotently. The runtime records a
`requestDigest` and accepts only an identical retry after commit, without rewriting results or
refreshing their grace period. Changed requests are rejected, including after acknowledgement
loss or a later host run. If the state setter raises, only local staged/cache state rolls back.
The backend may have committed, so the next operation reloads authoritative state before proceeding.

`source` accepts only valid published shared legacy `1.0.0`, `1.1.0`, or `1.2.0` state. All
version `2.x` sources are rejected. There is no private prototype v2 converter. The migration
path does not claim that nullable legacy fields are normalized, that lossy legacy data becomes
canonical automatically, or that unversioned private shapes are accepted. An existing
`source.data.migration` field is rejected, never overwritten. Any `source.data.pythonIngestion`
field is rejected before legacy parsing, including null, empty, foreign and recognized profiles.
It is neither relocated nor carried into v2 where it could become active bookkeeping. Other
unknown application fields remain at their original locations.

Nonempty legacy `ingestedPositions` requires `deliveryEvidence`. When supplied, this evidence
must contain `sourceDigest` matching the source snapshot, a nonblank `evidenceId`, `complete=true`,
and canonical `messages` from `Message.to_dict()`. It must cover all accepted inputs, including
evicted inputs and every accepted revision. The only additional field is `messagePositions`,
required when legacy `ingestedPositions` is nonempty and optional otherwise. When present, it must
align one-for-one with `messages`. Each entry is `null` for an unpositioned input or exactly
`producer` (a nonblank string) and `position` (a nonnegative integer, not a boolean). Attributed
producers and their maximum positions must match `ingestedPositions`. This checks consistency,
not completeness. Positions are never inferred from public message IDs or naming heuristics.

A complete, independent `completionEvidence` journal is required for any retained history,
a `truncation` field, nonempty session state or `ingestedPositions`, or nonempty supplied delivery
`messages`. An empty delivery journal alone does not require it. The journal must contain exactly
`sourceDigest` matching the source snapshot, a nonblank `evidenceId`, `complete=true`, and `results`,
including pruned original terminal results. Each result needs `correlationId`, the original
`outcome`, `completedAt`, and `response` with `messages`, plus `error` for failures.
`resultExpiresAt` is forbidden. An empty `results` list asserts there were no completions and is
valid only without retained responses. `requireKnownOutcomes=false` cannot waive these requirements.
Transcript fragments, accepted inputs, and later acknowledgements do not establish original outcomes.
Imported results receive a new bounded delivery grace period from the migration clock plus the
configured delivery window. Their original `completedAt` values remain unchanged, and identical
retries never extend the deadline.

Full request example for a fresh source. Before submitting, replace the `sourceDigest` placeholder
with `agent_framework_durabletask.state_snapshot_digest(source)` computed from the unmodified
`source` object.

```json
{
  "source": {
    "schemaVersion": "1.1.0",
    "data": {
      "conversationHistory": []
    }
  },
  "sourceDigest": "<computed source digest>",
  "sourceSessionId": "legacy-provider:source-session",
  "destinationSessionId": "@agent@dest-session",
  "migrationId": "migration-1",
  "ownershipTransferId": "transfer-1"
}
```

The optional migration helper budget `max_state_bytes` is a neutral admission limit, not a
pressure policy. If the fully staged destination exceeds that bound, migration fails with
`StateCapacityError`. It does not prune transcript, receipts, session state, or unknown JSON to
fit. Runtime transcript retention is configured separately below.

#### Session Restoration

If the legacy source carries external session state, its original `session_id` is preserved and
used by later host runs only after the migration is committed into the destination. Migration is
the boundary that transfers that destination binding. A nonblank saved `session_id` must match
`sourceSessionId`. An absent, null or blank saved ID is filled from `sourceSessionId`.
Within a session object, `state` must be an object when present. Only omission defaults to `{}`,
not null, a list or a scalar. `service_session_id` may be absent, null, any string or a JSON object,
matching Core 1.13 and 1.16's `str | ServiceSessionId | None` contract, where `ServiceSessionId`
is `Mapping[str, Any]`. Empty strings and objects are accepted. Object keys must be strings, but
no particular keys are required and values may be any strict JSON, including unknown metadata.
Service IDs, state values (including registered-type JSON), and unknown session siblings are
preserved without trimming, Core session deserialization, provider imports or provider calls
during migration. Cold session restoration preserves structured IDs. Continuation still requires
a compatible agent/provider, since Core's generic chat `Agent` requires a string service ID.

#### Workflow Starts and Child Identity

Workflow start input wrapping is not universal. `DurableWorkflowClient` and generated HTTP
start routes wrap new inputs for host-generated workflows. Application-owned native orchestrators
retain their original input contracts. Do not wrap every orchestration payload in this envelope.

Generated root starts take public application JSON, not internal checkpoint data. The workflow
client and generated HTTP routes remove reserved child markers. Application input is sanitized
before typed reconstruction.
At generated workflow entries, internal `__subworkflow_input__` and `__subworkflow_address__`
markers require an actual SDK-reported parent and a child address consistent with both SDK parent
and current instance IDs. Missing or mismatched provenance is rejected before checkpoint decoding.
A native application parent may still call a generated workflow with ordinary application JSON
inside `wrap_workflow_input(...)`. Without internal child markers, that payload remains application
input, even though the SDK reports a parent.

SDK parent metadata authenticates only the immediate parent/child relationship, not the full
ancestry or claimed root's authority. It does not protect against a malicious application parent
supplying an otherwise consistent envelope. Trusted worker and application deployments remain
required. The internal checkpoint codec still uses pickle and is not safe for arbitrary untrusted
input.

Generated child-ID scheme `1` always produces a physical instance ID of exactly 74 ASCII characters,
`dafxsw_v1_` followed by the full 64-character SHA-256 hex digest. The digest covers a domain separator
and length-framed UTF-8 values for the parent's actual instance ID, the exact executor ID and the
dispatch ordinal in decimal. Dispatch and provenance helpers use this same derivation at every hop.
The hash checks address consistency, not authentication.
Bounded physical IDs do not imply unlimited nesting, as logical paths and payloads still grow with
depth and their limits still apply.

Original executor IDs and workflow names, `~`-qualified HITL paths and the root notification address
remain unchanged. Clients follow actual child IDs in the parent's `subworkflows` status map at each
hop. They must not parse physical child IDs or reconstruct addresses from their names.

For a known standalone `DurableTaskSchedulerClient`, an explicit root ID must be nonblank, contain
1-100 printable ASCII characters and not start with `@`. It is validated, never rewritten or hashed. Generic
`TaskHubGrpcClient` behavior is preserved because its backend is unknown. The Functions generic route
validator retains its existing 100-character limit and Unicode-aware rules, not an ASCII-only rule,
because its provider is unknown. Application-owned native orchestrator calls are unchanged.

#### Invocation and State Boundaries

There is no automatic transcript recovery for a rejected service conversation ID. A specifically
rejected parent response can receive up to three additional identical-request retries for a
visibility delay, but only before any output, tool execution or session advance. The saved parent
ID is not cleared.

Streaming fallback requires an immediate capability refusal, not awaited setup, iteration or finalization errors.
Async streaming remains supported. Activity state deltas compare a detached receiving-worker encoding, not producer
pickle bytes. Explicit writes retain their intent, and in-place JSON changes remain type-sensitive.

Requires Python 3.10+, `durabletask>=1.11.0,<2` for scoped JSON decoding, SDK parent metadata
and corrected nested task completion,
`agent-framework-core>=1.19.0,<2` and `pydantic>=2.11,<3` for structured response handling.

#### History Provider Integration

Ownership is resolved per run from its `store` option, then the agent's `default_options`, then
the client's default. On client-owned runs, the active primary's load and storage flags select
history and appends. `skip_excluded=True` omits compaction exclusions from later model context.

Append staging is atomic in memory, undoing lazy internal-ID repairs and restoring the transcript,
working buffers, position indexes and append ordinal if staging fails. Earlier successful saves
and flushes, including the preliminary flush in `save_messages()`, remain intact. This does not
commit to the backend or roll back external provider writes.

Requests and responses are checked as stored transcript JSON before publication. Failed-run
finalization retains pending tool results if filtering or staging fails, allowing a retry.

Core sessions are loaded and staged through a private entity-backed `SessionStore` adapter.
It uses the existing entity session field, not a separate backend or commit. The agent still
creates its working session, and only the durable history adapter's temporary message/index
buffers are omitted from its snapshot. Snapshot copies isolate saved state from failing custom
decoders and retained working-session references. External history providers keep their own transcripts
and provider state. This does not make external writes transactional or migrate history between
providers. The adapter relies on Core's experimental store contract and is not a public storage
configuration API.

The history bridge requires an exact `2.0.0` snapshot. Even `get_messages()` may repair missing
or duplicate internal IDs, so it is not a read-only inspection path. Mutation-capable hooks reject
legacy and unsupported versions before changing stored history or working state. Use
`read_agent_state()` for legacy inspection instead.

External primary providers, service-owned history and store-only audit sinks keep separate roles.
Preparation permits only one `DurableHistoryProvider`, including an injected or replaced primary.
Different source IDs do not create separate durable transcripts. Multiple durable adapters are
rejected before model execution, even when loading or all store flags are disabled, because loading
and flushing can still update shared history. Use an ordinary store-only `HistoryProvider` with a
distinct source ID for an independent audit store.
A single load-disabled durable adapter may audit an external primary into the canonical transcript.
Its stored messages and public IDs do not acknowledge input delivery. Only the active primary's
acceptance evidence contributes ingestion receipts, so an audit cannot suppress a later retry.
On a service-owned turn, the inactive external primary's custom hooks are also suppressed because
they may load or persist history directly. Ownership-independent work belongs in a separate context
provider or store-only sink. Client-owned turns retain the original primary's hooks and resources.
External-primary observation uses its current storage flags without rebinding custom hooks.
Compaction aimed at an inactive external history source does not run its stored-history after
hook. Its before hook still operates on unrelated current context, as Core specifies.
Ordinary input and response IDs are preserved. Newly generated compaction summary occurrences get
unique IDs when a strategy reuses a candidate ID, so both summary revisions and their links survive.
Reconciliation distinguishes loaded occurrence IDs from unallocated summary IDs and compares
summary revisions with JSON-exact payloads.
With no primary, history is injected before a matching before-compaction provider. Core runs before
hooks forward and after hooks in reverse, so that provider's after hook sees the previously stored
history. After-only compaction retains the existing append-then-compact order. Per-service-call
history uses Core's middleware cadence and does not run compaction hooks per model call.
Injection honors a single configured compaction history source. Conflicting sources require an
explicit primary instead of silently selecting a source that cannot serve all configured hooks.
All explicit primary providers retain the user's hook order, including a built-in in-memory provider
replaced with durable history. With `[compaction, history]`, before compaction cannot see history
loaded later, while reverse after hooks append the current input and output before compaction.
With `[history, compaction]`, before compaction sees loaded history, while the ordinary per-run
after compaction runs before the current turn is appended. Choose the order explicitly for the
strategy's intended inputs. Preparation does not mutate the caller's agent or provider list.
Provider namespaces must be unique, including store-only sinks, and conflicts with an injected
history source raise an error rather than automatically reassigning a namespace.

Canonical media keeps its declared content kind rather than inferring it from the URI scheme.
Literal Core input preserves absent versus explicit-null function results and error details on
the first canonical write and later cold reads. Plain Core objects without an attached input
envelope keep canonical nullable defaults. Existing shared JSON retains its raw field presence.
New response timestamps without an offset are interpreted as UTC. Valid stored timestamps retain
their original offset and fractional precision.

### Retention and State Budgets

The retention default is `retention="keep_all"`. When wrapping `DurableTaskSchedulerWorker`,
an omitted `max_state_bytes` now enables its known 1 MiB pressure budget. Old eligible transcript
groups are removed only under pressure, not merely because model compaction excluded them.
Pass `max_state_bytes=None` to preserve the previous disabled-budget behavior. Generic workers
and Azure Functions remain disabled by default and require an explicit budget.

This default change can delete stored history in existing standalone DTS applications that omit
the budget. Eager pruning remains opt-in and independent of pressure eviction.

- `retention="follow_compaction"` physically removes eligible compaction exclusions when durable
  history is flushed, even without a pressure budget. It does not configure a compaction strategy.
- A positive integer `max_state_bytes` enables pressure eviction, including with `keep_all`.
  At `high_watermark=0.85`, the runtime plans removal of oldest eligible atomic message groups
  toward `low_watermark=0.70`, or the protected floor if it is higher but still below the high
  watermark. Watermarks must be finite and satisfy `0 < low_watermark < high_watermark <= 1`.
- Explicit `max_state_bytes=None` disables pressure eviction, not eager pruning. Booleans, zero and
  negative budgets are rejected.

The estimate covers the whole entity using Python's default JSON serialization with ASCII
escaping, including non-text content, delivery records, session state, metadata and truncation
records. It excludes transport framing and is not a backend acceptance guarantee.
`max_state_bytes="backend_limit"` is a 1 MiB (`1_048_576` bytes) convenience only when the wrapped
worker is a `DurableTaskSchedulerWorker`. A generic `TaskHubGrpcWorker` cannot resolve it.
`AgentFunctionApp` rejects it, even when Functions uses DTS. Use an explicit positive budget or
`None` there.

The local emulator tests compare actual persisted JSON bytes with the estimate, exercise
near-limit default/opt-out behavior, and verify incoming entity-operation payload rejection.
The tested emulator did not reject oversized entity state with budgeting disabled. These tests
do not establish Azure's hard state-rejection boundary, and a state budget cannot prevent an
oversized input or output from exceeding its separate transport limit.

Pressure eviction leaves terminal results, completion and ingestion receipts, session/control
state, opaque unknown entries and retained entry metadata intact. System messages and the latest
exchange are protected, and linked tool, reasoning and persisted atomic groups are not split.
Eager pruning also protects pending tool calls and groups with any included member. These
protections can prevent the requested reduction. A protected floor at or above the high watermark,
or an unreachable safe target, raises `StateCapacityError` without applying a partial pressure plan.
The enclosing run restores its local staged/cache state on failure. This does not undo model,
tool or external-provider side effects. If a host state write raises, backend acknowledgement is
unknown, not proof of rollback. The next operation reloads authoritative state.

#### Registration and History Ownership

`DurableAIAgentWorker` sets host defaults. At construction, omission or `INHERIT` uses the known
DTS budget when available, otherwise no budget. Explicit `None` always disables pressure eviction.
`add_agent()` and `configure_workflow()` accept
`retention`, `max_state_bytes`, both watermarks and `response_delivery_window_seconds` overrides.
Workflow settings apply to its agent entities and nested workflows, not one aggregate workflow
budget. For budget overrides, omission or `INHERIT` from `agent_framework_durabletask` uses the
host default, while explicit `None` disables pressure eviction. For the other overrides, `None`
inherits. Shared workflow registrations must have matching settings.

`AgentRegistrationSettings` retains its positional delivery-window and callback parameters.
Its added retention and budget fields are keyword-only and default to non-evicting behavior.

A hand-configured `DurableHistoryProvider(prune_excluded=...)` keeps its explicit value, including
`False`, ahead of the inherited eager-pruning policy. This does not disable a pressure budget.
An exact built-in in-memory primary is replaced with durable history while preserving its
`source_id` and storage settings. An external primary, including an in-memory subclass, stays
primary without a second durable primary. More than one load-enabled primary is rejected.
Local retention does not delete external-provider history.

History ownership is resolved per run. A service-owned conversation bypasses durable history
loading and eager compaction flushes. A configured pressure budget still checks local entity
state and can evict eligible older local transcript, but it neither compacts the remote
conversation nor clears the saved service branch ID.

#### Delivery and Maintenance

Canonical `terminalResults` hold immutable original response envelopes by correlation ID,
separately from transcript history. Matching `completionReceipts` retain `correlationId`, known
`outcome` (`succeeded` or `failed`) and `completedAt`. Results include the full `response`, including
messages, metadata and an optional explicit `value`, even null or falsey. Failures require `error.code`
and `error.message`, while successes forbid `error`. `resultState="available"` requires a matching result.
`resultState="unavailable"` forbids a result and requires `resultUnavailableAt`. Completion facts
and any `resultExpiresAt` agree across the pair. See the
[shared wire invariants](../../../schemas/README.md#proposed-wire-concepts-and-semantic-invariants).

Staging rejects unsupported live response fields before Core serialization, without invoking their
conversion hooks. Supported lazy values keep their existing policy. Duplicate completions remain
no-ops without inspecting the replacement producer.

Response availability has a default `response_delivery_window_seconds=60`. Transcript deletion
does not delete the independent result or its completion outcome. Response expiry removes delivery
payloads without deleting completion or ingestion receipts. There is no bounded receipt cleanup,
so receipt growth alone can exhaust a configured budget. Runs perform expiry checks, but idle
entities need application-owned maintenance to invoke `expire_responses()`. Polling reads do not
persist cleanup. `reset` clears local transcript and session state while preserving completion
receipts and live results. External-primary reset is rejected without a provider-owned clear
operation.

Generated agent entities on both Python hosts support only `run`, `reset`, `expire_responses`,
`migrate` and `delete`. Raw state setters and implementation helpers are not entity operations.
The deprecated `run_agent` operation has been removed; direct callers must use `run`.

`reset` returns no value. `expire_responses` returns the integer number of expired delivery
payloads removed by that execution, or zero for a successful no-op. Failures use the SDK operation
failure channel. A completed call acknowledges execution; a signal acknowledges only enqueueing.
The expiry count is not an idempotent receipt: an execution can remove payloads and lose its reply,
then a retry can return zero. Both hosts use these native results rather than host-specific wrappers.

`delete` is a privileged administrative operation that removes the entire entity, including
conversation state, live results, completion receipts and ingestion evidence. It is not a reset
or expiry operation. Reusing that entity key no longer benefits from its previous duplicate-run
protection, and queued operations can recreate it. Operators must quiesce and fence callers when
deletion must be final. Generated HTTP and MCP routes do not expose these maintenance operations.

At or after a delivery deadline, lookup reports completion with its retained outcome, even before
physical cleanup. It never returns the expired payload, reports pending work, reruns the request or
reconstructs an original from compacted or external history. Shared v2 has no `unknown` outcome.

### Retention Metrics

The `agent_framework.durabletask` meter emits `durable.retention.*` metrics for evaluations,
resolved budgets, before/after JSON size, staged message and entry removals, reclaimed bytes,
capacity failures, write attempts and observed run operations. Labels are bounded categories,
not agent, session, correlation or message identifiers, content or exception text.

Deletion measurements describe staged local state with `commit_status="not_attempted"`.
Write observations distinguish `stage="serialization"` from `stage="set_state"`, each with
`outcome="returned"` or `"failed"`. Successful serialization and transition validation record
`"returned"` with `commit_status="not_attempted"` before the host call. A serialization failure
records only that stage. After a `set_state` attempt, write and operation observations use
`commit_status="unknown"` even when the call returns. Neither status confirms a durable commit,
and staged deletions may later roll back locally.

Instrumentation uses only the OpenTelemetry API. The package does not configure an SDK, metric
reader or exporter. Applications own that setup, and telemetry failures do not replace the
operation's result or error.

### Workflow HITL and Mixed Parent/Child Execution

Non-agent `request_info` replies are reconstructed and validated inside the registered response
activity against the request's recorded type. The activity result checkpoints `accepted` or
`invalidreply`, so orchestration replay consumes that outcome instead of rerunning reply validators.
Rejected replies leave the request pending for a correction. Handler and output-serialization
failures remain activity failures, not invalid replies. Activity retries or redelivery can still
rerun application code.

Response-type descriptors retain declared types and generic arguments for supported `list`, `dict`,
`tuple`, `set`, union and `Any` annotations. Coercion and type checks follow the installed Core
version, so nested model/dataclass and tuple/set reconstruction from JSON is Core-dependent.
Custom types must already be loaded under their recorded module and qualified name. This does not
make every Python annotation a supported reply type.
Tuple descriptors preserve the declared argument shape, including Python 3.10's explicit empty-tuple
argument. They do not assume all tuple aliases admit the same JSON or native tuple values.

Valid replies for known fixed request IDs can arrive before their waits are published and remain
buffered by the service. Sending a reply acknowledges event delivery, not type validation or handler
success. Nested replies still require an active child path recorded by the parent. Pending event
waits survive other replies and mixed waves, including valid replies already buffered for them.

Parent and child HITL requests can remain active together. Ready parent handlers and downstream
work proceed without waiting for paused children. A ready child's downstream work can answer an
earlier paused child. Within a ready wave, results route in dispatch order and preserve each
result's message order. Local tasks read the dispatched state snapshot, and their reported updates
and deletes merge in dispatch order before downstream work or reply handlers run. Across waves,
recorded readiness defines order, not original child invocation order. This preserves durable
snapshots, not Core's in-process visibility of other executors' uncommitted writes.

### JSON runtime boundary

The current runtime writes canonical `2.0.0` state and supports the explicit migration above.
Construct `DurableAIAgentWorker` before starting the SDK worker. Generated entity state and
`run`/`migrate` inputs, blocking framework agent results, generated workflow starts, child results
and external-event values use plain JSON rather than SDK custom-object reconstruction.
Native co-hosted work keeps its original converter behavior. The
[integrated JSON boundary guide](../../../docs/features/python-durable-json-boundaries.md)
details both hosts, custom-converter constraints and separate checkpoint trust requirements.

### Basic Usage Example

```python
from agent_framework import Agent
from agent_framework.openai import OpenAIChatCompletionClient
from agent_framework_durabletask import DurableAIAgentWorker
from durabletask.worker import TaskHubGrpcWorker

# Create the worker
worker = TaskHubGrpcWorker(host_address="localhost:4001")

# Only use isolated_v2 for a new isolated hub with upgraded clients.
# Do not point this host at an existing hub and replay historical runs after updating.
agent_worker = DurableAIAgentWorker(worker, deployment_mode="isolated_v2")

chat_client = OpenAIChatCompletionClient()
my_agent = Agent(client=chat_client, name="assistant")
agent_worker.add_agent(my_agent)
```

For more details, review the standalone [Durable Task samples](https://github.com/microsoft/agent-framework-durable-extension/tree/main/python/samples) and the full [Agent Framework Python documentation](https://github.com/microsoft/agent-framework/tree/main/python).

