agentic-community / agentic-community/agentic-primitives-gateway

Schema migration framework for the spec store

Abierto
#6 0 comentarios 0 reacciones 0 asignados Ver en GitHub
enhancement
Lenguaje dominante
Python
Estrellas
17
Forks
5
Merge medio
1 d 25 min
PR fusionados (30 d)
7

Descripción

# Schema migration framework for the spec store

## Summary

The `migrate_from_legacy` one-shot path was removed when pre-versioned data
stopped being in the wild. Going forward any change to the on-disk (file) or
on-Redis layout of `_StoreState` / `AgentVersion` / `TeamVersion` / the
identity index / the Redis key scheme will need a proper migration, and
today there's no framework to put one in.

This issue tracks adding that framework before the next schema change lands.

## Baseline

The shape committed as of commit `7a0720f` ("Remove migrate_from_legacy;
treat current schema as v0") is **schema version 0**. Any future change
is a v0→v1 migration, v1→v2, etc.

Affected surfaces:

- `_StoreState` (`base_store.py`) — the in-memory shape every backend
persists.
- `AgentVersion` / `TeamVersion` (`models/agents.py` / `models/teams.py`)
— the embedded records.
- The file layout (`agents.json` / `teams.json`) — a JSON document with
`versions` / `identities` / `proposals`.
- The Redis key layout (`gateway:agents:*`, `gateway:teams:*`).

Anything else (API response shapes, audit event fields, config) is out of
scope for this issue — those should evolve through normal additive
changes.

## Why not just keep adding `migrate_*` methods?

- Every shape change would stack another method on the mixins
(`migrate_from_v0`, `migrate_from_v1`, ...), polluting the persistence
layer with per-revision logic.
- No record of *which* version the current data is at — we'd have to
re-run every migration on every startup and rely on them being
idempotent.
- Multi-replica: lifespan-level migrations race across replicas. Only
idempotent or explicitly-locked migrations are safe.
- Test story: the ad-hoc `migrate_from_legacy` we had worked for v0→v1
because it's idempotent via UUIDv5. Nothing else is that forgiving.

## Proposed design

### 1. Stamp state with a schema version

`_StoreState` gains an integer `schema_version` field. Existing stores
load as version 0.

```python
class _StoreState:
versions: dict[str, dict]
identities: dict[str, dict]
proposals: list[str]
schema_version: int = 0 # NEW
```

The file store serializes it; the Redis store stores it alongside the
existing hashes (e.g. `gateway:agents:schema_version`).

### 2. Numbered migration registry

A `src/agentic_primitives_gateway/agents/migrations/` subpackage with
modules named `_0001_*.py`, `_0002_*.py`, etc. Each exports a single
`MIGRATION` object:

```python
# migrations/_0001_example.py
from agentic_primitives_gateway.agents.migrations import Migration

MIGRATION = Migration(
target_version=1,
entity="agent", # "agent" | "team" | "both"
description="Rename spec.primitives.agents.tools to delegates",
apply=_apply,
)

def _apply(state: _StoreState) -> None:
for version in state.versions.values():
spec = version["spec"]
if "primitives" in spec and "agents" in spec["primitives"]:
tools = spec["primitives"]["agents"].pop("tools", None)
if tools is not None:
spec["primitives"]["agents"]["delegates"] = tools
```

### 3. Run-migrations method on `SpecStore`

```python
async def run_migrations(self) -> None:
state = await self._load_state()
registry = self._load_migration_registry()
pending = [m for m in registry
if m.entity in (self._entity_label, "both")
and m.target_version > state.schema_version]
if not pending:
return
pending.sort(key=lambda m: m.target_version)
for m in pending:
logger.info("Applying migration %s → v%d", m.description, m.target_version)
m.apply(state)
state.schema_version = m.target_version
await self._save_state(state)
```

### 4. Lifespan wiring

`main.py` gains:

```python
await agent_store.run_migrations()
await team_store.run_migrations()
```

after construction, before seed.

### 5. Multi-replica safety

File store: single-process, no lock needed.

Redis store: wrap `run_migrations` in a `SET NX EX 300` lock on
`gateway:agents:migration_lock` so only one replica applies. Others
spin until the stamp bumps, or fail fast with "schema version N+1
expected, got N" if the lock holder dies.

### 6. Forward-only

Rollback migrations are out of scope. If we need to revert, ship a
forward migration that undoes the offending change. Downgrade requires
redeploying the older binary + replaying from a backup.

## Success criteria

- [ ] `_StoreState.schema_version` exists and serializes to both backends.
- [ ] `migrations/` subpackage with an obvious naming + loading scheme.
- [ ] `SpecStore.run_migrations()` implemented, invoked from lifespan.
- [ ] At least one toy migration (`_0001_*.py`) to validate the end-to-end
path, plus an integration test that starts a store at v0, runs
migrations, asserts stamp bumps + data transforms.
- [ ] Redis lock wraps the Redis mixin's migration run.
- [ ] Docs updated (`docs/concepts/agent-versioning.md`) explaining when
operators need to think about migrations (spoiler: almost never —
they just run automatically).

## Out of scope

- Rollback migrations.
- Online migrations that run while traffic is served (the lifespan
model assumes a brief downtime window at boot, which matches every
other lifespan-scoped setup today).
- Cross-entity migrations (e.g. "rename this agent name everywhere it
appears in a team's workers list"). These are harder because they
need both stores locked simultaneously. Defer until actually needed.
- GUI for migration history / manual re-run. Logs + the stamp are
enough for the operator use case.

## References

- Commit `7a0720f` — removes `migrate_from_legacy`, establishes v0 baseline.
- `src/agentic_primitives_gateway/agents/base_store.py` — where `_StoreState`
lives today.
- `src/agentic_primitives_gateway/agents/file_store.py` /
`src/agentic_primitives_gateway/agents/redis_store.py` — the two backends
that need the stamp + lock.

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.