Skip to content

feat(states): add storage-phase config and task-state repository selector - #380

Open
vineetvora-scale wants to merge 2 commits into
mainfrom
vineetvora/task-state-storage-phase-seam
Open

feat(states): add storage-phase config and task-state repository selector#380
vineetvora-scale wants to merge 2 commits into
mainfrom
vineetvora/task-state-storage-phase-seam

Conversation

@vineetvora-scale

@vineetvora-scale vineetvora-scale commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

First PR in a short series adding an optional PostgreSQL backend for task state storage, selected per deployment by a config flag. This PR lands the configuration and the selection seam only — no behavior change: the default phase is mongodb, and existing deployments continue to run exactly as before. Data-migration phases are out of scope for this series; if that effort happens later, it adds its own phase values (additive, not breaking).

What's in this PR

1. Storage-phase settings (src/config/environment_variables.py)

  • TASK_STATE_STORAGE_PHASE and TASK_MESSAGE_STORAGE_PHASE: mongodb | postgres, default mongodb.
  • Values are validated against a StoragePhase enum — a typo fails at startup instead of silently routing to a backend the operator didn't choose.
  • Phases that parse but have no repository yet (postgres, until it lands in a follow-up) are rejected at config refresh — a misconfigured deployment fails at process startup with the variable named, instead of request-time 500s or a Temporal worker crash mid-wiring.
  • Derived mongodb_required property (true unless every store's phase is postgres) for later use in conditional Mongo startup/readiness.

2. TaskStateRepositoryProtocol (src/domain/repositories/task_state_repository.py)

  • Structural interface capturing everything callers actually invoke through the DTaskStateRepository seam — the states use case and authorization shortcuts (create/get/update/delete/list), the retention service (find_by_field/delete_by_field/batch_create), and get_by_task_and_agent.
  • The docstring records the behavioral contract a backend must honor (string ids, caller-supplied timestamp passthrough, ItemDoesNotExist/DuplicateItemError, the Mongo-shaped $in list filter). The MongoDB repository satisfies it unchanged.

3. Phase selector wired into the seam

  • get_task_state_repository() resolves the repository for the configured phase; DTaskStateRepository now points at it, so every DI consumer follows automatically.
  • Construction is lazy per branch: only the mongodb branch touches a Mongo handle, which is what will allow running without a MongoDB connection once no store needs one.
  • The selector's NotImplementedError branch remains as defense in depth; startup validation (above) makes it unreachable through env config.

4. Temporal factories routed through the selector (src/temporal/task_retention_factory.py, src/temporal/scheduled_agent_run_factory.py)

  • Both previously constructed TaskStateRepository(mongodb_database) by hand, bypassing DI. Without this change, a postgres-phase deployment's workers would silently keep using MongoDB while the API used Postgres.

Plus docker-compose pass-throughs for both variables (defaulting to mongodb) on the API and worker services.

Review focus

  • The Protocol's method set: it's derived from actual call sites, notably the retention service's find_by_field/delete_by_field/batch_create — these are part of the contract the future Postgres repository must implement.
  • The two factory changes are the correctness-critical part of this PR (worker/API consistency).
  • Protocol location (repositories module) and shape (Protocol vs the existing CRUDRepository ABC): the ABC's list() takes no filter/pagination arguments and lacks the task-state-specific methods, and a Protocol requires no change to the MongoDB repository's class hierarchy.

Testing

  • New unit tests: env parsing/validation and the mongodb_required matrix; selector resolution for all phases, including a test pinning that non-mongodb branches never touch Mongo wiring; both Temporal factories resolving through the selector.
  • Full config/selector/factory unit suites pass locally (42 tests). openapi.yaml regenerated with no diff; ruff clean.

Follow-ups (separate PRs)

  1. TaskStateORM + a schema change creating the new (empty) task_states table — no data is moved.
  2. TaskStatePostgresRepository + repository tests parametrized over both backends; selector's postgres branch goes live.
  3. Task messages and conditional Mongo startup later in the series.

Greptile Summary

Adds fail-fast storage-phase configuration while preserving MongoDB as the only currently enabled backend.

  • Defines validated task-state and task-message storage phases plus a derived MongoDB requirement flag.
  • Introduces a shared task-state repository protocol and phase-aware selector.
  • Routes FastAPI dependency injection and Temporal factories through the shared selector.
  • Passes the new configuration into API and worker containers and adds focused unit coverage.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; unsupported PostgreSQL phases are rejected before the API accepts requests or Temporal workers construct repositories, resolving the previously reported deployment-path failure.

Important Files Changed

Filename Overview
agentex/src/config/environment_variables.py Adds typed storage-phase settings, rejects unavailable backends during process startup, and exposes whether MongoDB remains required.
agentex/src/domain/repositories/task_state_repository.py Defines the task-state repository protocol and centralizes backend selection while retaining the existing MongoDB implementation.
agentex/src/temporal/scheduled_agent_run_factory.py Routes scheduled-agent-run task-state access through the shared repository selector.
agentex/src/temporal/task_retention_factory.py Routes retention task-state access through the shared repository selector.
agentex/docker-compose.yml Passes both storage-phase variables to API and worker services with backward-compatible MongoDB defaults.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    E[Process environment] --> R[EnvironmentVariables.refresh]
    R --> V{Configured phases supported?}
    V -- No --> F[Fail during process startup]
    V -- Yes: mongodb --> S[get_task_state_repository]
    S --> M[TaskStateRepository]
    M --> A[FastAPI consumers]
    M --> T[Temporal factories]
Loading

Reviews (2): Last reviewed commit: "fix(states): reject unimplemented storag..." | Re-trigger Greptile

@vineetvora-scale
vineetvora-scale force-pushed the vineetvora/task-state-storage-phase-seam branch 2 times, most recently from 0be2190 to c7ec547 Compare July 30, 2026 17:12
…ctor

Introduce TASK_STATE_STORAGE_PHASE and TASK_MESSAGE_STORAGE_PHASE settings
(mongodb|postgres, default mongodb) with a derived mongodb_required
property, a TaskStateRepositoryProtocol capturing the contract callers
rely on through the DTaskStateRepository seam, and a
get_task_state_repository() selector that resolves the backend by phase.

The DI seam and both Temporal factories (task retention, scheduled agent
runs) now construct the repository through the selector, so a phase switch
applies to API handlers and workers alike. The postgres phase raises
NotImplementedError until the Postgres repository lands, and the mongodb
default keeps existing deployments unchanged.
@vineetvora-scale
vineetvora-scale force-pushed the vineetvora/task-state-storage-phase-seam branch from c7ec547 to d28a45c Compare July 31, 2026 19:19
@vineetvora-scale
vineetvora-scale marked this pull request as ready for review July 31, 2026 19:26
@vineetvora-scale
vineetvora-scale requested a review from a team as a code owner July 31, 2026 19:26
Comment thread agentex/src/domain/repositories/task_state_repository.py
An accepted-but-unimplemented phase previously surfaced only on first
use: request-time 500s from the API and a crash inside Temporal worker
wiring. Fail at process startup instead, with the misconfigured variable
named in the error. The selector keeps its NotImplementedError as
defense in depth for configs constructed outside refresh().
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant