Skip to content

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

Merged
vineetvora-scale merged 4 commits into
mainfrom
vineetvora/task-state-storage-phase-seam
Aug 4, 2026
Merged

feat(states): add storage-phase config and task-state repository selector#380
vineetvora-scale merged 4 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

The PR adds validated storage-phase configuration and routes task-state repository construction through a shared selector.

  • Defaults task-state and task-message storage to MongoDB.
  • Rejects unsupported PostgreSQL phases during process startup.
  • Defines the task-state repository protocol used by API and worker consumers.
  • Routes FastAPI dependency injection and both Temporal factories through the selector.
  • Passes the storage-phase variables into API and worker containers.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the unsupported PostgreSQL phase is now rejected during API and Temporal process startup before repository wiring or request handling.

Important Files Changed

Filename Overview
agentex/src/config/environment_variables.py Adds typed storage phases, MongoDB requirement derivation, and startup validation that rejects currently unsupported backends.
agentex/src/domain/repositories/task_state_repository.py Defines the task-state repository protocol and introduces the centralized phase-based repository selector.
agentex/src/temporal/scheduled_agent_run_factory.py Routes scheduled-run task-state repository construction through the shared selector.
agentex/src/temporal/task_retention_factory.py Routes retention task-state repository construction through the shared selector.
agentex/docker-compose.yml Passes both storage-phase settings to the API and Temporal worker with MongoDB defaults.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    E[Process startup] --> R[EnvironmentVariables.refresh]
    R --> V{Storage phase supported?}
    V -->|No| F[Fail startup with named configuration error]
    V -->|Yes: mongodb| S[get_task_state_repository]
    S --> M[TaskStateRepository using MongoDB]
    M --> A[FastAPI DI consumers]
    M --> T[Temporal factories]
Loading

Reviews (6): Last reviewed commit: "refactor(states): make the Mongo reposit..." | Re-trigger Greptile

@vineetvora-scale
vineetvora-scale force-pushed the vineetvora/task-state-storage-phase-seam branch 3 times, most recently 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
PROD = "production"


class StoragePhase(str, Enum):

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.

why not just enum? Why is string needed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Matches the other enums in this file (Environment, EnvVarKeys). Existing code compares raw env strings to those members directly, which only works when members are strings. Also keeps it log/JSON-friendly without .value everywhere. Pydantic would parse either way, so it's more for consistency

# Resolved through the shared selector (not constructed by hand) so the
# worker honors TASK_STATE_STORAGE_PHASE the same way the API does.
task_state_repository = get_task_state_repository()
task_message_service = TaskMessageService(

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.

why not do the same for messages too since youre introducing that env var in this pr?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The env var and the selector are separate pieces here. The selector only exists where there are two backends to select between. Messages get theirs in M2 (Milestone 2), alongside the Postgres message repo, same as state got its selector in this PR.

The env var can't wait for M2 though: mongodb_required is derived from both phases, so both settings need to exist for it to have its final shape. Until M2 consumes it, startup validation keeps it locked to mongodb.

"""
phase = EnvironmentVariables.refresh().TASK_STATE_STORAGE_PHASE
if phase == StoragePhase.MONGODB:
return TaskStateRepository(GlobalDependencies().mongodb_database)

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.

mongo db class should implement TaskStateRepositoryProtocol no? To keep the two coupled and hold the repository up to protocol standard (no extra methods are added that diverge from mongo vs postgres repository)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right, I just added it. The repo now declares the Protocol as a base, and a test enforces that every protocol method has a real implementation, so any drift fails CI at the class instead of at some call site. The follow-up's Postgres repo gets the same base and test, plus the shared test suite running against both backends, which is what keeps the two aligned.

@vineetvora-scale
vineetvora-scale force-pushed the vineetvora/task-state-storage-phase-seam branch from 05a801e to 7aa6ffe Compare August 3, 2026 20:42
…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.
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().
…Protocol

Declares TaskStateRepositoryProtocol as a base of TaskStateRepository so
conformance is visible at the class and checkable by IDEs. Because an
explicit subclass silently inherits the protocol's placeholder methods,
a test pins that every required method is a real implementation.
@vineetvora-scale
vineetvora-scale force-pushed the vineetvora/task-state-storage-phase-seam branch from 7aa6ffe to 07433d0 Compare August 4, 2026 15:02
@vineetvora-scale
vineetvora-scale merged commit 354f35e into main Aug 4, 2026
46 checks passed
@vineetvora-scale
vineetvora-scale deleted the vineetvora/task-state-storage-phase-seam branch August 4, 2026 15:11
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.

2 participants