community-agent-framework-postgres 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. community_agent_framework_postgres-0.1.1/.gitignore +14 -0
  2. community_agent_framework_postgres-0.1.1/CHANGELOG.md +72 -0
  3. community_agent_framework_postgres-0.1.1/CONTRIBUTING.md +106 -0
  4. community_agent_framework_postgres-0.1.1/LICENSE +21 -0
  5. community_agent_framework_postgres-0.1.1/PKG-INFO +367 -0
  6. community_agent_framework_postgres-0.1.1/README.md +336 -0
  7. community_agent_framework_postgres-0.1.1/docker-compose.yml +19 -0
  8. community_agent_framework_postgres-0.1.1/docs/azure-entra.md +106 -0
  9. community_agent_framework_postgres-0.1.1/docs/checkpoints.md +111 -0
  10. community_agent_framework_postgres-0.1.1/docs/compatibility.md +94 -0
  11. community_agent_framework_postgres-0.1.1/docs/history.md +117 -0
  12. community_agent_framework_postgres-0.1.1/docs/leases.md +75 -0
  13. community_agent_framework_postgres-0.1.1/docs/migrations.md +125 -0
  14. community_agent_framework_postgres-0.1.1/docs/retention.md +99 -0
  15. community_agent_framework_postgres-0.1.1/docs/sessions.md +72 -0
  16. community_agent_framework_postgres-0.1.1/docs/thread-snapshots.md +72 -0
  17. community_agent_framework_postgres-0.1.1/pyproject.toml +95 -0
  18. community_agent_framework_postgres-0.1.1/samples/azure_entra.py +36 -0
  19. community_agent_framework_postgres-0.1.1/samples/checkpoint_storage.py +44 -0
  20. community_agent_framework_postgres-0.1.1/samples/history_provider.py +31 -0
  21. community_agent_framework_postgres-0.1.1/samples/leases.py +43 -0
  22. community_agent_framework_postgres-0.1.1/samples/session_store.py +30 -0
  23. community_agent_framework_postgres-0.1.1/samples/thread_snapshot_store.py +30 -0
  24. community_agent_framework_postgres-0.1.1/src/agent_framework_community_postgres/__init__.py +77 -0
  25. community_agent_framework_postgres-0.1.1/src/agent_framework_community_postgres/__main__.py +3 -0
  26. community_agent_framework_postgres-0.1.1/src/agent_framework_community_postgres/_checkpoint_storage.py +218 -0
  27. community_agent_framework_postgres-0.1.1/src/agent_framework_community_postgres/_cli.py +97 -0
  28. community_agent_framework_postgres-0.1.1/src/agent_framework_community_postgres/_client.py +390 -0
  29. community_agent_framework_postgres-0.1.1/src/agent_framework_community_postgres/_entra.py +178 -0
  30. community_agent_framework_postgres-0.1.1/src/agent_framework_community_postgres/_framework.py +148 -0
  31. community_agent_framework_postgres-0.1.1/src/agent_framework_community_postgres/_history_provider.py +261 -0
  32. community_agent_framework_postgres-0.1.1/src/agent_framework_community_postgres/_json.py +34 -0
  33. community_agent_framework_postgres-0.1.1/src/agent_framework_community_postgres/_leases.py +196 -0
  34. community_agent_framework_postgres-0.1.1/src/agent_framework_community_postgres/_migrations.py +235 -0
  35. community_agent_framework_postgres-0.1.1/src/agent_framework_community_postgres/_persistence.py +184 -0
  36. community_agent_framework_postgres-0.1.1/src/agent_framework_community_postgres/_retention.py +159 -0
  37. community_agent_framework_postgres-0.1.1/src/agent_framework_community_postgres/_session_store.py +135 -0
  38. community_agent_framework_postgres-0.1.1/src/agent_framework_community_postgres/_store.py +66 -0
  39. community_agent_framework_postgres-0.1.1/src/agent_framework_community_postgres/_thread_snapshot_store.py +162 -0
  40. community_agent_framework_postgres-0.1.1/src/agent_framework_community_postgres/py.typed +0 -0
  41. community_agent_framework_postgres-0.1.1/tests/conformance/test_checkpoint_storage.py +53 -0
  42. community_agent_framework_postgres-0.1.1/tests/conformance/test_history_provider.py +59 -0
  43. community_agent_framework_postgres-0.1.1/tests/conformance/test_session_store.py +39 -0
  44. community_agent_framework_postgres-0.1.1/tests/conformance/test_thread_snapshot_store.py +48 -0
  45. community_agent_framework_postgres-0.1.1/tests/conftest.py +87 -0
  46. community_agent_framework_postgres-0.1.1/tests/integration/test_agent_run.py +99 -0
  47. community_agent_framework_postgres-0.1.1/tests/integration/test_checkpoint_storage.py +246 -0
  48. community_agent_framework_postgres-0.1.1/tests/integration/test_cli.py +85 -0
  49. community_agent_framework_postgres-0.1.1/tests/integration/test_client.py +184 -0
  50. community_agent_framework_postgres-0.1.1/tests/integration/test_database.py +14 -0
  51. community_agent_framework_postgres-0.1.1/tests/integration/test_entra.py +91 -0
  52. community_agent_framework_postgres-0.1.1/tests/integration/test_history_provider.py +185 -0
  53. community_agent_framework_postgres-0.1.1/tests/integration/test_jsonb.py +55 -0
  54. community_agent_framework_postgres-0.1.1/tests/integration/test_leases.py +263 -0
  55. community_agent_framework_postgres-0.1.1/tests/integration/test_migrations.py +120 -0
  56. community_agent_framework_postgres-0.1.1/tests/integration/test_persistence.py +168 -0
  57. community_agent_framework_postgres-0.1.1/tests/integration/test_retention.py +140 -0
  58. community_agent_framework_postgres-0.1.1/tests/integration/test_session_store.py +100 -0
  59. community_agent_framework_postgres-0.1.1/tests/integration/test_thread_snapshot_store.py +93 -0
  60. community_agent_framework_postgres-0.1.1/tests/unit/test_cli.py +78 -0
  61. community_agent_framework_postgres-0.1.1/tests/unit/test_client.py +278 -0
  62. community_agent_framework_postgres-0.1.1/tests/unit/test_docs.py +82 -0
  63. community_agent_framework_postgres-0.1.1/tests/unit/test_entra.py +405 -0
  64. community_agent_framework_postgres-0.1.1/tests/unit/test_framework.py +171 -0
  65. community_agent_framework_postgres-0.1.1/tests/unit/test_history_provider.py +45 -0
  66. community_agent_framework_postgres-0.1.1/tests/unit/test_json.py +108 -0
  67. community_agent_framework_postgres-0.1.1/tests/unit/test_migrations.py +104 -0
  68. community_agent_framework_postgres-0.1.1/tests/unit/test_package.py +9 -0
  69. community_agent_framework_postgres-0.1.1/tests/unit/test_public_api.py +42 -0
  70. community_agent_framework_postgres-0.1.1/tests/unit/test_retention.py +119 -0
  71. community_agent_framework_postgres-0.1.1/tests/unit/test_thread_snapshot_protocol.py +13 -0
  72. community_agent_framework_postgres-0.1.1/uv.lock +2292 -0
