akgentic-catalog
Configuration management for the
Akgentic multi-agent
framework. Store, query, clone, validate, and resolve versioned
configuration entries (teams, agents, tools, prompts, models, and any
allowlisted Pydantic model) through a single unified Catalog service
backed by a pluggable EntryRepository.
Table of Contents
- Overview
- Installation
- Quick Start
- Architecture
- The Entry Model
- Registering customer model types
- Storage Backends
- Sharing scalars between entries
- References Between Entries
- Querying the Catalog
- CLI
- REST API
- Development
- License
Overview
Version 2 of akgentic-catalog replaces the v1 four-catalog split
(templates, tools, agents, teams each with its own service, repository,
model, and query) with a single Entry model, a single Catalog service,
and a single EntryRepository protocol. An entry is identified by the
compound key (kind, namespace, id) and carries an opaque, schema-validated
payload sized for any allowlisted Pydantic model type.
Key properties:
- Unified
Entrymodel — one Pydantic shape for every kind of configuration. Built-in kinds includeteam,agent,tool,prompt, andmodel, and arbitrary new kinds are allowed as long as the payload'smodel_typeresolves through the configured prefix allowlist (akgentic.always, widenable per deployment). - Namespaces as tenancy / environment boundaries. Each namespace is a
self-contained bundle: one
teamroot entry plus any number of sub-entries referencing it. - Two-phase ref model — sub-entries embed sentinel
{"__ref__": "<id>", "__type__": "<model_type>"}dicts where the team references them; the resolver walks these refs (with cycle detection) to produce a fully-populated runtime object. - Pluggable storage — YAML-file-per-entry and MongoDB single-collection
backends ship in the box behind the
EntryRepositoryprotocol. - Namespace bundles — export/import a whole namespace (team + all sub-entries) as a single YAML document for round-tripping between environments.
- CLI and REST API — manage entries and bundles outside of Python.
Installation
Workspace Installation (Recommended)
This package is designed for use within the Akgentic monorepo workspace:
git clone git@github.com:b12consulting/akgentic-quick-start.git
cd akgentic-quick-start
git submodule update --init --recursive
uv venv
source .venv/bin/activate
uv sync --all-packages --all-extras
All dependencies (akgentic-core, akgentic-llm, akgentic-tool,
akgentic-team) resolve automatically via workspace configuration.
Optional Extras
| Extra | Packages pulled in | Enables |
|---|---|---|
api |
fastapi, uvicorn |
create_app() FastAPI factory |
cli |
typer, rich |
ak-catalog console script |
mongo |
pymongo |
MongoEntryRepository |
postgres |
nagra, psycopg[binary] |
PostgresEntryRepository, init_db |
uv sync --extra api
uv sync --extra cli
uv sync --extra mongo
uv sync --extra postgres
uv sync --all-extras
Quick Start
Create a fresh YAML-backed catalog, seed a team namespace, and resolve it:
import tempfile
from pathlib import Path
from akgentic.catalog import (
Catalog,
Entry,
UNSET_NAMESPACE,
YamlEntryRepository,
)
with tempfile.TemporaryDirectory() as tmp:
repo = YamlEntryRepository(Path(tmp))
catalog = Catalog(repo)
# Create the team root with a to-be-minted namespace.
team = Entry(
id="research-team",
kind="team",
namespace=UNSET_NAMESPACE,
user_id="u1",
model_type="akgentic.team.models.TeamCard",
payload={
"name": "Research Team",
"entry_point": {
"__ref__": "lead-agent",
"__type__": "akgentic.core.AgentCard",
},
"members": [],
},
)
team = catalog.create(team) # namespace replaced by a fresh UUID
namespace = team.namespace
# Create a sub-entry in the same namespace.
agent = catalog.create(Entry(
id="lead-agent",
kind="agent",
namespace=namespace,
user_id="u1",
model_type="akgentic.core.AgentCard",
payload={"role": "Lead", "description": "Coordinates the team"},
))
# Read / resolve.
stored_team = catalog.get(namespace=namespace, id="research-team")
team_card = catalog.load_team(namespace) # TeamCard with refs populated
See the architecture shards for a namespace-bundle walkthrough and YAML authoring guidance.
Architecture
flowchart LR
PY[Python API] --> CAT
CLI[ak-catalog CLI] --> CAT
API[FastAPI /catalog] --> CAT
CAT[Catalog service] --> RES[resolver.py]
CAT --> REPO[EntryRepository]
REPO --> YAML[(YamlEntryRepository)]
REPO --> MONGO[(MongoEntryRepository)]
REPO --> POSTGRES[(PostgresEntryRepository)]
The runtime layout under src/akgentic/catalog/ mirrors shard 10:
src/akgentic/catalog/
__init__.py Public API (Catalog, Entry, EntryKind, EntryQuery, ...)
catalog.py Unified Catalog service (CRUD + clone + resolve + load_team)
resolver.py Two-phase ref resolver + allowlisted model loader
env.py ${VAR} substitution for YAML payloads
serialization.py Namespace bundle load/dump
validation.py Namespace-level validation report
models/ Entry, EntryKind, EntryQuery, CloneRequest, errors
repositories/ EntryRepository protocol + YAML + Mongo impls
api/ FastAPI app + /catalog router
cli/ Typer ak-catalog app
Layered invariants (enforced by Catalog)
- Namespace bootstrap — non-team entries require a pre-existing team entry in the same namespace.
- Namespace minting — creating a team with
namespace=UNSET_NAMESPACEmints a fresh UUID before any other pipeline step runs. - Ownership propagation — every sub-entry inherits the team's
user_id. - Delete guards — deleting an entry referenced by another entry in the
same namespace raises
CatalogValidationErrorlisting inbound referrers. - Clone atomicity —
clonecollects every intended write in memory and emits them in a single pass; partial failures leave the destination untouched.
The Entry Model
Every catalog row is an Entry:
from akgentic.catalog import Entry, EntryKind
Entry(
id="lead-agent", # stable within (kind, namespace)
kind=EntryKind.AGENT, # "team" | "agent" | "tool" | "prompt" | "model" | ...
namespace="tenant-42", # tenancy / environment boundary
user_id="u1", # ownership; propagated from the team
model_type="akgentic.core.AgentCard", # allowlisted Pydantic class
payload={"role": "Lead", "description": "..."},
)
model_type is a dotted path to a Pydantic BaseModel subclass whose
prefix is on the configured allowlist — akgentic. always, plus whatever
the deployment authorized (see
Registering customer model types). The
resolver calls akgentic.catalog.resolver.load_model_type to materialize
it. Payloads validate against that class at create/update time.
Registering customer model types
model_type prefixes are a deployment policy. akgentic. is always
allowed and is never removable; configuration only widens the set. Point
AKGENTIC_CATALOG_MODEL_TYPE_PREFIXES at your own namespace — as a
comma-separated list or a JSON array, both parse identically — and catalog
entries may name your classes:
# Comma-separated — or, equivalently, a JSON array:
export AKGENTIC_CATALOG_MODEL_TYPE_PREFIXES=acme.core.models.,contoso.models.
export AKGENTIC_CATALOG_MODEL_TYPE_PREFIXES='["acme.core.models.","contoso.models."]'
id: case-ingestion
kind: tool
namespace: acme-prod
user_id: anonymous
model_type: acme.core.models.CaseIngestionConfig
description: Case ingestion settings
payload: { source: sftp, batch_size: 200 }
Prefer the narrowest prefix — acme.core.models., not acme.: every
module under an allowed prefix becomes something a catalog entry can cause
to be imported, so a prefix is a blast radius, not just a gate. Give
every process the same value — server, worker, and CLI — or one process
will accept an entry another refuses to resolve. The setting is
startup-only, process-wide, and never reachable from the HTTP surface;
set_allowed_prefixes(["acme.core.models."]) from akgentic.catalog,
called during startup wiring before the first Entry is constructed or
resolved, is the in-code equivalent.
Storage Backends
YAML (default)
YamlEntryRepository(root) lays out one file per entry, namespaced
directory per namespace, partitioned by kind:
<root>/
<namespace>/
team/research-team.yaml
agent/lead-agent.yaml
tool/web-search.yaml
from akgentic.catalog import Catalog, YamlEntryRepository
catalog = Catalog(YamlEntryRepository("./catalog"))
MongoDB
MongoEntryRepository stores every entry in a single collection indexed by
the compound (kind, namespace, id) key. Install the mongo extra and
provide a connection:
from akgentic.catalog import Catalog, MongoCatalogConfig, MongoEntryRepository
cfg = MongoCatalogConfig(
connection_string="mongodb://localhost:27017",
database="akgentic",
)
catalog = Catalog(MongoEntryRepository(cfg))
PostgreSQL
PostgresEntryRepository stores every entry in a single catalog_entries
table keyed by the compound (namespace, id) primary key. Install the
postgres extra and provide a DSN via one of three supply channels
(flag-wins precedence on the CLI, explicit kwarg on the API factory):
| Supply channel | Consumer |
|---|---|
PostgresCatalogConfig(connection_string=) |
create_app() factory |
--postgres-conn-string CLI flag |
ak-catalog |
DB_CONN_STRING_PERSISTENCE env var |
CLI + init-container |
from akgentic.catalog import Catalog
from akgentic.catalog.api.app import create_app
from akgentic.catalog.repositories.postgres import (
PostgresCatalogConfig,
PostgresEntryRepository,
)
# Programmatic / API path.
cfg = PostgresCatalogConfig(
connection_string="postgresql://postgres:pw@localhost:5432/catalog",
)
app = create_app(backend="postgres", postgres_config=cfg)
# Direct repository path.
catalog = Catalog(PostgresEntryRepository(cfg.connection_string))
Deployment prerequisite. The
PostgresEntryRepositoryconstructor does NOT create the schema — it only validates the DSN. Before starting the catalog service against a fresh database, run the runnable init-container module once per environment (KubernetesinitContainer/ Nomadprestartpattern):DB_CONN_STRING_PERSISTENCE=postgresql://postgres:pw@localhost:5432/catalog \ python -m akgentic.catalog.scripts.init_dbExit code
0on success,2when the env var is missing,1on any other failure (unreachable host, malformed DSN, driver error).
All three backends expose the same EntryRepository protocol; parity
tests under tests/repositories/test_entry_repository_contract.py keep
them interchangeable.
Sharing scalars between entries
When several entries need to reuse the same bare scalar — a prompt body, a
default role label, a model id — the catalog ships a sanctioned wrapper
called NativeValue. A NativeValue entry carries a single value field;
the resolver unwraps the value at the ref-splice site so a typed str /
int / bool field on the consuming entry receives the bare scalar
instead of the wrapper.
# data/catalog/agent-team/prompt/id_team_template.yaml
id: id_team_template
kind: prompt
namespace: agent-team
user_id: anonymous
model_type: akgentic.catalog.NativeValue
description: System-prompt template body for team members
payload:
value: "You are {role}. Collaborate with your team."
# data/catalog/agent-team/prompt/id_team_role.yaml
id: id_team_role
kind: prompt
namespace: agent-team
user_id: anonymous
model_type: akgentic.catalog.NativeValue
description: Default role label for team members
payload:
value: "a helpful team member"
# data/catalog/agent-team/prompt/id_team_prompt.yaml
id: id_team_prompt
kind: prompt
namespace: agent-team
user_id: anonymous
model_type: akgentic.llm.prompts.PromptTemplate
description: Default system prompt for team members
payload:
template: { __ref__: "id_team_template" } # resolves to str
params:
role: { __ref__: "id_team_role" } # resolves to str
Two things are worth pinning explicitly:
- The resolver unwraps
.valueat ref-splice time. From every other layer's perspective — repositories, CLI, HTTP, bundle export — aNativeValueentry is a normal entry with a{"value": <scalar>}payload. The unwrap fires only when a__ref__marker targets aNativeValue; direct retrieval viaCatalog.getreturns theEntrylike any other entry. NativeValue.value: dict[str, Any]is for JSON literals at boundaries, NOT for structured catalog content. Storing a typed structure undervalueas an untyped dict effectively bypasses the "payload is aBaseModel" invariant — the consuming side has nothing to validate against. If you need typed structured content, write a realBaseModel. The catalog does not mechanically block this anti-pattern; the discipline is on the catalog author.
See _bmad-output/akgentic-catalog/decisions/adr-15-native-value-refs.md
for the full rationale and design.
References Between Entries
Sub-entries are embedded in the team payload (and in each other) as sentinel ref dicts, not by plain ID strings. A ref is a two-key dict:
{"__ref__": "<entry-id>", "__type__": "<model_type>"}
The constants REF_KEY and TYPE_KEY are re-exported from
akgentic.catalog for construction/inspection. The resolver walks these
refs in two phases — populate_refs (ensures every ref resolves to a
known entry) and resolve (materializes the runtime Pydantic object) —
with cycle detection. See architecture/05-validation.md and
architecture/06-service-and-env.md for the full rules.
Querying the Catalog
EntryQuery is the single query model for all kinds. Any subset of
filters may be provided; unspecified filters are ignored.
from akgentic.catalog import EntryQuery
# Every entry in a namespace.
catalog.list_by_namespace("tenant-42")
# Cross-namespace filter.
catalog.list(EntryQuery(kind="agent", user_id="u1"))
# Description substring search.
catalog.list(EntryQuery(kind="tool", description_contains="search"))
CLI
The optional ak-catalog console script (enabled by --extra cli)
mounts a Typer app with one subcommand group per kind plus top-level
verbs for namespace-scoped and schema operations.
# Kind-scoped CRUD.
ak-catalog --root ./catalog team list --namespace tenant-42
ak-catalog --root ./catalog agent get --namespace tenant-42 lead-agent
ak-catalog --root ./catalog agent create ./lead-agent.yaml
# Namespace bundle round-trip.
ak-catalog --root ./catalog export --namespace tenant-42 > tenant-42.yaml
ak-catalog --root ./catalog import ./tenant-42.yaml
# Validation & schema.
ak-catalog --root ./catalog validate --namespace tenant-42
ak-catalog --root ./catalog validate ./tenant-42.yaml # dry-run from bundle
ak-catalog schema akgentic.core.AgentCard
ak-catalog model-types # list allowlisted types
Full reference: docs/cli-usage-guide.md.
REST API
The optional FastAPI app (enabled by --extra api) mounts the /catalog
router. Start it in-process:
uvicorn "akgentic.catalog:create_app" --factory
Backend is selected via environment variables at app-factory time
(YAML by default; set AKGENTIC_CATALOG_BACKEND=mongo plus connection
fields for MongoDB). Error responses map catalog exceptions to HTTP:
| Status | Cause |
|---|---|
404 |
EntryNotFoundError |
409 |
CatalogValidationError |
422 |
Pydantic ValidationError on payload |
See src/akgentic/catalog/api/router.py for the full endpoint surface
(CRUD per kind, namespace bundle export/import, schema, resolve, validate).
Development
Prerequisites
- Python 3.12+
- uv package manager
Setup
uv sync --all-extras
Commands
# Run tests
uv run pytest tests/
# Run tests with coverage
uv run pytest tests/ --cov=akgentic.catalog --cov-fail-under=80
# Lint
uv run ruff check src/ tests/
# Format
uv run ruff format src/ tests/
# Type check
uv run mypy src/
License
This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0).
Dual licensing & CLA — Akgentic is available under the AGPL-3.0 open-source license. A commercial license is also planned for organizations that require alternative terms. Contact Yuma for more information. External contributions will be accepted once a Contributor License Agreement (CLA) is in place. Until then, please hold off on submitting pull requests.
Metadata
Release files for akgentic-catalog 2.2.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| akgentic_catalog-2.2.2.tar.gz | 313.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| akgentic_catalog-2.2.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 450.7 kB
Release files / akgentic_catalog-2.2.2.tar.gz
| Download URL | akgentic_catalog-2.2.2.tar.gz |
|---|---|
| Size | 313.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
02dac397e87f6c409109d8241509c4831bf50fe2e0995dfaf4b9947fd31ca830
|
|
BLAKE2b-256 checksum How to use checksums |
43f76f8454f41c03faa0be4b40d4bcbb4af827d6e14d6217c12ffcac9da23732
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 7, 2026.
Transparency logRelease files / akgentic_catalog-2.2.2-py3-none-any.whl
| Download URL | akgentic_catalog-2.2.2-py3-none-any.whl |
|---|---|
| Size | 137.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
463541fb94c9e56133baa614daa984e20797c0117ccc9aaa0a31946e014ebb92
|
|
BLAKE2b-256 checksum How to use checksums |
a87335d8565ea854b6122fa93d8e9072135bc4bee9121b6138be4b9f1cca1ab3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 7, 2026.
Transparency log