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,493 @@
|
|
|
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
|
+
"""App-owned thread ownership (one principal per thread) and per-thread run locks.
|
|
16
|
+
|
|
17
|
+
Under `fastapi` the `threads` table (`thread_id`, `principal_id`, `tenant`,
|
|
18
|
+
`actor`, `created_at`, `updated_at`) lives beside the library-owned checkpointer
|
|
19
|
+
schema; the ownership check runs before any checkpointer access, so a thread
|
|
20
|
+
id alone cannot cross a principal boundary. Claiming a new thread is one
|
|
21
|
+
atomic insert-if-absent, so two principals racing for the same new id cannot
|
|
22
|
+
both win. Under `langgraph-server` the same rule is applied by `chat.py` to
|
|
23
|
+
the thread metadata `{principal_id, tenant}` the app writes at creation (the
|
|
24
|
+
server's own `@auth.on` filters in `auth.py` protect the native Threads/Runs
|
|
25
|
+
API called from outside; the app's loopback SDK calls bypass them).
|
|
26
|
+
|
|
27
|
+
Only the owner may continue (write to) or delete a thread. A thread an agent
|
|
28
|
+
started for a user (a delegated principal, `Principal.actor`) records that
|
|
29
|
+
actor: the user (a direct principal with the same subject) owns it as well,
|
|
30
|
+
and may read, continue and delete it; the agent reaches only the threads
|
|
31
|
+
started under its own subject and actor, never another agent's for the same
|
|
32
|
+
user. A thread without an actor (a direct one, or one created before 0.3) is
|
|
33
|
+
never an agent's. Roles listed in
|
|
34
|
+
`AUTH_READ_ACROSS_ROLES` may additionally *read* other principals' threads
|
|
35
|
+
(direct principals only: a delegated principal's roles never read across);
|
|
36
|
+
listing lists the caller's own threads unless the caller asks for every
|
|
37
|
+
principal's (`GET /threads?scope=all`, read-across roles only). Listed rows
|
|
38
|
+
carry the owner as the hashed principal id (`owner`), never the raw id.
|
|
39
|
+
|
|
40
|
+
Thread ids are one namespace shared by every caller. When a request names no
|
|
41
|
+
thread, the server generates a random id (a UUID4). A client that picks its
|
|
42
|
+
own ids must make them unguessable: an id another principal sent first is
|
|
43
|
+
theirs (403), so a predictable id can be claimed ahead of its intended user,
|
|
44
|
+
and a 403 tells a caller that the id exists.
|
|
45
|
+
|
|
46
|
+
Deleting a thread (`ThreadStore.delete`, which the owner's DELETE and the
|
|
47
|
+
retention purge both reach) tells the listeners in `DELETE_LISTENERS` (the
|
|
48
|
+
A2A task store drops that conversation's tasks). An approval's outcome (a
|
|
49
|
+
decision, over A2A or HTTP, and the run it resumed; an expiry) is told to the
|
|
50
|
+
listeners in `A2A_TASK_LISTENERS` (`approval_outcome`): the A2A tasks still
|
|
51
|
+
waiting on that approval follow it.
|
|
52
|
+
|
|
53
|
+
One run per thread (`ThreadLocks`, `ThreadBusy`: HTTP 409 `{"code":
|
|
54
|
+
"thread_busy"}`) lives in `run_locks.py`: in-process locks plus Postgres
|
|
55
|
+
leases that every replica honours; the names are re-exported here.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
from __future__ import annotations
|
|
59
|
+
|
|
60
|
+
import logging
|
|
61
|
+
import os
|
|
62
|
+
from collections.abc import Awaitable, Callable, Mapping
|
|
63
|
+
from dataclasses import dataclass, field
|
|
64
|
+
from datetime import datetime
|
|
65
|
+
from typing import Any
|
|
66
|
+
|
|
67
|
+
from fastapi import HTTPException
|
|
68
|
+
|
|
69
|
+
from {{cookiecutter.agent_directory}}.app_utils.auth import Principal
|
|
70
|
+
from {{cookiecutter.agent_directory}}.app_utils.db import Database, utcnow_iso
|
|
71
|
+
from {{cookiecutter.agent_directory}}.app_utils.run_locks import (
|
|
72
|
+
THREAD_BUSY,
|
|
73
|
+
LeaseLost,
|
|
74
|
+
ThreadBusy,
|
|
75
|
+
ThreadLease,
|
|
76
|
+
ThreadLocks,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
__all__ = [
|
|
80
|
+
"A2A_TASK_LISTENERS",
|
|
81
|
+
"DELETE_LISTENERS",
|
|
82
|
+
"OUTCOME_COMPLETED",
|
|
83
|
+
"OUTCOME_FAILED",
|
|
84
|
+
"OUTCOME_INPUT_REQUIRED",
|
|
85
|
+
"SCOPES",
|
|
86
|
+
"SCOPE_ALL",
|
|
87
|
+
"SCOPE_OWN",
|
|
88
|
+
"THREAD_BUSY",
|
|
89
|
+
"ApprovalOutcome",
|
|
90
|
+
"LeaseLost",
|
|
91
|
+
"ThreadBusy",
|
|
92
|
+
"ThreadLease",
|
|
93
|
+
"ThreadLocks",
|
|
94
|
+
"ThreadRecord",
|
|
95
|
+
"ThreadStore",
|
|
96
|
+
"approval_outcome",
|
|
97
|
+
"check_scope",
|
|
98
|
+
"is_owner",
|
|
99
|
+
"own_view",
|
|
100
|
+
"owner_hash",
|
|
101
|
+
"owner_metadata_filter",
|
|
102
|
+
"search_server_threads",
|
|
103
|
+
"thread_deleted",
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
logger = logging.getLogger(__name__)
|
|
107
|
+
|
|
108
|
+
# `GET /threads?scope=`: the caller's threads (default), or every principal's
|
|
109
|
+
# (read-across roles only).
|
|
110
|
+
SCOPE_OWN = "own"
|
|
111
|
+
SCOPE_ALL = "all"
|
|
112
|
+
SCOPES = (SCOPE_OWN, SCOPE_ALL)
|
|
113
|
+
|
|
114
|
+
# Called with a thread id after the thread's row is deleted (best effort: a
|
|
115
|
+
# failing listener is logged and never fails the delete).
|
|
116
|
+
DELETE_LISTENERS: list[Callable[[str], Awaitable[None]]] = []
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# The states an A2A task waiting on an approval takes when the approval's outcome is known.
|
|
120
|
+
OUTCOME_COMPLETED = "completed"
|
|
121
|
+
OUTCOME_FAILED = "failed"
|
|
122
|
+
OUTCOME_INPUT_REQUIRED = "input-required"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass(frozen=True)
|
|
126
|
+
class ApprovalOutcome:
|
|
127
|
+
"""What became of an approval: decided (and the run it resumed ended so), or expired.
|
|
128
|
+
|
|
129
|
+
The A2A tasks on `thread_id` that still wait on it (`input-required`, their
|
|
130
|
+
approval request lists `approval_id` or the decision named them in
|
|
131
|
+
`references`) and belong to its requester (`requester_hash`, the hashed
|
|
132
|
+
subject, and `requester_actor`, "" for a direct one) take `state`
|
|
133
|
+
(`completed`, `failed` or `input-required` with the new pending
|
|
134
|
+
`approvals`) and say `text`. `continued_in` is the A2A task that carried
|
|
135
|
+
the decision (never itself updated). `answer` is the structured answer of
|
|
136
|
+
a completed run (a project with a response schema), which they take whole
|
|
137
|
+
as a `response` artifact.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
thread_id: str
|
|
141
|
+
approval_id: str
|
|
142
|
+
requester_hash: str
|
|
143
|
+
requester_actor: str
|
|
144
|
+
state: str
|
|
145
|
+
text: str
|
|
146
|
+
approvals: tuple[dict[str, Any], ...] = ()
|
|
147
|
+
references: tuple[str, ...] = ()
|
|
148
|
+
continued_in: str | None = None
|
|
149
|
+
answer: Any = None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# Called with an `ApprovalOutcome` (best effort: a failing listener is logged and
|
|
153
|
+
# never fails the decision, the run or the sweep).
|
|
154
|
+
A2A_TASK_LISTENERS: list[Callable[[ApprovalOutcome], Awaitable[None]]] = []
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass
|
|
158
|
+
class ThreadRecord:
|
|
159
|
+
thread_id: str
|
|
160
|
+
principal_id: str
|
|
161
|
+
tenant: str | None = None
|
|
162
|
+
created_at: str = field(default_factory=utcnow_iso)
|
|
163
|
+
updated_at: str | None = None
|
|
164
|
+
# The agent that started the thread for `principal_id` (a delegated principal's
|
|
165
|
+
# actor); "" for a direct thread.
|
|
166
|
+
actor: str = ""
|
|
167
|
+
|
|
168
|
+
def __post_init__(self) -> None:
|
|
169
|
+
if self.updated_at is None:
|
|
170
|
+
self.updated_at = self.created_at
|
|
171
|
+
|
|
172
|
+
def public(self) -> dict[str, Any]:
|
|
173
|
+
"""What `GET /threads` returns; the owner hashed (the raw id may be an email address)."""
|
|
174
|
+
return {
|
|
175
|
+
"thread_id": self.thread_id,
|
|
176
|
+
"owner": owner_hash(self.principal_id),
|
|
177
|
+
"created_at": self.created_at,
|
|
178
|
+
"updated_at": self.updated_at,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def owner_hash(principal_id: str | None) -> str | None:
|
|
183
|
+
"""A thread owner's id as logs and listings show it (`Principal.hashed_id()`)."""
|
|
184
|
+
return Principal(id=principal_id).hashed_id() if principal_id else None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def read_across_roles() -> set[str]:
|
|
188
|
+
"""Roles allowed to read other principals' threads (`AUTH_READ_ACROSS_ROLES`)."""
|
|
189
|
+
raw = os.environ.get("AUTH_READ_ACROSS_ROLES", "")
|
|
190
|
+
return {r.strip() for r in raw.split(",") if r.strip()}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def reads_across(principal: Principal) -> bool:
|
|
194
|
+
"""A direct principal holding a read-across role (a delegated one never reads across)."""
|
|
195
|
+
return not principal.delegated and bool(set(principal.roles) & read_across_roles())
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def is_owner(principal: Principal, record: ThreadRecord) -> bool:
|
|
199
|
+
"""The thread's subject, directly; or the agent that started it, for that subject.
|
|
200
|
+
|
|
201
|
+
A direct principal owns every thread of its subject, those its agents
|
|
202
|
+
started included. A delegated one owns only the threads started under its
|
|
203
|
+
exact subject and actor.
|
|
204
|
+
"""
|
|
205
|
+
if record.principal_id != principal.id:
|
|
206
|
+
return False
|
|
207
|
+
return principal.actor is None or (record.actor or "") == principal.actor.id
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def can_access(principal: Principal, record: ThreadRecord) -> bool:
|
|
211
|
+
"""Owner, or a principal holding one of the read-across roles."""
|
|
212
|
+
return is_owner(principal, record) or reads_across(principal)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def assert_access(principal: Principal, record: ThreadRecord) -> None:
|
|
216
|
+
"""Read access: the owner or a read-across role."""
|
|
217
|
+
if not can_access(principal, record):
|
|
218
|
+
raise HTTPException(status_code=403, detail="This thread belongs to another principal.")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def assert_owner(principal: Principal, record: ThreadRecord) -> None:
|
|
222
|
+
"""Write access (continue, delete): the owner only; read-across roles are read-only."""
|
|
223
|
+
if not is_owner(principal, record):
|
|
224
|
+
raise HTTPException(status_code=403, detail="This thread belongs to another principal.")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def check_scope(principal: Principal, scope: str) -> str:
|
|
228
|
+
"""`scope` of a thread listing, or 403 for `all` without a read-across role (422 if unknown)."""
|
|
229
|
+
if scope not in SCOPES:
|
|
230
|
+
raise HTTPException(status_code=422, detail="scope must be 'own' or 'all'.")
|
|
231
|
+
if scope == SCOPE_ALL and not reads_across(principal):
|
|
232
|
+
raise HTTPException(
|
|
233
|
+
status_code=403,
|
|
234
|
+
detail="Listing every principal's threads needs a role in AUTH_READ_ACROSS_ROLES.",
|
|
235
|
+
)
|
|
236
|
+
return scope
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def own_view(principal: Principal) -> Principal:
|
|
240
|
+
"""`principal` without its read-across roles: what it lists as its own threads."""
|
|
241
|
+
across = read_across_roles()
|
|
242
|
+
return Principal(
|
|
243
|
+
id=principal.id,
|
|
244
|
+
roles=[r for r in principal.roles if r not in across],
|
|
245
|
+
permissions=set(principal.permissions),
|
|
246
|
+
attributes=principal.public_attributes(),
|
|
247
|
+
actor=principal.actor,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _iso(value: Any) -> str | None:
|
|
252
|
+
if value is None:
|
|
253
|
+
return None
|
|
254
|
+
return value.isoformat() if isinstance(value, datetime) else str(value)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _record_from_row(row: dict[str, Any]) -> ThreadRecord:
|
|
258
|
+
return ThreadRecord(
|
|
259
|
+
thread_id=row["thread_id"],
|
|
260
|
+
principal_id=row["principal_id"],
|
|
261
|
+
tenant=row.get("tenant"),
|
|
262
|
+
created_at=_iso(row.get("created_at")) or utcnow_iso(),
|
|
263
|
+
updated_at=_iso(row.get("updated_at")),
|
|
264
|
+
actor=row.get("actor") or "",
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class ThreadStore:
|
|
269
|
+
"""`threads` table under postgres, an in-process dict under memory."""
|
|
270
|
+
|
|
271
|
+
def __init__(self, db: Database) -> None:
|
|
272
|
+
self.db = db
|
|
273
|
+
self._memory: dict[str, ThreadRecord] = {}
|
|
274
|
+
|
|
275
|
+
async def get(self, thread_id: str) -> ThreadRecord | None:
|
|
276
|
+
if not self.db.is_postgres:
|
|
277
|
+
return self._memory.get(thread_id)
|
|
278
|
+
row = await self.db.fetchone("SELECT * FROM threads WHERE thread_id = %s", (thread_id,))
|
|
279
|
+
return _record_from_row(row) if row is not None else None
|
|
280
|
+
|
|
281
|
+
async def claim(self, thread_id: str, principal: Principal) -> tuple[ThreadRecord, bool]:
|
|
282
|
+
"""The thread's record, created for `principal` if absent; `True` when this call created it.
|
|
283
|
+
|
|
284
|
+
One atomic insert-if-absent: when two principals claim the same new
|
|
285
|
+
id at once, exactly one gets `created=True` and the other reads the
|
|
286
|
+
winner's record (and is refused by the ownership check).
|
|
287
|
+
"""
|
|
288
|
+
record = ThreadRecord(
|
|
289
|
+
thread_id=thread_id,
|
|
290
|
+
principal_id=principal.id,
|
|
291
|
+
tenant=principal.public_attributes().get("tenant"),
|
|
292
|
+
actor=principal.actor.id if principal.actor is not None else "",
|
|
293
|
+
)
|
|
294
|
+
if not self.db.is_postgres:
|
|
295
|
+
existing = self._memory.setdefault(thread_id, record)
|
|
296
|
+
return existing, existing is record
|
|
297
|
+
for _ in range(3):
|
|
298
|
+
row = await self.db.fetchone(
|
|
299
|
+
"""
|
|
300
|
+
INSERT INTO threads (thread_id, principal_id, tenant, actor, created_at,
|
|
301
|
+
updated_at)
|
|
302
|
+
VALUES (%s, %s, %s, %s, %s, %s) ON CONFLICT (thread_id) DO NOTHING
|
|
303
|
+
RETURNING *
|
|
304
|
+
""",
|
|
305
|
+
(
|
|
306
|
+
record.thread_id,
|
|
307
|
+
record.principal_id,
|
|
308
|
+
record.tenant,
|
|
309
|
+
record.actor,
|
|
310
|
+
record.created_at,
|
|
311
|
+
record.created_at,
|
|
312
|
+
),
|
|
313
|
+
)
|
|
314
|
+
if row is not None:
|
|
315
|
+
return _record_from_row(row), True
|
|
316
|
+
existing = await self.get(thread_id)
|
|
317
|
+
if existing is not None:
|
|
318
|
+
return existing, False
|
|
319
|
+
# Deleted between the conflict and the read: claim it again.
|
|
320
|
+
raise RuntimeError(f"could not claim thread {thread_id!r}")
|
|
321
|
+
|
|
322
|
+
async def create(self, thread_id: str, principal: Principal) -> ThreadRecord:
|
|
323
|
+
record, _created = await self.claim(thread_id, principal)
|
|
324
|
+
return record
|
|
325
|
+
|
|
326
|
+
async def ensure(
|
|
327
|
+
self,
|
|
328
|
+
thread_id: str,
|
|
329
|
+
principal: Principal,
|
|
330
|
+
*,
|
|
331
|
+
has_state: Callable[[str], Awaitable[bool]] | None = None,
|
|
332
|
+
) -> ThreadRecord:
|
|
333
|
+
"""Return the thread, creating it for `principal` when it does not exist.
|
|
334
|
+
|
|
335
|
+
This is the write path (`chat.send`): raises 403 when the thread exists
|
|
336
|
+
and belongs to another principal, even for a read-across role. A
|
|
337
|
+
continued thread's `updated_at` moves forward (retention counts idle time).
|
|
338
|
+
|
|
339
|
+
`has_state(thread_id)` tells whether the checkpointer holds state for
|
|
340
|
+
the id. An id with state but no owner row is never claimed (403): its
|
|
341
|
+
state belongs to whoever had it, not to the next caller. Rows are
|
|
342
|
+
removed only after their state, under the run lock, so such an id
|
|
343
|
+
should never exist; this is the guard if one does.
|
|
344
|
+
"""
|
|
345
|
+
record = await self.get(thread_id)
|
|
346
|
+
if record is None:
|
|
347
|
+
if has_state is not None and await has_state(thread_id):
|
|
348
|
+
logger.warning(
|
|
349
|
+
"thread has state but no owner; refusing to give it a new owner",
|
|
350
|
+
extra={"thread_id": thread_id},
|
|
351
|
+
)
|
|
352
|
+
raise HTTPException(
|
|
353
|
+
status_code=403, detail="This thread belongs to another principal."
|
|
354
|
+
)
|
|
355
|
+
record, created = await self.claim(thread_id, principal)
|
|
356
|
+
if created:
|
|
357
|
+
return record
|
|
358
|
+
assert_owner(principal, record)
|
|
359
|
+
await self.touch(thread_id)
|
|
360
|
+
return record
|
|
361
|
+
|
|
362
|
+
async def touch(self, thread_id: str) -> None:
|
|
363
|
+
now = utcnow_iso()
|
|
364
|
+
if not self.db.is_postgres:
|
|
365
|
+
if thread_id in self._memory:
|
|
366
|
+
self._memory[thread_id].updated_at = now
|
|
367
|
+
return
|
|
368
|
+
await self.db.execute(
|
|
369
|
+
"UPDATE threads SET updated_at = %s WHERE thread_id = %s", (now, thread_id)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
async def list_for(
|
|
373
|
+
self, principal: Principal, *, limit: int = 1000, offset: int = 0, scope: str | None = None
|
|
374
|
+
) -> list[ThreadRecord]:
|
|
375
|
+
"""Threads `principal` may read, most recent first.
|
|
376
|
+
|
|
377
|
+
`scope="own"`: its own threads only (a read-across role included);
|
|
378
|
+
`scope="all"`, or no scope: every principal's for a read-across role,
|
|
379
|
+
its own for anyone else. (`GET /threads` checks the scope and passes
|
|
380
|
+
the caller without its read-across roles for its own threads.) A
|
|
381
|
+
direct principal's own threads are every thread of its subject; a
|
|
382
|
+
delegated one's only those of its subject and actor.
|
|
383
|
+
"""
|
|
384
|
+
if scope == SCOPE_OWN:
|
|
385
|
+
principal = own_view(principal)
|
|
386
|
+
if not self.db.is_postgres:
|
|
387
|
+
records = sorted(
|
|
388
|
+
(r for r in self._memory.values() if can_access(principal, r)),
|
|
389
|
+
key=lambda r: (r.updated_at or "", r.thread_id),
|
|
390
|
+
reverse=True,
|
|
391
|
+
)
|
|
392
|
+
return records[offset : offset + limit]
|
|
393
|
+
if reads_across(principal):
|
|
394
|
+
rows = await self.db.fetchall(
|
|
395
|
+
"SELECT * FROM threads ORDER BY updated_at DESC, thread_id DESC LIMIT %s OFFSET %s",
|
|
396
|
+
(limit, offset),
|
|
397
|
+
)
|
|
398
|
+
elif principal.actor is not None:
|
|
399
|
+
rows = await self.db.fetchall(
|
|
400
|
+
"SELECT * FROM threads WHERE principal_id = %s AND actor = %s "
|
|
401
|
+
"ORDER BY updated_at DESC, thread_id DESC LIMIT %s OFFSET %s",
|
|
402
|
+
(principal.id, principal.actor.id, limit, offset),
|
|
403
|
+
)
|
|
404
|
+
else:
|
|
405
|
+
rows = await self.db.fetchall(
|
|
406
|
+
"SELECT * FROM threads WHERE principal_id = %s "
|
|
407
|
+
"ORDER BY updated_at DESC, thread_id DESC LIMIT %s OFFSET %s",
|
|
408
|
+
(principal.id, limit, offset),
|
|
409
|
+
)
|
|
410
|
+
return [_record_from_row(r) for r in rows]
|
|
411
|
+
|
|
412
|
+
async def idle_before(self, cutoff_iso: str, *, limit: int = 500) -> list[str]:
|
|
413
|
+
"""Ids of threads not continued since `cutoff_iso` (oldest first)."""
|
|
414
|
+
if not self.db.is_postgres:
|
|
415
|
+
idle = sorted(
|
|
416
|
+
(r for r in self._memory.values() if (r.updated_at or r.created_at) < cutoff_iso),
|
|
417
|
+
key=lambda r: r.updated_at or r.created_at,
|
|
418
|
+
)
|
|
419
|
+
return [r.thread_id for r in idle[:limit]]
|
|
420
|
+
rows = await self.db.fetchall(
|
|
421
|
+
"SELECT thread_id FROM threads WHERE updated_at < %s ORDER BY updated_at LIMIT %s",
|
|
422
|
+
(cutoff_iso, limit),
|
|
423
|
+
)
|
|
424
|
+
return [r["thread_id"] for r in rows]
|
|
425
|
+
|
|
426
|
+
async def delete(self, thread_id: str) -> None:
|
|
427
|
+
if not self.db.is_postgres:
|
|
428
|
+
self._memory.pop(thread_id, None)
|
|
429
|
+
else:
|
|
430
|
+
await self.db.execute("DELETE FROM threads WHERE thread_id = %s", (thread_id,))
|
|
431
|
+
await thread_deleted(thread_id)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def owner_metadata_filter(principal: Principal) -> dict[str, Any]:
|
|
435
|
+
"""langgraph-server: the thread-metadata filter of a principal's own threads.
|
|
436
|
+
|
|
437
|
+
A containment filter: the subject's (direct) matches every thread stamped
|
|
438
|
+
with it, its agents' included; a delegated principal's names its actor
|
|
439
|
+
too, which a direct thread (no `actor` key) never matches.
|
|
440
|
+
"""
|
|
441
|
+
if principal.actor is None:
|
|
442
|
+
return {"principal_id": principal.id}
|
|
443
|
+
return {"principal_id": principal.id, "actor": principal.actor.id}
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
async def thread_deleted(thread_id: str) -> None:
|
|
447
|
+
"""Tell every `DELETE_LISTENERS` entry that `thread_id` is gone; never raises."""
|
|
448
|
+
for listener in list(DELETE_LISTENERS):
|
|
449
|
+
try:
|
|
450
|
+
await listener(thread_id)
|
|
451
|
+
except Exception:
|
|
452
|
+
logger.warning("a thread-delete listener failed", exc_info=True)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
async def approval_outcome(outcome: ApprovalOutcome) -> None:
|
|
456
|
+
"""Tell every `A2A_TASK_LISTENERS` entry what became of an approval; never raises."""
|
|
457
|
+
for listener in list(A2A_TASK_LISTENERS):
|
|
458
|
+
try:
|
|
459
|
+
await listener(outcome)
|
|
460
|
+
except Exception:
|
|
461
|
+
logger.warning("an approval-outcome listener failed", exc_info=True)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
async def search_server_threads(
|
|
465
|
+
client: Any, principal: Principal, *, scope: str, limit: int, offset: int
|
|
466
|
+
) -> list[dict[str, Any]]:
|
|
467
|
+
"""langgraph-server: a `GET /threads` page from the server's thread search.
|
|
468
|
+
|
|
469
|
+
`scope="own"` filters on the owner the app wrote into the thread's
|
|
470
|
+
metadata at creation; `scope="all"` (checked by `check_scope`) lists
|
|
471
|
+
every thread. Rows carry the owner hashed, like `ThreadRecord.public()`.
|
|
472
|
+
"""
|
|
473
|
+
filters: dict[str, Any] = {}
|
|
474
|
+
if scope != SCOPE_ALL:
|
|
475
|
+
filters["metadata"] = owner_metadata_filter(principal)
|
|
476
|
+
threads = await client.threads.search(
|
|
477
|
+
limit=limit, offset=offset, sort_by="updated_at", sort_order="desc", **filters
|
|
478
|
+
)
|
|
479
|
+
rows: list[dict[str, Any]] = []
|
|
480
|
+
for thread in threads:
|
|
481
|
+
if not isinstance(thread, Mapping):
|
|
482
|
+
continue
|
|
483
|
+
metadata = thread.get("metadata") if isinstance(thread.get("metadata"), Mapping) else {}
|
|
484
|
+
owner = metadata.get("principal_id")
|
|
485
|
+
rows.append(
|
|
486
|
+
{
|
|
487
|
+
"thread_id": str(thread.get("thread_id")),
|
|
488
|
+
"owner": owner_hash(owner if isinstance(owner, str) else None),
|
|
489
|
+
"created_at": _iso(thread.get("created_at")),
|
|
490
|
+
"updated_at": _iso(thread.get("updated_at")),
|
|
491
|
+
}
|
|
492
|
+
)
|
|
493
|
+
return rows
|