@@ -0,0 +1,14 @@
1
+ .venv*/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ .coverage
10
+ uv.lock.bak
11
+ .env
12
+
13
+ CLAUDE.md
14
+ AGENTS.md
@@ -0,0 +1,72 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [0.1.1] - 2026-10-11
8
+
9
+ The first release on PyPI.
10
+
11
+ ### Added
12
+
13
+ - Microsoft Entra ID sign-in for Azure Database for PostgreSQL: `credential=` on every store and on `PostgresPersistence` takes a sync or async `azure-identity` credential, and the owned pool uses a fresh token as the password of each new connection. A failing credential raises `PostgresStorageError` naming its error on the first call. With a credential, `sslmode` defaults to `verify-full` with the system trust store, and `disable`, `allow` and `prefer` are refused. `EntraCredential` and `ENTRA_SCOPE` are exported. See `docs/azure-entra.md`.
14
+ - The `azure` extra, which installs `azure-identity` and `aiohttp`, the async transport the `azure.identity.aio` credentials need.
15
+ - `PostgresStorageError.sqlstate` and `.retryable`.
16
+ - `pool_kwargs` on every store and the hub.
17
+ - `PostgresLeases.purge()`, which deletes the expired leases of its application and returns a `PurgeReport`.
18
+ - `compatibility()` and `Compatibility`: the installed `agent-framework-core` and `agent-framework-ag-ui` next to the tested minors. The first store or hub built in a process logs one warning per untested distribution: a newer minor than the tested ones, or a new major, which the warning calls outside the declared range. Importing the package emits no warning and logs nothing.
19
+ - `migrate(lock_timeout=)`, `PostgresPersistence.current_version()`, `migrate --print --from-version N`.
20
+ - `tenant_id` on `PostgresCheckpointStorage` and `PostgresPersistence.checkpoint_storage()`, part of the checkpoint row key next to `scope`.
21
+ - `PostgresHistoryProvider.list_sessions(limit=, after=)` pages session ids by keyset: `after` starts a page after the given id.
22
+
23
+ ### Changed
24
+
25
+ - Requires `agent-framework-core>=1.20.0,<2` and, through the `ag-ui` extra, `agent-framework-ag-ui>=1.5.0,<2`. On MAF 1.19, history dedupe drops any new message that matches one already stored, so a repeated "yes" was lost. 1.20 fixed that. Tested on `agent-framework-core` 1.20.0 and 1.21.0, with `agent-framework-ag-ui` 1.5.0 and 1.5.1.
26
+ - Owned pools set TCP keepalives and `tcp_user_timeout` unless the connection string or `pool_kwargs` sets them.
27
+ - The migration advisory lock key is now `hashtextextended` over the schema and table prefix; it was a crc32 of the schema-qualified migrations table name.
28
+ - Purge runs in batches of 1000 rows, one transaction per batch, and the hub purge also removes expired leases.
29
+ - Schema version 1 is the first released schema: checkpoints carry `tenant_id`, `payload_format` and a write `sequence`; every purged table has an expiry index; there is no document table. A database created by the unpublished 0.1.0 snapshot must be recreated.
30
+ - Checkpoint `get_latest` and listing order by write order within a scope, not by `checkpoint.timestamp`, so a host whose clock runs behind cannot hide the checkpoint it wrote last. Saves in one scope take turns on an advisory lock, and re-saving an id replaces its content and keeps its position.
31
+ - Checkpoint `load` and `get_latest` raise `WorkflowCheckpointException` when the row fails to decode, instead of `get_latest` falling back to an older checkpoint. `list_checkpoints` still skips such rows with a warning.
32
+ - `list_checkpoint_ids` reads ids without decoding, so it returns every id, including one whose row fails to decode.
33
+ - `PostgresSessionStore.get` raises `ValueError` when the stored snapshot has an unsupported `snapshot_version` or is not a JSON object, as MAF's `FileSessionStore` does for an unreadable file, instead of returning `None` as if the session were absent. A purged session still returns `None`.
34
+ - `PostgresAGUIThreadSnapshotStore.clear` follows the AG-UI protocol signature, `clear(*, scope=None)`: no scope removes every snapshot of the application, a scope removes that scope's. An empty scope raises `ValueError`.
35
+ - History saves and `clear` take the session's advisory lock inside a `READ COMMITTED` transaction, so concurrent saves no longer duplicate history on a server whose default isolation level is `REPEATABLE READ`. The lock key now includes the schema and table prefix, and `clear` no longer runs in the middle of a save. Migrations and lease statements run in `READ COMMITTED` too, so on such a server replicas that start together no longer fail on a table another replica just created, and a contended lease returns no lease instead of raising a serialization failure.
36
+ - A history save reads only the newest stored messages, as many as it was given, to deduplicate, instead of the whole session. The result is the same under MAF 1.20's `filter_new_messages`, and a save no longer slows down as a session grows.
37
+ - A history save raises `PostgresStorageError` when a stored message it needs for deduplication no longer deserializes, instead of skipping it and possibly storing a replay twice. `get_messages` still skips such rows with a warning.
38
+ - History retention keeps a session whole: a purge deletes a session's messages only once all of them have expired, instead of deleting an active session's oldest messages.
39
+
40
+ ### Removed
41
+
42
+ - The document store is gone before the first release: `PostgresDocumentStore`, `Document`, `DocumentSummary`, `RevisionConflict` and `PostgresPersistence.document_store()` are removed, and schema version 1 creates no `af_documents` table. No MAF abstraction maps to them. `PostgresLeases` stays, as a cross-replica lock.
43
+ - The CLI's `--connection-string`; set `POSTGRES_CONNECTION_STRING`, which keeps the password out of the command line that `ps` shows and out of shell history.
44
+
45
+ ### Fixed
46
+
47
+ - A lease's release after a failed or cancelled block waits at most 5 seconds, so a slow database cannot hold a cancelling task. The release keeps running in the background, and an unreleased lease frees itself on expiry.
48
+ - Custom credential exception text stays out of package errors and logs. Azure SDK exception diagnostics remain available, and the original exception remains the chained cause.
49
+ - Hub factories now reject `schema` and `table_prefix` overrides, so every store they create stays within the namespace covered by `hub.migrate()` and `hub.purge()`. Per-store `retention` overrides remain supported.
50
+ - A string with a lone surrogate now raises `ValueError` before any SQL runs, instead of a `UnicodeEncodeError` from inside psycopg.
51
+ - `PurgeReport` can be pickled, copied and passed to `dataclasses.asdict`, and its `counts` is read-only: it is a mapping, not a `dict`, so no `dict` method can change it.
52
+
53
+ ## [0.1.0] - 2026-10-01
54
+
55
+ Never published to PyPI. It declared `agent-framework-core>=1.19.0,<2`.
56
+
57
+ ### Added
58
+
59
+ - `PostgresHistoryProvider`, a MAF `HistoryProvider` with one row per message, scoped by application, tenant, agent, source and session.
60
+ - `PostgresSessionStore`, a MAF `SessionStore` for `AgentSession` snapshots.
61
+ - `PostgresCheckpointStorage`, a workflow `CheckpointStorage` with MAF's checkpoint encoding and an optional `scope`.
62
+ - `PostgresAGUIThreadSnapshotStore`, an `AGUIThreadSnapshotStore` for AG-UI, behind the `ag-ui` extra.
63
+ - `PostgresDocumentStore`, JSON documents with revisions, optimistic concurrency and keyset paging.
64
+ - `PostgresLeases`, renewable leases judged by the database clock. Each acquisition gets its own token, so a lease has one holder at a time even when two holders share an `owner`, and a crashed holder is recovered by expiry.
65
+ - `PostgresPersistence`, a hub that shares one connection pool across every store and runs migrations.
66
+ - `RetentionPolicy` with tombstone and delete modes, and `purge()` on every store and on the hub. A purge removes every row whose stored `expires_at` has passed, whichever policy stamped it, so the hub and the CLI need no TTL.
67
+ - Numbered schema migrations through `migrate()`, serialized across processes by an advisory lock.
68
+ - A CLI, `python -m agent_framework_community_postgres`, with `migrate`, `migrate --print`, `status` and `purge [--mode tombstone|delete] --application-id ID`.
69
+ - Stores created by a hub refuse to run after the hub closes.
70
+
71
+ [0.1.1]: https://github.com/matias-io/community-agent-framework-postgres/compare/50c91c5...v0.1.1
72
+ [0.1.0]: https://github.com/matias-io/community-agent-framework-postgres/tree/50c91c5
@@ -0,0 +1,106 @@
1
+ # Contributing
2
+
3
+ ## Run the database
4
+
5
+ The tests use the PostgreSQL service in `docker-compose.yml`. It listens on port 5433 and runs PostgreSQL 17 unless you override it.
6
+
7
+ ```bash
8
+ docker compose up -d --wait
9
+ ```
10
+
11
+ To test against PostgreSQL 16, start it as a separate compose project. The default project's volume holds a PostgreSQL 17 data directory, which PostgreSQL 16 refuses to start on.
12
+
13
+ ```bash
14
+ POSTGRES_VERSION=16 POSTGRES_PORT=5434 docker compose -p cafp-pg16 up -d --wait
15
+ export POSTGRES_TEST_CONNECTION_STRING=postgresql://postgres:postgres@127.0.0.1:5434/agent_framework
16
+ uv run pytest
17
+ docker compose -p cafp-pg16 down -v
18
+ ```
19
+
20
+ In PowerShell:
21
+
22
+ ```powershell
23
+ $env:POSTGRES_VERSION = "16"; $env:POSTGRES_PORT = "5434"; docker compose -p cafp-pg16 up -d --wait
24
+ Remove-Item Env:POSTGRES_VERSION, Env:POSTGRES_PORT
25
+ $env:POSTGRES_TEST_CONNECTION_STRING = "postgresql://postgres:postgres@127.0.0.1:5434/agent_framework"
26
+ uv run pytest
27
+ docker compose -p cafp-pg16 down -v
28
+ ```
29
+
30
+ The last command stops it and deletes its volume.
31
+
32
+ ## Run the checks
33
+
34
+ ```bash
35
+ export POSTGRES_TEST_CONNECTION_STRING=postgresql://postgres:postgres@127.0.0.1:5433/agent_framework
36
+ uv run pytest
37
+ uv run ruff check .
38
+ uv run ruff format --check .
39
+ uv run pyright
40
+ ```
41
+
42
+ In PowerShell:
43
+
44
+ ```powershell
45
+ $env:POSTGRES_TEST_CONNECTION_STRING = "postgresql://postgres:postgres@127.0.0.1:5433/agent_framework"
46
+ uv run pytest
47
+ uv run ruff check .
48
+ uv run ruff format --check .
49
+ uv run pyright
50
+ ```
51
+
52
+ Without `POSTGRES_TEST_CONNECTION_STRING`, the integration and conformance tests are skipped. Each integration test creates its own schema and drops it afterwards.
53
+
54
+ ## Commit messages
55
+
56
+ Use [Conventional Commits](https://www.conventionalcommits.org/). Write `type(scope): subject`, for example `fix(history): skip rows that fail to decode`. The types in use are `feat`, `fix`, `docs`, `test`, `refactor`, `ci` and `chore`.
57
+
58
+ ## Private Agent Framework imports
59
+
60
+ Import private Agent Framework names only in `src/agent_framework_community_postgres/_framework.py`, and add a test for each one to `tests/unit/test_framework.py`. Every other module imports from `_framework.py` or from MAF's public API.
61
+
62
+ ## Add a migration
63
+
64
+ 1. Write a function that takes `TableNames` and returns its statements, and append it to `MIGRATIONS` in `_migrations.py`. Its version is its position plus one.
65
+ 2. Never edit a migration that has shipped in a release. Fix it with a new one. Nothing shipped before 0.1.1, so its version 1 is the first.
66
+ 3. If the migration adds a table or index name longer than `history_messages_session_expiry_idx`, set `_LONGEST_IDENTIFIER` in `_client.py` to it, so `TableNames` still rejects a prefix that would push a name past 63 bytes.
67
+ 4. Add a test to `tests/integration/test_migrations.py`.
68
+
69
+ ## Add a store
70
+
71
+ 1. Add its table in a new migration in `_migrations.py`.
72
+ 2. Write the module, `_<name>.py`, with a class that extends `BaseStore` and has a `purge()`. Export it from `__init__.py` and add a factory to `PostgresPersistence`, including its table in `PostgresPersistence.purge()`.
73
+ 3. Add an integration test under `tests/integration/`.
74
+ 4. When MAF has an in-memory equivalent, add a conformance test under `tests/conformance/` that runs the same cases against both.
75
+ 5. Add a page under `docs/`, link it from `README.md`, and add a sample under `samples/`.
76
+
77
+ ## GitHub Actions
78
+
79
+ Every action in `.github/workflows/` is pinned by commit SHA, with the tag or branch it came from in a comment. To update one, resolve the new tag to its commit (dereference an annotated tag to the commit it points at) and change the SHA and the comment together. Dependabot (`.github/dependabot.yml`) checks the pins weekly and opens a pull request that does both.
80
+
81
+ `ci.yml` runs the locked MAF versions on Python 3.11 to 3.14 against PostgreSQL 16 and 17. It also runs the newest MAF release the declared ranges allow on each Python version against PostgreSQL 17. All these jobs block a merge. `canary.yml` runs every Monday against the newest MAF release: the compatibility check logs a warning on a newer minor and the suite still runs. When it passes on a new minor, add that minor to `TESTED_CORE` or `TESTED_AG_UI` in `_framework.py` and to `docs/compatibility.md` in the next release; when it fails, the fix ships in a patch release. GitHub disables a scheduled workflow after 60 days without repository activity, so check that the canary is still enabled. `release.yml` runs the full suite against PostgreSQL 17 before it builds, and uploads only if that passes and a reviewer approves.
82
+
83
+ ## Releasing
84
+
85
+ A release goes to PyPI through `release.yml`, and only through two manual steps: publishing a GitHub Release, then approving the upload. Pushing a tag publishes nothing. PyPI never lets a version be uploaded twice, even after its files are deleted, so check everything before you approve.
86
+
87
+ One-time setup:
88
+
89
+ 1. On PyPI, add a pending trusted publisher for the project `community-agent-framework-postgres`: owner `matias-io`, repository `community-agent-framework-postgres`, workflow `release.yml`, environment `pypi`. After the first upload it becomes the project's trusted publisher. Until this exists, no workflow can upload anything.
90
+ 2. On GitHub, create the environment `pypi` under Settings, Environments, and add yourself as a required reviewer. Every upload then waits for your approval, whichever commit's workflow asked for it.
91
+ 3. Optionally, add a tag ruleset under Settings, Rules that restricts who can create `v*` tags.
92
+
93
+ For each release:
94
+
95
+ 1. On the release branch, set `project.version` in `pyproject.toml`, add the version's section and link reference to `CHANGELOG.md` with the publication date, update the tested versions in `_framework.py` and `docs/compatibility.md`, and point the README's links at the new tag (`blob/vX.Y.Z/`). `tests/unit/test_docs.py` checks the date and that the links match `project.version`.
96
+ 2. Push the branch and open a pull request against `main`.
97
+ 3. Wait for the full CI matrix to pass, including PostgreSQL 16 and the `latest` MAF leg.
98
+ 4. Merge the pull request.
99
+ 5. Tag the merge commit and push only that tag: `git tag -a v0.1.1 -m "v0.1.1"`, then `git push origin v0.1.1`. Never `git push --tags` or `--follow-tags`.
100
+ 6. On GitHub, draft a release for the tag and publish it. That starts `release.yml`, which checks that the tag is `v` plus `project.version`, runs the full suite against PostgreSQL 17 and builds.
101
+ 7. Approve the `publish` job when GitHub asks. Reject it if anything looks wrong; nothing reaches PyPI until you approve.
102
+ 8. Open the project page on PyPI and check the README renders: links resolve to the tag on GitHub, and the tables display.
103
+
104
+ 0.1.0 was never published. Its commit carries the local tag `archive/0.1.0`, and its old `release.yml` still publishes on a `v*` tag push, so never give that commit a `v*` tag.
105
+
106
+ Optional dry run on TestPyPI before step 6: register the same pending publisher on test.pypi.org, then build locally with `uv build` and upload with `uv publish --publish-url https://test.pypi.org/legacy/` using a TestPyPI token, or add a job with `repository-url: https://test.pypi.org/legacy/` to the publish action. A version on TestPyPI cannot be reused either.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matias Suxo Salinas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,367 @@
1
+ Metadata-Version: 2.5
2
+ Name: community-agent-framework-postgres
3
+ Version: 0.1.1
4
+ Summary: PostgreSQL storage for Microsoft Agent Framework: chat history, sessions, workflow checkpoints, AG-UI thread snapshots and leases.
5
+ Project-URL: Source, https://github.com/matias-io/community-agent-framework-postgres
6
+ Project-URL: Issues, https://github.com/matias-io/community-agent-framework-postgres/issues
7
+ Project-URL: Changelog, https://github.com/matias-io/community-agent-framework-postgres/blob/main/CHANGELOG.md
8
+ Project-URL: Documentation, https://github.com/matias-io/community-agent-framework-postgres/tree/main/docs
9
+ Author: Matias Suxo Salinas
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agent-framework,checkpoint,history-provider,microsoft-agent-framework,postgresql,psycopg,session-store
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Database
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: agent-framework-core<2,>=1.20.0
24
+ Requires-Dist: psycopg[binary,pool]<4,>=3.3.5
25
+ Provides-Extra: ag-ui
26
+ Requires-Dist: agent-framework-ag-ui<2,>=1.5.0; extra == 'ag-ui'
27
+ Provides-Extra: azure
28
+ Requires-Dist: aiohttp<4,>=3.9; extra == 'azure'
29
+ Requires-Dist: azure-identity<2,>=1.19; extra == 'azure'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # community-agent-framework-postgres
33
+ A community-maintained PostgreSQL persistence implementation for Microsoft Agent Framework (MAF), supporting agent chat history, sessions, workflow checkpoints, AG-UI snapshots, and a lease that serializes work across replicas.
34
+
35
+ The package provides `PostgresHistoryProvider` for chat history, `PostgresSessionStore` for `AgentSession` snapshots, `PostgresCheckpointStorage` for workflow checkpoints, `PostgresAGUIThreadSnapshotStore` for AG-UI thread snapshots and `PostgresLeases` for a lock across replicas. `PostgresPersistence` creates all of them over one connection pool and runs the schema migrations. This is a community package written by Matias Suxo Salinas. Microsoft does not maintain or support it.
36
+
37
+ ## Status
38
+
39
+ Alpha. Public names and constructor arguments may change in a 0.x minor release, and the [changelog](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/CHANGELOG.md) lists every such change. Schema changes ship as new numbered migrations, so `migrate()` upgrades an existing database in place.
40
+
41
+ ## Compatibility
42
+
43
+ | 0.1.1 | Declared | Tested |
44
+ |---|---|---|
45
+ | `agent-framework-core` | `>=1.20.0,<2` | 1.20.0, 1.21.0 |
46
+ | `agent-framework-ag-ui` (`ag-ui` extra) | `>=1.5.0,<2` | 1.5.0, 1.5.1 |
47
+ | Python | `>=3.11` | 3.11, 3.12, 3.13, 3.14 |
48
+ | PostgreSQL | | 16, 17 |
49
+ | psycopg | `>=3.3.5,<4` | 3.3.6 |
50
+
51
+ pip and uv refuse a MAF version outside the declared range. A newer minor inside it installs. The first store or hub built in a process then logs one warning for each untested distribution, and a new major, which pip and uv install only when forced, is logged as outside the declared range. `compatibility()` returns the installed and tested versions for a diagnostics command. A newer minor is expected to work, because MAF keeps breaking changes for major versions. [docs/compatibility.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/compatibility.md) explains the support policy and the compatibility check.
52
+
53
+ MAF 1.19 and older are not supported. Their history dedupe drops any new message that matches one already stored, so a user who answers "yes" twice loses the second "yes". MAF 1.20 fixed that, so 1.20 is the floor.
54
+
55
+ 0.1.1 is the first release on PyPI. 0.1.0 was never published.
56
+
57
+ ## What it implements
58
+
59
+ | Class | MAF interface | Extra | Conditions |
60
+ |---|---|---|---|
61
+ | `PostgresHistoryProvider` | `HistoryProvider` | none | Drops replayed messages with MAF's own `filter_new_messages` |
62
+ | `PostgresSessionStore` | `SessionStore` | none | MAF marks `SessionStore` experimental |
63
+ | `PostgresCheckpointStorage` | `CheckpointStorage` | none | Decodes only MAF's safe types and its own; `allowed_checkpoint_types` adds more |
64
+ | `PostgresAGUIThreadSnapshotStore` | `AGUIThreadSnapshotStore` | `ag-ui` | The AG-UI endpoint needs a `snapshot_scope_resolver` next to `snapshot_store` |
65
+ | `PostgresLeases` | none (this package's own) | none | A cross-replica lock; MAF hosting serializes runs per process only |
66
+ | `PostgresPersistence` | none (creates the stores above over one pool) | none | |
67
+ | Microsoft Entra ID sign-in (`credential=`) | none | `azure` | Azure Database for PostgreSQL with Entra authentication |
68
+
69
+ ## Install
70
+
71
+ ```bash
72
+ uv add community-agent-framework-postgres
73
+ uv add "community-agent-framework-postgres[ag-ui]" # adds PostgresAGUIThreadSnapshotStore
74
+ uv add "community-agent-framework-postgres[azure]" # adds Microsoft Entra ID sign-in
75
+ pip install community-agent-framework-postgres
76
+ pip install "community-agent-framework-postgres[ag-ui]"
77
+ pip install "community-agent-framework-postgres[azure]"
78
+ ```
79
+
80
+ The `ag-ui` extra installs `agent-framework-ag-ui`. Without it, every store except `PostgresAGUIThreadSnapshotStore` works. `from agent_framework_community_postgres import *` also needs the extra, because `__all__` lists that store. The `azure` extra installs `azure-identity`, and `aiohttp` for its async credentials, for [Microsoft Entra ID sign-in](#microsoft-entra-id-on-azure).
81
+
82
+ On Windows, async psycopg cannot run on the default `ProactorEventLoop`. Run a script's entry point on a selector loop:
83
+
84
+ ```python
85
+ with asyncio.Runner(loop_factory=asyncio.SelectorEventLoop if sys.platform == "win32" else None) as runner:
86
+ runner.run(main())
87
+ ```
88
+
89
+ Run uvicorn 0.36 or later with `--loop asyncio:SelectorEventLoop`. uvicorn imports that value as the loop factory. Its plain `--loop asyncio` picks `ProactorEventLoop` on Windows unless reload or workers are on. Use `127.0.0.1` rather than `localhost` in local connection strings. libpq tries `::1` first, and some Windows and WSL setups drop that traffic without an error.
90
+
91
+ ## Quick start
92
+
93
+ This script runs against the database from `docker-compose.yml`. It needs the `ag-ui` extra.
94
+
95
+ ```python
96
+ import asyncio
97
+ import sys
98
+
99
+ from agent_framework import AgentSession, Message
100
+
101
+ from agent_framework_community_postgres import PostgresPersistence
102
+
103
+ DSN = "postgresql://postgres:postgres@127.0.0.1:5433/agent_framework"
104
+
105
+
106
+ async def main() -> None:
107
+ async with PostgresPersistence(application_id="quickstart", connection_string=DSN) as hub:
108
+ await hub.migrate()
109
+ history = hub.history_provider()
110
+ await history.save_messages("session-1", [Message(role="user", contents=["hello"])])
111
+ print([m.text for m in await history.get_messages("session-1")])
112
+ await hub.session_store().set("session-1", AgentSession(session_id="session-1"))
113
+ print(await hub.checkpoint_storage(scope="session-1").list_checkpoint_ids(workflow_name="demo"))
114
+ print(await hub.thread_snapshot_store().get(scope="user-1", thread_id="thread-1"))
115
+
116
+
117
+ with asyncio.Runner(loop_factory=asyncio.SelectorEventLoop if sys.platform == "win32" else None) as runner:
118
+ runner.run(main())
119
+ ```
120
+
121
+ `compatibility()` returns the installed and tested MAF versions, for a startup check or a diagnostics command.
122
+
123
+ Every store the hub creates shares its pool, `application_id`, `schema` and `table_prefix`. The factories raise `TypeError` if you pass `application_id`, `client`, `credential`, `pool_kwargs`, `schema` or `table_prefix`. For another schema or prefix, create a separate hub or a standalone store. You can override `retention` per store. `hub.purge()` covers every store created by that hub, including one with its own `retention`, and needs no policy of its own. It purges the rows whose `expires_at` has passed, in the mode you pass or else the hub policy's mode. `hub.leases()` returns a `PostgresLeases` over the same pool. After `hub.close()`, every store it created raises `PostgresStorageError`, even when the hub was built over your own pool.
124
+
125
+ ## Use with Agent Framework
126
+
127
+ Give an agent the history provider in `context_providers` and pass a session to `run`. The provider loads and saves that session's messages. This snippet needs a chat client. It uses `OpenAIChatClient` from `agent-framework-openai` with `OPENAI_API_KEY` set, but any MAF chat client works.
128
+
129
+ ```python
130
+ import asyncio
131
+ import sys
132
+
133
+ from agent_framework import Agent
134
+ from agent_framework.openai import OpenAIChatClient
135
+
136
+ from agent_framework_community_postgres import PostgresPersistence
137
+
138
+ DSN = "postgresql://postgres:postgres@127.0.0.1:5433/agent_framework"
139
+
140
+
141
+ async def main() -> None:
142
+ async with PostgresPersistence(application_id="my-app", connection_string=DSN) as hub:
143
+ await hub.migrate()
144
+ agent = Agent(OpenAIChatClient(model="gpt-4o"), "Answer briefly.", context_providers=[hub.history_provider()])
145
+ session = agent.create_session()
146
+ print((await agent.run("My name is Ada.", session=session)).text)
147
+ print((await agent.run("What is my name?", session=session)).text)
148
+
149
+
150
+ with asyncio.Runner(loop_factory=asyncio.SelectorEventLoop if sys.platform == "win32" else None) as runner:
151
+ runner.run(main())
152
+ ```
153
+
154
+ Pass the checkpoint storage to `WorkflowBuilder`. The workflow then writes checkpoints you can resume from. [docs/checkpoints.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/checkpoints.md) has the full example.
155
+
156
+ ```python
157
+ import asyncio
158
+ import sys
159
+
160
+ from agent_framework import Executor, WorkflowBuilder, WorkflowContext, handler
161
+
162
+ from agent_framework_community_postgres import PostgresPersistence
163
+
164
+ DSN = "postgresql://postgres:postgres@127.0.0.1:5433/agent_framework"
165
+
166
+
167
+ class Shout(Executor):
168
+ @handler
169
+ async def run(self, text: str, ctx: WorkflowContext[str, str]) -> None:
170
+ await ctx.yield_output(text.upper())
171
+
172
+
173
+ async def main() -> None:
174
+ async with PostgresPersistence(application_id="my-app", connection_string=DSN) as hub:
175
+ await hub.migrate()
176
+ storage = hub.checkpoint_storage(scope="conversation-1")
177
+ workflow = WorkflowBuilder(start_executor=Shout(id="shout"), checkpoint_storage=storage).build()
178
+ print((await workflow.run("hello")).get_outputs())
179
+ print(await storage.get_latest(workflow_name=workflow.name) is not None)
180
+
181
+
182
+ with asyncio.Runner(loop_factory=asyncio.SelectorEventLoop if sys.platform == "win32" else None) as runner:
183
+ runner.run(main())
184
+ ```
185
+
186
+ Save the `AgentSession` with `PostgresSessionStore` to continue a conversation in another process. An application loads the session at the start of a request and saves it after `agent.run` returns.
187
+
188
+ ```python
189
+ import asyncio
190
+ import sys
191
+
192
+ from agent_framework import AgentSession
193
+
194
+ from agent_framework_community_postgres import PostgresPersistence
195
+
196
+ DSN = "postgresql://postgres:postgres@127.0.0.1:5433/agent_framework"
197
+
198
+
199
+ async def main() -> None:
200
+ async with PostgresPersistence(application_id="my-app", connection_string=DSN) as hub:
201
+ await hub.migrate()
202
+ sessions = hub.session_store()
203
+ session = await sessions.get("user-1:chat-1") or AgentSession(session_id="chat-1")
204
+ session.state["visits"] = session.state.get("visits", 0) + 1
205
+ await sessions.set("user-1:chat-1", session)
206
+ print(session.session_id, session.state)
207
+
208
+
209
+ with asyncio.Runner(loop_factory=asyncio.SelectorEventLoop if sys.platform == "win32" else None) as runner:
210
+ runner.run(main())
211
+ ```
212
+
213
+ In an agent, create the new session with `agent.create_session(session_id=...)` instead of `AgentSession(...)`. See [docs/history.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/history.md) and [docs/sessions.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/sessions.md).
214
+
215
+ ## Standalone use
216
+
217
+ Each store also works on its own. A standalone store does not migrate, so create the tables first with `hub.migrate()` or the CLI.
218
+
219
+ ```bash
220
+ POSTGRES_CONNECTION_STRING=postgresql://postgres:postgres@127.0.0.1:5433/agent_framework python -m agent_framework_community_postgres migrate
221
+ ```
222
+
223
+ In PowerShell:
224
+
225
+ ```powershell
226
+ $env:POSTGRES_CONNECTION_STRING = "postgresql://postgres:postgres@127.0.0.1:5433/agent_framework"; python -m agent_framework_community_postgres migrate
227
+ ```
228
+
229
+ ```python
230
+ import asyncio
231
+ import sys
232
+
233
+ from agent_framework import Message
234
+
235
+ from agent_framework_community_postgres import PostgresHistoryProvider
236
+
237
+ DSN = "postgresql://postgres:postgres@127.0.0.1:5433/agent_framework"
238
+
239
+
240
+ async def main() -> None:
241
+ async with PostgresHistoryProvider(application_id="my-app", connection_string=DSN) as history:
242
+ await history.save_messages("session-1", [Message(role="user", contents=["hello"])])
243
+ print([m.text for m in await history.get_messages("session-1")])
244
+
245
+
246
+ with asyncio.Runner(loop_factory=asyncio.SelectorEventLoop if sys.platform == "win32" else None) as runner:
247
+ runner.run(main())
248
+ ```
249
+
250
+ Every store and the hub take the same connection arguments.
251
+
252
+ - `connection_string` or `client`. Pass exactly one. When both are absent, the connection string comes from `POSTGRES_CONNECTION_STRING`, read through MAF's `load_settings`. An explicit argument wins over the `.env` file named by `env_file_path`, which wins over the environment.
253
+ - `client` is a psycopg `AsyncConnection` or `AsyncConnectionPool`. A pool that is not open is opened on first use. The package never closes a client it did not create.
254
+ - `credential` signs the owned pool in to Azure Database for PostgreSQL with Microsoft Entra ID tokens. It needs a connection string and cannot be combined with `client`. With a credential, the pool defaults to `sslmode=verify-full` with `sslrootcert=system`. An `sslmode` or `sslrootcert` set in `pool_kwargs["kwargs"]`, the connection string, `PGSSLMODE` or `PGSSLROOTCERT` takes precedence over these defaults, and the store or hub raises `ValueError` when it is created if the `sslmode` in effect is `disable`, `allow` or `prefer`. An explicit `sslmode` also drops the `sslrootcert=system` default. On Windows, `system` finds no certificates in psycopg's binary wheel, so set `PGSSLROOTCERT` to a CA file; [docs/azure-entra.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/azure-entra.md) says which. See [Microsoft Entra ID on Azure](#microsoft-entra-id-on-azure).
255
+ - A single `AsyncConnection` runs one call at a time. Every store and hub over it waits for the same lock, so a server should pass a pool. On a connection already inside your own transaction, writes and multi-statement calls become a savepoint, single-statement reads run directly inside your transaction, and transaction-scoped locks (the history save lock and the migration lock) are held until your transaction ends.
256
+ - A connection string makes the store own a pool with `min_size=1`, `max_size=10`, autocommit, libpq `connect_timeout=10` unless the string sets one, and a pool `timeout` of 10 seconds.
257
+ - The owned pool also sets TCP keepalives (`keepalives=1`, `keepalives_idle=30`, `keepalives_interval=10`, `keepalives_count=3`) and `tcp_user_timeout=30000` unless the connection string sets them, so a silently dead connection fails within about a minute. The pool sets no statement or lock timeout; `migrate()` sets its own lock timeout.
258
+ - `pool_kwargs` is merged over the owned pool's defaults, and the `kwargs` entry inside it over the connection defaults; for example `{"max_size": 50, "kwargs": {"prepare_threshold": None}}` for PgBouncer. A connection parameter in `kwargs` wins over the same parameter in the connection string. It cannot be combined with `client`.
259
+ - `schema="public"` must be a lowercase identifier that does not start with `pg_`. `table_prefix="af_"` allows lowercase letters, digits and underscores, up to 28 bytes.
260
+
261
+ When the database is down or rejects the login, or every pooled connection stays busy, each call raises `PostgresStorageError("No PostgreSQL connection became available within 10 seconds: ...")` after about 10 seconds. psycopg_pool raises the same timeout in both cases, so the message names both. Over your own pool, the message gives that pool's `timeout`. The owned pool keeps retrying in the background and recovers when the database returns. psycopg_pool logs the driver's reason on the `psycopg.pool` logger. If you need other timeouts, set them through `pool_kwargs` or pass your own pool. A malformed connection string raises `PostgresStorageError("Invalid connection string.")` and never repeats libpq's text, which can contain the password.
262
+
263
+ A driver failure raises `PostgresStorageError` with the driver exception chained as the cause. Its `sqlstate` is the server's error code when there was one. Its `retryable` is `True` for SQLSTATE classes `08` (connection exceptions), `40` (serialization failures and deadlocks), `53` (insufficient resources, such as too many connections), codes `57P0x` (operator intervention, such as an administrator shutdown) and `55P03` (a lock timeout), for a connection lost without a SQLSTATE, and for the pool timeout above. A fresh call may get past these.
264
+
265
+ After a database restart or failover, each stale connection in the owned pool fails one call before the pool replaces it. Behind PgBouncer in transaction mode older than 1.21, or without `max_prepared_statements`, psycopg's prepared statements fail, because psycopg prepares a query after its fifth run. For either case, pass your own pool. `check=AsyncConnectionPool.check_connection` tests each connection as the pool hands it out, at the cost of one round trip, and `prepare_threshold=None` turns prepared statements off.
266
+
267
+ ```python
268
+ import asyncio
269
+ import sys
270
+
271
+ from psycopg_pool import AsyncConnectionPool
272
+
273
+ from agent_framework_community_postgres import PostgresPersistence
274
+
275
+ DSN = "postgresql://postgres:postgres@127.0.0.1:5433/agent_framework"
276
+
277
+
278
+ async def main() -> None:
279
+ pool = AsyncConnectionPool(
280
+ DSN,
281
+ open=False,
282
+ kwargs={"autocommit": True, "prepare_threshold": None},
283
+ check=AsyncConnectionPool.check_connection,
284
+ )
285
+ async with pool, PostgresPersistence(application_id="my-app", client=pool) as hub:
286
+ await hub.migrate()
287
+ print(await hub.pending_migrations())
288
+
289
+
290
+ with asyncio.Runner(loop_factory=asyncio.SelectorEventLoop if sys.platform == "win32" else None) as runner:
291
+ runner.run(main())
292
+ ```
293
+
294
+ ## Microsoft Entra ID on Azure
295
+
296
+ With the `azure` extra, pass an `azure-identity` credential as `credential=` and the owned pool signs in to Azure Database for PostgreSQL with a Microsoft Entra ID token for each new connection, instead of a password:
297
+
298
+ ```python
299
+ from azure.identity.aio import DefaultAzureCredential
300
+
301
+ DSN = "host=my-server.postgres.database.azure.com dbname=agent_framework user=ada@contoso.com"
302
+
303
+
304
+ async def main() -> None:
305
+ async with (
306
+ DefaultAzureCredential() as credential,
307
+ PostgresPersistence(application_id="my-app", connection_string=DSN, credential=credential) as hub,
308
+ ):
309
+ await hub.migrate()
310
+ ```
311
+
312
+ A failing credential raises `PostgresStorageError` naming its error on the first call. A managed identity or a service principal has no user name in its token, so set `user=` to its database role.
313
+
314
+ This is verified with a fake credential against PostgreSQL 17 locally, and the CI matrix runs the same tests on PostgreSQL 16 and 17. No real Azure server has been used yet. [docs/azure-entra.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/azure-entra.md) covers the server setup, managed identities, token lifetime and the CLI.
315
+
316
+ ## Schema
317
+
318
+ Tables are created in `schema` and named `{table_prefix}{name}`. With the defaults:
319
+
320
+ | Table | Key | Holds |
321
+ |---|---|---|
322
+ | `af_history_messages` | `id`, read by application, tenant, agent, source and session | One `Message.to_dict()` per row |
323
+ | `af_sessions` | `application_id, session_id` | `AgentSession.to_dict()` |
324
+ | `af_checkpoints` | `application_id, tenant_id, scope, checkpoint_id` | The encoded `WorkflowCheckpoint` |
325
+ | `af_thread_snapshots` | `application_id, scope, thread_id` | The latest AG-UI thread snapshot |
326
+ | `af_leases` | `application_id, resource` | Lease owner, token and expiry |
327
+ | `af_migrations` | `version` | Applied migration versions |
328
+
329
+ ## Retention
330
+
331
+ Retention is off unless you pass `RetentionPolicy(ttl=...)`. With a TTL, every write sets `expires_at` to now plus the TTL, and `purge()` or the `purge` CLI command tombstones or deletes the rows whose `expires_at` has passed. History messages, checkpoints and expired leases are always deleted. Leases take no policy. A history session is purged whole, once every one of its messages has expired, so an active conversation keeps its transcript. A purge works in batches of 1000 rows, one transaction per batch, and skips rows another purge has locked. `hub.purge()` covers every table, leases included. It returns a `PurgeReport`; `report.counts` is a read-only mapping, so use `dict(report.counts)` for a copy you can change. Until a purge runs, reads still return expired rows. See [docs/retention.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/retention.md).
332
+
333
+ ## Private Agent Framework imports
334
+
335
+ The package uses three names MAF does not export. `filter_new_messages` deduplicates history, and `encode_checkpoint_value` and `decode_checkpoint_value` encode checkpoints. Microsoft's own Cosmos DB package imports all three, and its Redis package imports `filter_new_messages`. `_framework.py` is the only module that imports them, and `tests/unit/test_framework.py` fails when one of them moves. CI also runs the suite against the newest `agent-framework-core` the declared range allows, and a weekly canary runs it against the newest MAF release, so an upstream rename shows up there first. See [docs/compatibility.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/compatibility.md).
336
+
337
+ ## Development
338
+
339
+ ```bash
340
+ docker compose up -d --wait
341
+ export POSTGRES_TEST_CONNECTION_STRING=postgresql://postgres:postgres@127.0.0.1:5433/agent_framework
342
+ # PowerShell: $env:POSTGRES_TEST_CONNECTION_STRING = "postgresql://postgres:postgres@127.0.0.1:5433/agent_framework"
343
+ uv run pytest
344
+ uv run ruff check .
345
+ uv run ruff format --check .
346
+ uv run pyright
347
+ ```
348
+
349
+ Without `POSTGRES_TEST_CONNECTION_STRING`, the integration and conformance tests are skipped. See [CONTRIBUTING.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/CONTRIBUTING.md).
350
+
351
+ ## Documentation
352
+
353
+ - [docs/history.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/history.md) covers `PostgresHistoryProvider`.
354
+ - [docs/sessions.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/sessions.md) covers `PostgresSessionStore`.
355
+ - [docs/checkpoints.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/checkpoints.md) covers `PostgresCheckpointStorage`.
356
+ - [docs/thread-snapshots.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/thread-snapshots.md) covers `PostgresAGUIThreadSnapshotStore`.
357
+ - [docs/leases.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/leases.md) covers `PostgresLeases`.
358
+ - [docs/retention.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/retention.md) covers `RetentionPolicy` and purging.
359
+ - [docs/migrations.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/migrations.md) covers `migrate()`, the CLI and SQL for a DBA.
360
+ - [docs/azure-entra.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/azure-entra.md) covers Microsoft Entra ID sign-in on Azure Database for PostgreSQL.
361
+ - [docs/compatibility.md](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/docs/compatibility.md) covers versions, private imports, JSONB limits and Windows.
362
+
363
+ Runnable scripts for each store are in [samples/](https://github.com/matias-io/community-agent-framework-postgres/tree/v0.1.1/samples).
364
+
365
+ ## License
366
+
367
+ MIT. See [LICENSE](https://github.com/matias-io/community-agent-framework-postgres/blob/v0.1.1/LICENSE).