Persistence¶
Part of core components.
What funduq stores, one line per table:
| table | holds |
|---|---|
providers |
one row per identity ever seen: public key, fingerprint, optional display name |
agents |
registered agents: (provider_key, name), agent card, joined/last-seen timestamps |
llm_providers |
registered LLM offerings: (provider_key, name), metadata, timestamps |
threads |
conversation containers: id, owning agent, parent-thread lineage, metadata |
runs |
one run per row: status, the AG-UI input it was dispatched with, metadata |
run_events |
the ordered AG-UI event log of each run, as relayed |
thread_messages |
the folded message history a thread reads back |
alembic_version |
the schema revision health() checks against the expected one |
How it is implemented¶
The tables are declared once as SQLAlchemy Core metadata (schema.py)
— table objects and typed columns, no ORM classes, no lazy loading.
Free-form content (agent cards, metadata, event payloads, run input) is
stored in JSON columns; identities and names are plain strings;
timestamps are UTC.
Every read and write goes through one module of async functions
(repo.py): each function takes an open session, builds a Core
statement (select/insert/update/delete), and the writing ones
commit before returning. Registration is an upsert — registering a name
that exists updates its card and last_seen_at rather than erroring —
and attach refreshes last_seen_at for the names it serves, which is
what the roster listings use to hide stale entries.
Dialect neutrality is structural, not disciplined: because everything is
built from the shared metadata and Core expressions, the same statements
compile for SQLite (the zero-config default — an on-disk file, async via
aiosqlite) and Postgres (an extra plus a DSN). The test suite runs
against both backends; dialect-specific SQL is not accepted into this
layer.
Schema lifecycle: the Alembic chain ships inside the package
(funduq/alembic in the installed wheel), and
funduq.migrate(database_url=None, db_schema=None) — or
python -m funduq.migrate — runs it programmatically. A fresh database is
created at head, an old one upgrades in place, and the version row is
written by the same mechanism. There is deliberately no second
create-the-tables path to drift against.
Managing the schema yourself¶
Plenty of deployments will not let an application migrate its own database. funduq does not require it to. Three tiers, each smaller than the last, and all of them supported rather than tolerated.
Tier 1 — you run the chain, on your terms. Point your own
alembic.ini at the packaged chain:
[alembic]
script_location = funduq:alembic
That single line is what this repository's own alembic.ini uses, so
you are running exactly what funduq runs, on your schedule and under your
review.
Tier 2 — funduq writes the SQL, your DBA applies it. Alembic's offline mode emits the DDL without contacting a database:
alembic upgrade head --sql
Run it from funduq/ — that is where alembic.ini lives, not the
repository root. The output ends by stamping the version row itself, so a DBA who applies
the script has a database funduq recognizes without funduq ever holding
credentials. alembic upgrade <from>:<to> --sql narrows it to one step
when you are upgrading in place.
Tier 3 — no Alembic at all. Make the tables match, then tell funduq which revision they match. Two facts, both importable:
funduq.schema.metadata— the SQLAlchemyMetaDataevery table is declared in, and the same object the migration chain targets. Whatever builds your schema must produce these tables and columns.funduq.db_schema.EXPECTED_SCHEMA_REVISION— the one string to write intoalembic_version. A test asserts it equals the chain's head, so it cannot drift from the migrations.
health() checks the revision you wrote
Funduq.health() returns both the revision it found and the one funduq
expected, and the Health it returns compares them:
schema_current is that comparison, and ready requires it. So a
Tier-3 database with the wrong revision row reports not-ready rather
than failing later in some unrelated place. (Note also that
Funduq.health() — the readiness probe — is a different thing from
the health module, which runs the stale-paused-run sweeps.)