Skip to content

Use task state store for common.ai durable execution on Airflow 3.3+#68926

Merged
kaxil merged 5 commits into
apache:mainfrom
astronomer:common-ai-durable-task-state-store
Jul 8, 2026
Merged

Use task state store for common.ai durable execution on Airflow 3.3+#68926
kaxil merged 5 commits into
apache:mainfrom
astronomer:common-ai-durable-task-state-store

Conversation

@kaxil

@kaxil kaxil commented Jun 24, 2026

Copy link
Copy Markdown
Member

durable=True on AgentOperator / @task.agent caches each model response and tool
result so a retry replays completed steps instead of re-running expensive LLM calls.
Until now that cache was always an ObjectStorage JSON file, which required
[common.ai] durable_cache_path to be set.

The durable cache is per-task-instance, per-run, and cleared on success, which is exactly
the scope of the AIP-103 task state store.
On Airflow >= 3.3 the cache now lives there:

  • No durable_cache_path configuration needed.
  • Large step payloads offload automatically when [workers] state_store_backend is set.
  • Cached steps are deleted on success, and reclaimed by the DAG-run cascade if a run fails permanently.

On Airflow < 3.3 the ObjectStorage backend is unchanged and selected automatically.

Design

  • Both backends implement a shared DurableStorageProtocol, so CachingModel and
    CachingToolset depend on the interface, not a concrete backend.
  • AgentOperator._build_durable_storage picks the backend by Airflow version.
  • Durable keys use a reserved prefix (__commonai_durable__...) so they cannot collide
    with keys user code writes via context["task_state_store"] in the same task instance.
  • Entries are written with NEVER_EXPIRE so a retry can replay them regardless of
    retry_delay or the global retention config; cleanup() removes only the keys this run touched.

Gotchas

  • On a default 3.3+ install (no state_store_backend), cached step values are stored in
    the metadata database. For agents with large responses, configure
    [workers] state_store_backend to offload them to external storage.
  • A tool passed via agent_params={"tools": [...]} is not wrapped by the caching toolset
    (only toolsets= are), so only its model steps replay. This is existing behavior, unchanged here.

Verification

Verified end-to-end on a real Airflow 3.3 standalone: a durable AgentOperator whose tool
fails on the first attempt replays the cached model step from the task state store on the
retry, and a direct round-trip task confirms cache, replay, and cleanup against the live
Execution API and metadata DB.

ui_02_durable_agent_logs ui_01_grid

@kaxil
kaxil requested a review from gopidesupavan as a code owner June 24, 2026 00:40
@kaxil
kaxil requested a review from amoghrajesh June 24, 2026 00:41
@kaxil kaxil changed the title Use task state store for common.ai durable execution on Airflow 3.3+ Use task state store for common.ai durable execution on Airflow 3.3+ Jun 24, 2026

@amoghrajesh amoghrajesh left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

Comment thread providers/common/ai/src/airflow/providers/common/ai/durable/task_state_store.py Outdated
Comment thread providers/common/ai/tests/unit/common/ai/operators/test_agent.py Outdated
Comment thread providers/common/ai/tests/unit/common/ai/operators/test_agent.py Outdated
Comment thread providers/common/ai/tests/unit/common/ai/operators/test_agent.py Outdated
Comment thread providers/common/ai/tests/unit/common/ai/durable/test_base.py
@kaxil
kaxil marked this pull request as draft June 24, 2026 13:04
kaxil added 4 commits July 7, 2026 10:34
durable=True caching is per-task-instance, per-run, and cleared on success --
exactly the scope of the AIP-103 task state store. On Airflow >= 3.3 the cache
now lives there instead of an ObjectStorage JSON file, so durable execution no
longer needs the [common.ai] durable_cache_path config, and large step payloads
are offloaded automatically through the configured worker state store backend.

The ObjectStorage backend is kept as the fallback for Airflow < 3.3, selected at
runtime by the operator. Both backends share one storage interface, so the
caching model/toolset wrappers are unchanged.
…pelling

Three CI failures on this branch, all in the durable execution code:

- Compat 3.0.6/3.1.8/3.2.2: ``test_task_state_store`` used
  ``pytest.importorskip("airflow.sdk.execution_time.context")`` to skip on
  pre-3.3 cores, but that module exists on older cores -- only the
  ``NEVER_EXPIRE`` name it pulls in (via ``task_state_store``) is 3.3+. Collection
  blew up before the skip could fire. Gate on ``AIRFLOW_V_3_3_PLUS`` instead,
  matching how ``AgentOperator._build_durable_storage`` guards the import.

- Non-DB / Low-dep core: ``test_providers_modules_should_have_tests`` flagged the
  new ``durable/base.py`` as having no test. Add ``test_base.py`` covering the
  reserved-key invariants and the ``DurableStorageProtocol`` runtime contract.

- Docs spellcheck: "sizeable" -> "sizable" in the agent operator docs.
- task_state_store docstring: "older cores" -> "older airflow versions".
- test_agent: skip the task-state-store build test on < 3.3 instead of
  patching AIRFLOW_V_3_3_PLUS -- the inline 3.3-only import would otherwise
  fail on the compat runners once collection is fixed.
- test_agent: give the accessor a TaskStateStoreAccessor spec and the task
  instance an attribute spec; hoist the version-agnostic DurableStorage import.
- test_base: assert the two shipped backends (ObjectStorage always, task
  state store on 3.3+) satisfy DurableStorageProtocol, to catch method drift.
- save_tool_result: normalize results through JSON before writing. The store
  validates against pydantic JsonValue (stricter than json.dumps -- rejects
  tuples and non-string dict keys), so a tool returning `(value, meta)` or an
  int-keyed dict previously crashed a step that already succeeded. Normalizing
  (tuple->list, non-str keys->str) matches the <3.3 ObjectStorage backend and
  keeps the step durable.
- save_model_response / save_tool_result: make the store write best-effort --
  a failed write (e.g. an oversize value on the metadata DB) is skipped with a
  warning instead of failing an already-succeeded step and self-poisoning retry.
- cleanup: log delete failures instead of silently suppressing, so a value
  orphaned in external storage is at least visible.
- test: make FakeTaskStateStore mirror the real contract (reject None, validate
  JsonValue) and add tuple / non-string-key regression tests it now catches.
- test_base: trim to the real-backend protocol-conformance tests + key-prefix
  invariant; drop the tests that only exercised typing.Protocol machinery.
- docs: note durable keys carry the reserved DURABLE_KEY_PREFIX.
@kaxil
kaxil force-pushed the common-ai-durable-task-state-store branch from c3f52b0 to db57c02 Compare July 7, 2026 05:27
@kaxil
kaxil marked this pull request as ready for review July 7, 2026 05:29

@gopidesupavan gopidesupavan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice :)

@kaxil
kaxil merged commit 2e56ce0 into apache:main Jul 8, 2026
84 checks passed
@kaxil
kaxil deleted the common-ai-durable-task-state-store branch July 8, 2026 09:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants