graph-agents-cli 0.3.1__py3-none-any.whl
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.
- graph_agents_cli/__init__.py +26 -0
- graph_agents_cli/_api_policy.py +2145 -0
- graph_agents_cli/_approvals.py +400 -0
- graph_agents_cli/_build.py +186 -0
- graph_agents_cli/_build_info.json +7 -0
- graph_agents_cli/_chat_client.py +462 -0
- graph_agents_cli/_click.py +157 -0
- graph_agents_cli/_defaults.py +139 -0
- graph_agents_cli/_experiments.py +64 -0
- graph_agents_cli/_http.py +192 -0
- graph_agents_cli/_output.py +83 -0
- graph_agents_cli/_project.py +462 -0
- graph_agents_cli/_remote.py +220 -0
- graph_agents_cli/_response_schema.py +264 -0
- graph_agents_cli/_runner.py +319 -0
- graph_agents_cli/_skills_check.py +274 -0
- graph_agents_cli/_tools.py +189 -0
- graph_agents_cli/_trust.py +66 -0
- graph_agents_cli/api/__init__.py +15 -0
- graph_agents_cli/api/_changes.py +506 -0
- graph_agents_cli/api/_files.py +658 -0
- graph_agents_cli/api/cmd_api.py +2480 -0
- graph_agents_cli/deploy/__init__.py +15 -0
- graph_agents_cli/deploy/_config.py +171 -0
- graph_agents_cli/deploy/_image.py +128 -0
- graph_agents_cli/deploy/_kube.py +286 -0
- graph_agents_cli/deploy/_modes.py +234 -0
- graph_agents_cli/deploy/_preflight.py +370 -0
- graph_agents_cli/deploy/_values.py +168 -0
- graph_agents_cli/deploy/cmd_deploy.py +1866 -0
- graph_agents_cli/deploy/gitops.py +562 -0
- graph_agents_cli/deploy/local_load.py +273 -0
- graph_agents_cli/dev/__init__.py +13 -0
- graph_agents_cli/dev/cmd_build.py +131 -0
- graph_agents_cli/dev/cmd_install.py +78 -0
- graph_agents_cli/dev/cmd_lint.py +119 -0
- graph_agents_cli/dev/cmd_playground.py +297 -0
- graph_agents_cli/dev/policy_check.py +1287 -0
- graph_agents_cli/eval/__init__.py +22 -0
- graph_agents_cli/eval/_client.py +670 -0
- graph_agents_cli/eval/_common.py +177 -0
- graph_agents_cli/eval/_judge.py +168 -0
- graph_agents_cli/eval/_judge_runner.py +238 -0
- graph_agents_cli/eval/_paths.py +212 -0
- graph_agents_cli/eval/checks.py +581 -0
- graph_agents_cli/eval/cmd_analyze.py +278 -0
- graph_agents_cli/eval/cmd_compare.py +284 -0
- graph_agents_cli/eval/cmd_eval_group.py +80 -0
- graph_agents_cli/eval/cmd_generate.py +558 -0
- graph_agents_cli/eval/cmd_grade.py +466 -0
- graph_agents_cli/eval/cmd_metric.py +156 -0
- graph_agents_cli/eval/cmd_run.py +370 -0
- graph_agents_cli/eval/cmd_submit.py +400 -0
- graph_agents_cli/eval/config.py +435 -0
- graph_agents_cli/eval/dataset.py +350 -0
- graph_agents_cli/eval/gate.py +420 -0
- graph_agents_cli/eval/transcript.py +192 -0
- graph_agents_cli/extension/__init__.py +13 -0
- graph_agents_cli/extension/_compat.py +86 -0
- graph_agents_cli/extension/_loader.py +293 -0
- graph_agents_cli/extension/_manifest.py +135 -0
- graph_agents_cli/extension/_overrides.py +195 -0
- graph_agents_cli/extension/_paths.py +91 -0
- graph_agents_cli/extension/_refs.py +193 -0
- graph_agents_cli/extension/_resolver.py +453 -0
- graph_agents_cli/extension/_schema.py +106 -0
- graph_agents_cli/extension/_spec.py +253 -0
- graph_agents_cli/extension/_sync.py +102 -0
- graph_agents_cli/extension/_trust.py +58 -0
- graph_agents_cli/extension/cmd_extension_add.py +259 -0
- graph_agents_cli/extension/cmd_extension_group.py +57 -0
- graph_agents_cli/extension/cmd_extension_list.py +56 -0
- graph_agents_cli/extension/cmd_extension_remove.py +61 -0
- graph_agents_cli/extension/cmd_extension_update.py +195 -0
- graph_agents_cli/info/__init__.py +13 -0
- graph_agents_cli/info/cmd_info.py +222 -0
- graph_agents_cli/infra/__init__.py +15 -0
- graph_agents_cli/infra/checks.py +1169 -0
- graph_agents_cli/infra/cmd_infra.py +103 -0
- graph_agents_cli/main.py +591 -0
- graph_agents_cli/peer/__init__.py +15 -0
- graph_agents_cli/peer/_generate.py +254 -0
- graph_agents_cli/peer/cmd_peer.py +1151 -0
- graph_agents_cli/run/__init__.py +13 -0
- graph_agents_cli/run/_local_server.py +1157 -0
- graph_agents_cli/run/_signals.py +141 -0
- graph_agents_cli/run/cmd_approvals.py +530 -0
- graph_agents_cli/run/cmd_run.py +1421 -0
- graph_agents_cli/scaffold/__init__.py +19 -0
- graph_agents_cli/scaffold/agents/README.md +24 -0
- graph_agents_cli/scaffold/agents/empty_py/.template/templateconfig.yaml +22 -0
- graph_agents_cli/scaffold/agents/langgraph/.env.example +292 -0
- graph_agents_cli/scaffold/agents/langgraph/.template/templateconfig.yaml +28 -0
- graph_agents_cli/scaffold/agents/langgraph/Dockerfile +59 -0
- graph_agents_cli/scaffold/agents/langgraph/Dockerfile.langgraph-server +59 -0
- graph_agents_cli/scaffold/agents/langgraph/README.md +571 -0
- graph_agents_cli/scaffold/agents/langgraph/api-policy.yaml +60 -0
- graph_agents_cli/scaffold/agents/langgraph/app/__init__.py +20 -0
- graph_agents_cli/scaffold/agents/langgraph/app/agent.py +174 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/__init__.py +15 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/a2a.py +2162 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/a2a_client.py +1167 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/api_client.py +4220 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/approvals.py +1349 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/auth.py +1986 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/chat.py +2962 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/checkpointer.py +432 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/content.py +569 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/db.py +580 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/limits.py +203 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/metrics.py +231 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/middleware.py +361 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/model.py +611 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/playground.py +230 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/run_locks.py +459 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/structured.py +755 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/telemetry.py +681 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/threads.py +493 -0
- graph_agents_cli/scaffold/agents/langgraph/app/app_utils/token_exchange.py +959 -0
- graph_agents_cli/scaffold/agents/langgraph/app/fast_api_app.py +770 -0
- graph_agents_cli/scaffold/agents/langgraph/app/policies/__init__.py +55 -0
- graph_agents_cli/scaffold/agents/langgraph/app/policies/custom.py +97 -0
- graph_agents_cli/scaffold/agents/langgraph/app/tools/__init__.py +46 -0
- graph_agents_cli/scaffold/agents/langgraph/app/tools/example_api.py +92 -0
- graph_agents_cli/scaffold/agents/langgraph/app/tools/weather.py +33 -0
- graph_agents_cli/scaffold/agents/langgraph/langgraph.json +14 -0
- graph_agents_cli/scaffold/agents/langgraph/pyproject.toml +78 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/conftest.py +376 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/eval/datasets/basic-dataset.json +53 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/eval/eval_config.yaml +32 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/approval_graph.py +137 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/fake_issuer.py +216 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/fake_openai.py +357 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_a2a_outcomes.py +569 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_a2a_relay.py +479 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_api_surface.py +812 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_approvals.py +1367 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_approvals_server.py +794 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_cross_actor.py +497 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_cross_actor_server.py +247 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_history_repair.py +278 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_model_apis.py +242 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_postgres.py +637 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_resilience_postgres.py +770 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_runtime_guardrails.py +854 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_server_e2e.py +340 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_server_runtime.py +989 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_structured_answers.py +584 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_structured_server.py +222 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_token_exchange_issuer.py +650 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/load_test/.results/.placeholder +0 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/load_test/README.md +22 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/load_test/conftest.py +21 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/load_test/load_test.py +81 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_a2a_client.py +824 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_a2a_scoping.py +724 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_client.py +1214 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_client_hardening.py +716 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_policy_rpc.py +767 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_approval_ledger.py +1536 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_fake_model.py +115 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_jwt_policy.py +991 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_limits.py +310 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_logging.py +148 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_logging_hardening.py +271 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_policy.py +378 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_resilience.py +610 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_server_auth.py +702 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_structured.py +673 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_telemetry.py +404 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_thread_listing.py +255 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_threads.py +268 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_token_exchange.py +1320 -0
- graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_untrusted_content.py +393 -0
- graph_agents_cli/scaffold/agents/langgraph/uv-fastapi.lock +2084 -0
- graph_agents_cli/scaffold/agents/langgraph/uv-langgraph-server.lock +2106 -0
- graph_agents_cli/scaffold/agents/langgraph/{{cookiecutter.agent_guidance_filename}} +129 -0
- graph_agents_cli/scaffold/base_templates/_shared/graph-agents-cli-manifest.yaml +36 -0
- graph_agents_cli/scaffold/base_templates/python/.dockerignore +32 -0
- graph_agents_cli/scaffold/base_templates/python/.github/CODEOWNERS +30 -0
- graph_agents_cli/scaffold/base_templates/python/.github/agent.env +7 -0
- graph_agents_cli/scaffold/base_templates/python/.github/workflows/pr_checks.yaml +214 -0
- graph_agents_cli/scaffold/base_templates/python/.gitignore +209 -0
- graph_agents_cli/scaffold/base_templates/python/tests/unit/test_dummy.py +23 -0
- graph_agents_cli/scaffold/base_templates/python/{{cookiecutter.agent_guidance_filename}} +35 -0
- graph_agents_cli/scaffold/cmd_scaffold_group.py +49 -0
- graph_agents_cli/scaffold/commands/__init__.py +13 -0
- graph_agents_cli/scaffold/commands/create.py +1424 -0
- graph_agents_cli/scaffold/commands/enhance.py +1652 -0
- graph_agents_cli/scaffold/commands/upgrade.py +570 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/agent.env +12 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/workflows/promote-to-prod.yaml +371 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/workflows/staging.yaml +450 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-dev.yaml +43 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-prod.yaml +41 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-staging.yaml +43 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/.helmignore +14 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/Chart.yaml +21 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/examples/networkpolicy.yaml +103 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/NOTES.txt +48 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/_helpers.tpl +189 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/certificate.yaml +15 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/configmap.yaml +10 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/deployment.yaml +199 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/hpa.yaml +22 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/httproute.yaml +30 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/ingress.yaml +39 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/networkpolicy.yaml +48 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/pdb.yaml +13 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/postgresql-secret.yaml +37 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/service.yaml +15 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/serviceaccount.yaml +13 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/servicemonitor.yaml +42 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-dev.yaml +22 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-prod.yaml +45 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-staging.yaml +29 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values.yaml +396 -0
- graph_agents_cli/scaffold/deployment_targets/kubernetes/python/tests/integration/test_chart.py +269 -0
- graph_agents_cli/scaffold/deployment_targets/none/README.md +5 -0
- graph_agents_cli/scaffold/deployment_targets/none/python/README.md +6 -0
- graph_agents_cli/scaffold/utils/__init__.py +13 -0
- graph_agents_cli/scaffold/utils/backup.py +212 -0
- graph_agents_cli/scaffold/utils/build_record.py +257 -0
- graph_agents_cli/scaffold/utils/cli_options.py +184 -0
- graph_agents_cli/scaffold/utils/fs.py +83 -0
- graph_agents_cli/scaffold/utils/generate_locks.py +214 -0
- graph_agents_cli/scaffold/utils/generation_metadata.py +88 -0
- graph_agents_cli/scaffold/utils/keyedit.py +768 -0
- graph_agents_cli/scaffold/utils/keymerge.py +537 -0
- graph_agents_cli/scaffold/utils/language.py +138 -0
- graph_agents_cli/scaffold/utils/lock_utils.py +94 -0
- graph_agents_cli/scaffold/utils/logging.py +77 -0
- graph_agents_cli/scaffold/utils/manifest.py +292 -0
- graph_agents_cli/scaffold/utils/merge.py +970 -0
- graph_agents_cli/scaffold/utils/merge3.py +216 -0
- graph_agents_cli/scaffold/utils/openapi_seed.py +199 -0
- graph_agents_cli/scaffold/utils/remote_template.py +376 -0
- graph_agents_cli/scaffold/utils/template.py +1352 -0
- graph_agents_cli/scaffold/utils/upgrade.py +894 -0
- graph_agents_cli/scaffold/utils/version.py +438 -0
- graph_agents_cli/secrets/__init__.py +15 -0
- graph_agents_cli/secrets/_apply.py +954 -0
- graph_agents_cli/secrets/_required.py +188 -0
- graph_agents_cli/secrets/cmd_secrets.py +211 -0
- graph_agents_cli/setup/__init__.py +13 -0
- graph_agents_cli/setup/_antigravity.py +221 -0
- graph_agents_cli/setup/cmd_auth.py +1030 -0
- graph_agents_cli/setup/cmd_dev_token.py +513 -0
- graph_agents_cli/setup/cmd_setup.py +428 -0
- graph_agents_cli/setup/cmd_update.py +140 -0
- graph_agents_cli/skills/__init__.py +13 -0
- graph_agents_cli/skills/_bundle.py +65 -0
- graph_agents_cli/skills/data/README.md +19 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/SKILL.md +357 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/references/github-settings.md +113 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/references/gitops.md +137 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/references/kubernetes.md +315 -0
- graph_agents_cli/skills/data/graph-agents-cli-deploy/references/secrets.md +160 -0
- graph_agents_cli/skills/data/graph-agents-cli-eval/SKILL.md +303 -0
- graph_agents_cli/skills/data/graph-agents-cli-eval/references/dataset_schema.md +282 -0
- graph_agents_cli/skills/data/graph-agents-cli-eval/references/metrics-guide.md +143 -0
- graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/SKILL.md +659 -0
- graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/langchain-models.md +124 -0
- graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/langgraph.md +235 -0
- graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/template-contract.md +477 -0
- graph_agents_cli/skills/data/graph-agents-cli-observability/SKILL.md +231 -0
- graph_agents_cli/skills/data/graph-agents-cli-observability/references/langsmith.md +46 -0
- graph_agents_cli/skills/data/graph-agents-cli-observability/references/otel.md +59 -0
- graph_agents_cli/skills/data/graph-agents-cli-scaffold/SKILL.md +414 -0
- graph_agents_cli/skills/data/graph-agents-cli-scaffold/references/flags.md +134 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/SKILL.md +478 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/brainstorming.md +118 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/commands.md +419 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/extension.md +156 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/internals.md +67 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/spec-template.md +56 -0
- graph_agents_cli/skills/data/graph-agents-cli-workflow/references/terminology.md +119 -0
- graph_agents_cli/system/__init__.py +15 -0
- graph_agents_cli/system/_apply.py +519 -0
- graph_agents_cli/system/_checks.py +1023 -0
- graph_agents_cli/system/_deploy.py +215 -0
- graph_agents_cli/system/_model.py +363 -0
- graph_agents_cli/system/_system.py +664 -0
- graph_agents_cli/system/_views.py +208 -0
- graph_agents_cli/system/cmd_system.py +423 -0
- graph_agents_cli-0.3.1.dist-info/METADATA +162 -0
- graph_agents_cli-0.3.1.dist-info/RECORD +291 -0
- graph_agents_cli-0.3.1.dist-info/WHEEL +4 -0
- graph_agents_cli-0.3.1.dist-info/entry_points.txt +2 -0
- graph_agents_cli-0.3.1.dist-info/licenses/LICENSE +201 -0
- graph_agents_cli-0.3.1.dist-info/licenses/NOTICE +19 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
# Copyright 2026 graph-agents-cli contributors
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""Checkpointer selection for the fastapi runtime (memory or postgres), and the Postgres pool.
|
|
16
|
+
|
|
17
|
+
`CHECKPOINTER=memory` gives an `InMemorySaver` (no database, state lost on
|
|
18
|
+
restart); `CHECKPOINTER=postgres` gives an `AsyncPostgresSaver` on
|
|
19
|
+
`POSTGRES_DSN` with its schema set up. Under `langgraph-server` the server owns
|
|
20
|
+
persistence through `DATABASE_URI`/`REDIS_URI` and `agent.py` always exports
|
|
21
|
+
an unbound graph.
|
|
22
|
+
|
|
23
|
+
Pools (`open_pool`) check each connection before handing it out and replace
|
|
24
|
+
dead ones, so a Postgres restart or failover costs no failed requests once the
|
|
25
|
+
database is back. Size them with `DB_POOL_MIN_SIZE` (default 1) and
|
|
26
|
+
`DB_POOL_MAX_SIZE` (default 10); the fastapi runtime shares one pool between
|
|
27
|
+
the checkpointer and the app tables. Schema setup (the saver's migrations and
|
|
28
|
+
the app's DDL) runs under a Postgres advisory lock (`schema_lock`), so
|
|
29
|
+
replicas starting together against a fresh database take turns.
|
|
30
|
+
|
|
31
|
+
Outages are bounded on every connection the app opens (`connection_kwargs`):
|
|
32
|
+
a connection attempt gives up after `connect_timeout` (5 s), and TCP
|
|
33
|
+
keepalives on both ends notice a vanished peer in about a minute instead of the
|
|
34
|
+
operating system's two hours. A parameter already set in the DSN wins. While
|
|
35
|
+
the database is known to be unreachable (`DbHealth`), a request waits at most
|
|
36
|
+
`DOWN_POOL_TIMEOUT_S` for a connection instead of `POOL_TIMEOUT_S`, and the
|
|
37
|
+
pool retries in short cycles (`RECONNECT_TIMEOUT_S`), so requests fail fast
|
|
38
|
+
during an outage and the app is ready again seconds after the database is.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import asyncio
|
|
44
|
+
import logging
|
|
45
|
+
import os
|
|
46
|
+
import re
|
|
47
|
+
import time
|
|
48
|
+
from collections.abc import AsyncIterator, Callable
|
|
49
|
+
from contextlib import asynccontextmanager, suppress
|
|
50
|
+
from functools import cache
|
|
51
|
+
from typing import Any
|
|
52
|
+
|
|
53
|
+
from {{cookiecutter.agent_directory}}.app_utils import metrics
|
|
54
|
+
from {{cookiecutter.agent_directory}}.app_utils.limits import SettingsError
|
|
55
|
+
|
|
56
|
+
logger = logging.getLogger(__name__)
|
|
57
|
+
|
|
58
|
+
MEMORY = "memory"
|
|
59
|
+
POSTGRES = "postgres"
|
|
60
|
+
|
|
61
|
+
# Advisory-lock key serialising schema setup across replicas (any fixed 64-bit value).
|
|
62
|
+
SCHEMA_LOCK_KEY = 0x6761_7363_6865_6D61 # "gaschema"
|
|
63
|
+
SCHEMA_LOCK_POLL_S = 0.25
|
|
64
|
+
SCHEMA_LOCK_TIMEOUT_S = 300.0
|
|
65
|
+
|
|
66
|
+
DB_POOL_MIN_SIZE = ("DB_POOL_MIN_SIZE", 1)
|
|
67
|
+
DB_POOL_MAX_SIZE = ("DB_POOL_MAX_SIZE", 10)
|
|
68
|
+
# Seconds a request waits for a free pooled connection before failing (503).
|
|
69
|
+
POOL_TIMEOUT_S = 5.0
|
|
70
|
+
# The same while the database is known to be unreachable: fail fast.
|
|
71
|
+
DOWN_POOL_TIMEOUT_S = 2.0
|
|
72
|
+
# A reconnect cycle of the pool gives up after this long; the next request
|
|
73
|
+
# starts a new one at once, instead of waiting out an ever longer back-off.
|
|
74
|
+
RECONNECT_TIMEOUT_S = 5.0
|
|
75
|
+
# Idle connections older than this are closed (keeps NAT/proxy reaping harmless).
|
|
76
|
+
POOL_MAX_IDLE_S = 300.0
|
|
77
|
+
|
|
78
|
+
# libpq parameters every connection gets unless the DSN sets them.
|
|
79
|
+
CONNECTION_DEFAULTS: dict[str, str] = {
|
|
80
|
+
"connect_timeout": "5",
|
|
81
|
+
"keepalives": "1",
|
|
82
|
+
"keepalives_idle": "30",
|
|
83
|
+
"keepalives_interval": "10",
|
|
84
|
+
"keepalives_count": "3",
|
|
85
|
+
# Unacknowledged data for this long (ms) means the server is gone (Linux).
|
|
86
|
+
"tcp_user_timeout": "60000",
|
|
87
|
+
}
|
|
88
|
+
# Server-side keepalives, so Postgres ends the sessions of a replica that
|
|
89
|
+
# vanished without closing its connections (they hold connection slots).
|
|
90
|
+
SERVER_KEEPALIVE_SQL = (
|
|
91
|
+
"SELECT set_config('tcp_keepalives_idle', '30', false), "
|
|
92
|
+
"set_config('tcp_keepalives_interval', '10', false), "
|
|
93
|
+
"set_config('tcp_keepalives_count', '3', false)"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def checkpointer_kind() -> str:
|
|
98
|
+
"""`memory` (default) or `postgres`, from `CHECKPOINTER`."""
|
|
99
|
+
return (os.environ.get("CHECKPOINTER") or MEMORY).strip().lower()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def postgres_dsn() -> str:
|
|
103
|
+
dsn = os.environ.get("POSTGRES_DSN", "").strip()
|
|
104
|
+
if not dsn:
|
|
105
|
+
raise RuntimeError(
|
|
106
|
+
"CHECKPOINTER=postgres requires POSTGRES_DSN (from the Secret or the chart)."
|
|
107
|
+
)
|
|
108
|
+
return dsn
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _size(spec: tuple[str, int]) -> int:
|
|
112
|
+
name, default = spec
|
|
113
|
+
raw = (os.environ.get(name) or "").strip()
|
|
114
|
+
if not raw:
|
|
115
|
+
return default
|
|
116
|
+
try:
|
|
117
|
+
value = int(raw)
|
|
118
|
+
except ValueError:
|
|
119
|
+
raise SettingsError(f"{name}={raw!r} is not an integer.") from None
|
|
120
|
+
if value < 0:
|
|
121
|
+
raise SettingsError(f"{name}={value} must be >= 0.")
|
|
122
|
+
return value
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def pool_sizes() -> tuple[int, int]:
|
|
126
|
+
"""`(DB_POOL_MIN_SIZE, DB_POOL_MAX_SIZE)`, validated."""
|
|
127
|
+
low, high = _size(DB_POOL_MIN_SIZE), _size(DB_POOL_MAX_SIZE)
|
|
128
|
+
if high < 1 or low > high:
|
|
129
|
+
raise SettingsError(f"DB_POOL_MAX_SIZE={high} must be >= 1 and >= DB_POOL_MIN_SIZE={low}.")
|
|
130
|
+
return low, high
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def connection_kwargs(dsn: str) -> dict[str, str]:
|
|
134
|
+
"""The `CONNECTION_DEFAULTS` the DSN does not set itself.
|
|
135
|
+
|
|
136
|
+
Raises `SettingsError` for a DSN that does not parse: a typo is a
|
|
137
|
+
configuration error that stops startup, not an outage to wait out.
|
|
138
|
+
"""
|
|
139
|
+
from psycopg import ProgrammingError
|
|
140
|
+
from psycopg.conninfo import conninfo_to_dict
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
given = conninfo_to_dict(dsn)
|
|
144
|
+
except ProgrammingError as exc:
|
|
145
|
+
# libpq quotes the part it could not read, which can be the password:
|
|
146
|
+
# keep what went wrong, never the text itself.
|
|
147
|
+
reason = re.sub(r'"[^"]*"', '"..."', str(exc).splitlines()[0] if str(exc) else "")
|
|
148
|
+
raise SettingsError(
|
|
149
|
+
f"The database URL does not parse ({reason or 'invalid'}); "
|
|
150
|
+
"its text is not shown because it can hold the password."
|
|
151
|
+
) from None
|
|
152
|
+
defaults = dict(CONNECTION_DEFAULTS)
|
|
153
|
+
if os.environ.get("PGCONNECT_TIMEOUT"):
|
|
154
|
+
defaults.pop("connect_timeout")
|
|
155
|
+
return {k: v for k, v in defaults.items() if k not in given}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def is_connection_error(exc: BaseException) -> bool:
|
|
159
|
+
"""True for errors that mean the database cannot be reached (not a failed statement)."""
|
|
160
|
+
from psycopg import InterfaceError, OperationalError, errors
|
|
161
|
+
from psycopg_pool import PoolTimeout
|
|
162
|
+
|
|
163
|
+
if isinstance(exc, PoolTimeout | InterfaceError | OSError | TimeoutError):
|
|
164
|
+
return True
|
|
165
|
+
if isinstance(exc, errors.QueryCanceled | errors.TransactionRollback):
|
|
166
|
+
return False
|
|
167
|
+
return isinstance(exc, OperationalError)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class DbHealth:
|
|
171
|
+
"""Whether the database was reachable at last contact, logged once per change.
|
|
172
|
+
|
|
173
|
+
Marked down when the pool times out while its connection attempts fail,
|
|
174
|
+
when a pool reconnect cycle gives up, or when a connection breaks under a
|
|
175
|
+
query; marked up by every connection handed out or opened.
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
def __init__(self) -> None:
|
|
179
|
+
self.up = True
|
|
180
|
+
self.changed_at = time.monotonic()
|
|
181
|
+
self.last_error: str | None = None
|
|
182
|
+
|
|
183
|
+
def mark_up(self) -> None:
|
|
184
|
+
if not self.up:
|
|
185
|
+
self.up = True
|
|
186
|
+
self.changed_at = time.monotonic()
|
|
187
|
+
metrics.observe_database(True)
|
|
188
|
+
logger.info("database reachable again")
|
|
189
|
+
|
|
190
|
+
def mark_down(self, reason: BaseException | str) -> None:
|
|
191
|
+
text = reason if isinstance(reason, str) else f"{type(reason).__name__}: {reason}"
|
|
192
|
+
self.last_error = text.strip().splitlines()[0][:300] if text.strip() else text
|
|
193
|
+
if self.up:
|
|
194
|
+
self.up = False
|
|
195
|
+
self.changed_at = time.monotonic()
|
|
196
|
+
metrics.observe_database(False)
|
|
197
|
+
logger.warning("database unreachable: %s", self.last_error)
|
|
198
|
+
|
|
199
|
+
def checkout_timeout(self) -> float:
|
|
200
|
+
return POOL_TIMEOUT_S if self.up else DOWN_POOL_TIMEOUT_S
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
async def connect(dsn: str, *, health: DbHealth | None = None, **kwargs: Any) -> Any:
|
|
204
|
+
"""A direct autocommit connection with the `connection_kwargs` defaults.
|
|
205
|
+
|
|
206
|
+
The server-side keepalives are set best effort (a proxy may refuse them).
|
|
207
|
+
"""
|
|
208
|
+
import psycopg
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
conn = await psycopg.AsyncConnection.connect(
|
|
212
|
+
dsn, autocommit=True, **{**connection_kwargs(dsn), **kwargs}
|
|
213
|
+
)
|
|
214
|
+
except Exception as exc:
|
|
215
|
+
if health is not None and is_connection_error(exc):
|
|
216
|
+
health.mark_down(exc)
|
|
217
|
+
raise
|
|
218
|
+
await _server_keepalives(conn)
|
|
219
|
+
if health is not None:
|
|
220
|
+
health.mark_up()
|
|
221
|
+
return conn
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
async def _server_keepalives(conn: Any) -> None:
|
|
225
|
+
try:
|
|
226
|
+
await conn.execute(SERVER_KEEPALIVE_SQL)
|
|
227
|
+
except Exception as exc: # not fatal: the connection works without them
|
|
228
|
+
logger.debug("could not set the server-side TCP keepalives: %s", type(exc).__name__)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@cache
|
|
232
|
+
def _pool_class() -> type:
|
|
233
|
+
"""`AsyncConnectionPool` whose checkout timeout follows the database's health."""
|
|
234
|
+
from psycopg_pool import AsyncConnectionPool, PoolTimeout
|
|
235
|
+
|
|
236
|
+
class HealthAwarePool(AsyncConnectionPool):
|
|
237
|
+
health: DbHealth
|
|
238
|
+
|
|
239
|
+
async def getconn(self, timeout: float | None = None) -> Any:
|
|
240
|
+
if timeout is None:
|
|
241
|
+
timeout = self.health.checkout_timeout()
|
|
242
|
+
errors_before = self.get_stats().get("connections_errors", 0)
|
|
243
|
+
try:
|
|
244
|
+
conn = await super().getconn(timeout)
|
|
245
|
+
except PoolTimeout as exc:
|
|
246
|
+
# Connection attempts failed while this request waited: the
|
|
247
|
+
# database is down (a busy pool alone does not count).
|
|
248
|
+
if self.get_stats().get("connections_errors", 0) > errors_before:
|
|
249
|
+
self.health.mark_down(exc)
|
|
250
|
+
raise
|
|
251
|
+
self.health.mark_up()
|
|
252
|
+
return conn
|
|
253
|
+
|
|
254
|
+
return HealthAwarePool
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@asynccontextmanager
|
|
258
|
+
async def open_pool(dsn: str, health: DbHealth | None = None) -> AsyncIterator[Any]:
|
|
259
|
+
"""An open `AsyncConnectionPool` that health-checks every connection it hands out.
|
|
260
|
+
|
|
261
|
+
Opening does not wait for the database: a pool opened during an outage
|
|
262
|
+
connects once the database is back. A connection killed by a Postgres
|
|
263
|
+
restart or failover fails the check and is replaced instead of failing the
|
|
264
|
+
request that drew it. One dead connection usually means every idle one died
|
|
265
|
+
with it, so the first failed check also sweeps the whole pool
|
|
266
|
+
(`pool.check()`) in the background: the waiting request then gets a fresh
|
|
267
|
+
connection on its next try instead of drawing, and backing off from, each
|
|
268
|
+
dead one in turn.
|
|
269
|
+
"""
|
|
270
|
+
from psycopg.rows import dict_row
|
|
271
|
+
|
|
272
|
+
health = health if health is not None else DbHealth()
|
|
273
|
+
extra = connection_kwargs(dsn) # a DSN that does not parse stops here
|
|
274
|
+
holder: dict[str, Any] = {}
|
|
275
|
+
|
|
276
|
+
async def check(conn: Any) -> None:
|
|
277
|
+
from psycopg_pool import AsyncConnectionPool
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
await AsyncConnectionPool.check_connection(conn)
|
|
281
|
+
except Exception:
|
|
282
|
+
sweep = holder.get("sweep")
|
|
283
|
+
if holder.get("pool") is not None and (sweep is None or sweep.done()):
|
|
284
|
+
holder["sweep"] = asyncio.ensure_future(holder["pool"].check())
|
|
285
|
+
raise
|
|
286
|
+
|
|
287
|
+
async def configure(conn: Any) -> None:
|
|
288
|
+
await _server_keepalives(conn)
|
|
289
|
+
health.mark_up()
|
|
290
|
+
|
|
291
|
+
async def reconnect_failed(pool: Any) -> None:
|
|
292
|
+
health.mark_down(f"no connection for {RECONNECT_TIMEOUT_S:g} s")
|
|
293
|
+
|
|
294
|
+
low, high = pool_sizes()
|
|
295
|
+
pool = _pool_class()(
|
|
296
|
+
conninfo=dsn,
|
|
297
|
+
min_size=low,
|
|
298
|
+
max_size=high,
|
|
299
|
+
open=False,
|
|
300
|
+
check=check,
|
|
301
|
+
configure=configure,
|
|
302
|
+
reconnect_failed=reconnect_failed,
|
|
303
|
+
reconnect_timeout=RECONNECT_TIMEOUT_S,
|
|
304
|
+
timeout=POOL_TIMEOUT_S,
|
|
305
|
+
max_idle=POOL_MAX_IDLE_S,
|
|
306
|
+
kwargs={"autocommit": True, "row_factory": dict_row, "prepare_threshold": 0, **extra},
|
|
307
|
+
)
|
|
308
|
+
pool.health = health
|
|
309
|
+
holder["pool"] = pool
|
|
310
|
+
await pool.open()
|
|
311
|
+
try:
|
|
312
|
+
yield pool
|
|
313
|
+
finally:
|
|
314
|
+
holder.pop("pool", None)
|
|
315
|
+
sweep = holder.pop("sweep", None)
|
|
316
|
+
if sweep is not None and not sweep.done():
|
|
317
|
+
sweep.cancel()
|
|
318
|
+
await pool.close()
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@asynccontextmanager
|
|
322
|
+
async def schema_lock(
|
|
323
|
+
dsn: str, *, timeout: float = SCHEMA_LOCK_TIMEOUT_S, health: DbHealth | None = None
|
|
324
|
+
) -> AsyncIterator[None]:
|
|
325
|
+
"""Hold the schema advisory lock (on a connection of its own) for the block.
|
|
326
|
+
|
|
327
|
+
Waiting replicas poll with `pg_try_advisory_lock` instead of blocking in
|
|
328
|
+
`pg_advisory_lock`: a session blocked in a statement holds a snapshot, and
|
|
329
|
+
the saver's `CREATE INDEX CONCURRENTLY` (run by the lock holder) waits for
|
|
330
|
+
every such snapshot, so a blocking wait would deadlock the two.
|
|
331
|
+
"""
|
|
332
|
+
async with await connect(dsn, health=health) as conn:
|
|
333
|
+
loop = asyncio.get_running_loop()
|
|
334
|
+
deadline = loop.time() + timeout
|
|
335
|
+
while True:
|
|
336
|
+
cur = await conn.execute("SELECT pg_try_advisory_lock(%s)", (SCHEMA_LOCK_KEY,))
|
|
337
|
+
row = await cur.fetchone()
|
|
338
|
+
if row and row[0]:
|
|
339
|
+
break
|
|
340
|
+
if loop.time() >= deadline:
|
|
341
|
+
raise TimeoutError(
|
|
342
|
+
f"another replica held the schema setup lock for over {timeout:g} s"
|
|
343
|
+
)
|
|
344
|
+
await asyncio.sleep(SCHEMA_LOCK_POLL_S)
|
|
345
|
+
try:
|
|
346
|
+
yield
|
|
347
|
+
finally:
|
|
348
|
+
# Closing the connection releases the lock too; this just makes it prompt.
|
|
349
|
+
with suppress(Exception):
|
|
350
|
+
await conn.execute("SELECT pg_advisory_unlock(%s)", (SCHEMA_LOCK_KEY,))
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# A fence raises when this process may no longer write the thread's state
|
|
354
|
+
# (its run lease on the thread expired or was taken over).
|
|
355
|
+
Fence = Callable[[str], None]
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@cache
|
|
359
|
+
def _fenced_saver_class() -> type:
|
|
360
|
+
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
|
|
361
|
+
|
|
362
|
+
class FencedPostgresSaver(AsyncPostgresSaver):
|
|
363
|
+
"""`AsyncPostgresSaver` that asks the fence before every write.
|
|
364
|
+
|
|
365
|
+
A run whose lease on its thread could not be renewed (another replica
|
|
366
|
+
may have taken the thread over) then fails instead of writing a
|
|
367
|
+
checkpoint beside, or over, the new owner's.
|
|
368
|
+
"""
|
|
369
|
+
|
|
370
|
+
fence: Fence | None = None
|
|
371
|
+
|
|
372
|
+
def _check(self, config: Any) -> None:
|
|
373
|
+
if self.fence is not None:
|
|
374
|
+
self.fence(str(config["configurable"]["thread_id"]))
|
|
375
|
+
|
|
376
|
+
async def aput(self, config: Any, checkpoint: Any, metadata: Any, new_versions: Any) -> Any:
|
|
377
|
+
self._check(config)
|
|
378
|
+
return await super().aput(config, checkpoint, metadata, new_versions)
|
|
379
|
+
|
|
380
|
+
async def aput_writes(
|
|
381
|
+
self, config: Any, writes: Any, task_id: str, task_path: str = ""
|
|
382
|
+
) -> None:
|
|
383
|
+
self._check(config)
|
|
384
|
+
await super().aput_writes(config, writes, task_id, task_path)
|
|
385
|
+
|
|
386
|
+
async def adelete_thread(self, thread_id: str) -> None:
|
|
387
|
+
if self.fence is not None:
|
|
388
|
+
self.fence(str(thread_id))
|
|
389
|
+
await super().adelete_thread(thread_id)
|
|
390
|
+
|
|
391
|
+
return FencedPostgresSaver
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def postgres_saver(pool: Any, fence: Fence | None = None) -> Any:
|
|
395
|
+
"""An `AsyncPostgresSaver` on `pool` whose writes pass `fence(thread_id)` first.
|
|
396
|
+
|
|
397
|
+
Its schema is not set up here: call `saver.setup()` under `schema_lock`.
|
|
398
|
+
"""
|
|
399
|
+
saver = _fenced_saver_class()(pool)
|
|
400
|
+
saver.fence = fence
|
|
401
|
+
return saver
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@asynccontextmanager
|
|
405
|
+
async def get_checkpointer(pool: Any = None, fence: Fence | None = None) -> AsyncIterator[Any]:
|
|
406
|
+
"""Yield a ready checkpointer for the configured kind.
|
|
407
|
+
|
|
408
|
+
Postgres uses `pool` when given (the app's shared pool), else a pool of
|
|
409
|
+
its own, and runs `setup()` (idempotent) under the schema lock so the
|
|
410
|
+
checkpoint tables exist before the first run.
|
|
411
|
+
"""
|
|
412
|
+
kind = checkpointer_kind()
|
|
413
|
+
if kind == MEMORY:
|
|
414
|
+
from langgraph.checkpoint.memory import InMemorySaver
|
|
415
|
+
|
|
416
|
+
yield InMemorySaver()
|
|
417
|
+
return
|
|
418
|
+
if kind == POSTGRES:
|
|
419
|
+
dsn = postgres_dsn()
|
|
420
|
+
if pool is not None:
|
|
421
|
+
saver = postgres_saver(pool, fence)
|
|
422
|
+
async with schema_lock(dsn):
|
|
423
|
+
await saver.setup()
|
|
424
|
+
yield saver
|
|
425
|
+
return
|
|
426
|
+
async with open_pool(dsn) as own_pool:
|
|
427
|
+
saver = postgres_saver(own_pool, fence)
|
|
428
|
+
async with schema_lock(dsn):
|
|
429
|
+
await saver.setup()
|
|
430
|
+
yield saver
|
|
431
|
+
return
|
|
432
|
+
raise RuntimeError(f"Unknown CHECKPOINTER {kind!r}; expected 'memory' or 'postgres'.")
|