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,215 @@
|
|
|
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
|
+
"""``system deploy``: ``graph-agents-cli deploy --env ENV`` in every project, callees first.
|
|
16
|
+
|
|
17
|
+
The projects deploy in waves: a wave holds the agents whose callees are all
|
|
18
|
+
deployed, so a caller's first card check finds its peers up (a cycle is broken
|
|
19
|
+
at its first agent in file order, with a warning). At most ``--parallel``
|
|
20
|
+
deploys run at once (default ``deploy.parallel`` of the file, else 3): round 1
|
|
21
|
+
of the experiments deployed seven agents at once and the hub took 158 s where
|
|
22
|
+
27-38 s was usual. A failed wave stops the run unless ``--keep-going``. Each
|
|
23
|
+
deploy is a child process in its project's directory with no terminal input,
|
|
24
|
+
its output kept in a log file; the per-agent build, image load and rollout
|
|
25
|
+
times are read from the commands it prints.
|
|
26
|
+
|
|
27
|
+
Outside ``dev`` every project must record its kube context
|
|
28
|
+
(``environments.<env>.context``): a deploy would otherwise ask to confirm the
|
|
29
|
+
kubeconfig's current context, and ``system deploy`` never answers that for it.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import os
|
|
35
|
+
import subprocess
|
|
36
|
+
import sys
|
|
37
|
+
import tempfile
|
|
38
|
+
import threading
|
|
39
|
+
import time
|
|
40
|
+
from collections.abc import Callable
|
|
41
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
42
|
+
from dataclasses import dataclass, field
|
|
43
|
+
from pathlib import Path
|
|
44
|
+
|
|
45
|
+
from graph_agents_cli.system._system import Node, System
|
|
46
|
+
|
|
47
|
+
BUILD = "build"
|
|
48
|
+
LOAD = "load"
|
|
49
|
+
ROLL = "roll"
|
|
50
|
+
# The commands `deploy` echoes (`▸ cmd` or `[dry-run] cmd`) that start each phase.
|
|
51
|
+
PHASE_MARKERS: tuple[tuple[str, tuple[str, ...]], ...] = (
|
|
52
|
+
(BUILD, ("docker build",)),
|
|
53
|
+
(
|
|
54
|
+
LOAD,
|
|
55
|
+
("kind load", "k3d image import", "minikube image load", "k3s ctr", "docker push"),
|
|
56
|
+
),
|
|
57
|
+
(ROLL, ("helm upgrade", "helm template")),
|
|
58
|
+
)
|
|
59
|
+
TAIL_LINES = 15
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class Outcome:
|
|
64
|
+
"""One project's deploy."""
|
|
65
|
+
|
|
66
|
+
agent: str
|
|
67
|
+
exit_code: int
|
|
68
|
+
seconds: float
|
|
69
|
+
phases: dict[str, float] = field(default_factory=dict)
|
|
70
|
+
log: Path | None = None
|
|
71
|
+
tail: list[str] = field(default_factory=list)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def ok(self) -> bool:
|
|
75
|
+
return self.exit_code == 0
|
|
76
|
+
|
|
77
|
+
def describe(self) -> str:
|
|
78
|
+
phases = ", ".join(f"{name} {secs:.1f} s" for name, secs in self.phases.items())
|
|
79
|
+
timing = f"{self.seconds:.1f} s" + (f" ({phases})" if phases else "")
|
|
80
|
+
if self.ok:
|
|
81
|
+
return f"{self.agent}: deployed in {timing}"
|
|
82
|
+
return f"{self.agent}: FAILED (exit {self.exit_code}) after {timing}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# (node, argv after `graph-agents-cli`, log path) -> (exit code, [(seconds since start, line)])
|
|
86
|
+
Runner = Callable[[Node, list[str], Path], tuple[int, list[tuple[float, str]]]]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def run_cli(node: Node, args: list[str], log: Path) -> tuple[int, list[tuple[float, str]]]:
|
|
90
|
+
"""Run ``graph-agents-cli <args>`` in ``node``'s project; every line timed and logged."""
|
|
91
|
+
command = [sys.executable, "-m", "graph_agents_cli.main", *args]
|
|
92
|
+
env = {**os.environ, "PYTHONUNBUFFERED": "1"}
|
|
93
|
+
start = time.monotonic()
|
|
94
|
+
lines: list[tuple[float, str]] = []
|
|
95
|
+
with log.open("w", encoding="utf-8") as sink:
|
|
96
|
+
process = subprocess.Popen(
|
|
97
|
+
command,
|
|
98
|
+
cwd=node.root,
|
|
99
|
+
env=env,
|
|
100
|
+
stdin=subprocess.DEVNULL,
|
|
101
|
+
stdout=subprocess.PIPE,
|
|
102
|
+
stderr=subprocess.STDOUT,
|
|
103
|
+
text=True,
|
|
104
|
+
encoding="utf-8",
|
|
105
|
+
errors="replace",
|
|
106
|
+
)
|
|
107
|
+
assert process.stdout is not None
|
|
108
|
+
for line in process.stdout:
|
|
109
|
+
lines.append((time.monotonic() - start, line.rstrip("\n")))
|
|
110
|
+
sink.write(line)
|
|
111
|
+
sink.flush()
|
|
112
|
+
code = process.wait()
|
|
113
|
+
return code, lines
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def phases(lines: list[tuple[float, str]], total: float) -> dict[str, float]:
|
|
117
|
+
"""How long each phase took, from the first line that starts it to the next phase."""
|
|
118
|
+
starts: dict[str, float] = {}
|
|
119
|
+
for seconds, line in lines:
|
|
120
|
+
for phase, markers in PHASE_MARKERS:
|
|
121
|
+
if phase not in starts and any(marker in line for marker in markers):
|
|
122
|
+
starts[phase] = seconds
|
|
123
|
+
ordered = sorted(starts.items(), key=lambda item: item[1])
|
|
124
|
+
result = {}
|
|
125
|
+
for index, (phase, begin) in enumerate(ordered):
|
|
126
|
+
end = ordered[index + 1][1] if index + 1 < len(ordered) else total
|
|
127
|
+
result[phase] = max(0.0, end - begin)
|
|
128
|
+
return {phase: result[phase] for phase, _ in PHASE_MARKERS if phase in result}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def waves(system: System, names: list[str]) -> tuple[list[list[str]], list[str]]:
|
|
132
|
+
"""The deploy waves of ``names`` (file order within a wave), and the cycle warnings."""
|
|
133
|
+
selected = [n for n in system.nodes if n in names]
|
|
134
|
+
callees = {
|
|
135
|
+
n: {lk.callee.name for lk in system.calls(system.nodes[n])} & set(selected)
|
|
136
|
+
for n in selected
|
|
137
|
+
}
|
|
138
|
+
done: set[str] = set()
|
|
139
|
+
remaining = list(selected)
|
|
140
|
+
result: list[list[str]] = []
|
|
141
|
+
warnings: list[str] = []
|
|
142
|
+
while remaining:
|
|
143
|
+
wave = [n for n in remaining if callees[n] <= done]
|
|
144
|
+
if not wave:
|
|
145
|
+
first = remaining[0]
|
|
146
|
+
waiting = sorted(callees[first] - done)
|
|
147
|
+
warnings.append(
|
|
148
|
+
f"a cycle: {first} is deployed before {', '.join(waiting)}, which it calls "
|
|
149
|
+
"(its first card checks may fail until they are up)"
|
|
150
|
+
)
|
|
151
|
+
wave = [first]
|
|
152
|
+
result.append(wave)
|
|
153
|
+
done.update(wave)
|
|
154
|
+
remaining = [n for n in remaining if n not in done]
|
|
155
|
+
return result, warnings
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def deploy(
|
|
159
|
+
system: System,
|
|
160
|
+
env: str,
|
|
161
|
+
names: list[str],
|
|
162
|
+
*,
|
|
163
|
+
parallel: int,
|
|
164
|
+
keep_going: bool,
|
|
165
|
+
extra: list[str],
|
|
166
|
+
runner: Runner | None = None,
|
|
167
|
+
report: Callable[[str], None] = print,
|
|
168
|
+
log_dir: Path | None = None,
|
|
169
|
+
) -> list[Outcome]:
|
|
170
|
+
"""Deploy ``names`` wave by wave; the outcome of each deploy that ran (``runner``: how a
|
|
171
|
+
deploy runs, ``run_cli`` by default)."""
|
|
172
|
+
run = runner or run_cli
|
|
173
|
+
plan, warnings = waves(system, names)
|
|
174
|
+
for warning in warnings:
|
|
175
|
+
report(f"Warning: {warning}")
|
|
176
|
+
logs = log_dir or Path(tempfile.mkdtemp(prefix=f"gac-system-deploy-{env}-"))
|
|
177
|
+
report(f"Logs: {logs}")
|
|
178
|
+
outcomes: list[Outcome] = []
|
|
179
|
+
lock = threading.Lock()
|
|
180
|
+
|
|
181
|
+
def one(name: str) -> Outcome:
|
|
182
|
+
node = system.nodes[name]
|
|
183
|
+
log = logs / f"{name}.log"
|
|
184
|
+
start = time.monotonic()
|
|
185
|
+
try:
|
|
186
|
+
code, lines = run(node, ["deploy", "--env", env, *extra], log)
|
|
187
|
+
except OSError as exc:
|
|
188
|
+
code, lines = 2, [(0.0, f"could not run graph-agents-cli deploy: {exc}")]
|
|
189
|
+
total = time.monotonic() - start
|
|
190
|
+
outcome = Outcome(
|
|
191
|
+
agent=name,
|
|
192
|
+
exit_code=code,
|
|
193
|
+
seconds=total,
|
|
194
|
+
phases=phases(lines, total),
|
|
195
|
+
log=log,
|
|
196
|
+
tail=[line for _, line in lines[-TAIL_LINES:]],
|
|
197
|
+
)
|
|
198
|
+
with lock:
|
|
199
|
+
report(outcome.describe())
|
|
200
|
+
if not outcome.ok:
|
|
201
|
+
for line in outcome.tail:
|
|
202
|
+
report(f" {line}")
|
|
203
|
+
report(f" log: {log}")
|
|
204
|
+
return outcome
|
|
205
|
+
|
|
206
|
+
for number, wave in enumerate(plan, start=1):
|
|
207
|
+
report(f"Wave {number}/{len(plan)}: {', '.join(wave)}")
|
|
208
|
+
with ThreadPoolExecutor(max_workers=max(1, parallel)) as pool:
|
|
209
|
+
results = list(pool.map(one, wave))
|
|
210
|
+
outcomes.extend(results)
|
|
211
|
+
if any(not r.ok for r in results) and not keep_going and number < len(plan):
|
|
212
|
+
skipped = [n for later in plan[number:] for n in later]
|
|
213
|
+
report(f"Stopped after wave {number}; not deployed: {', '.join(skipped)}")
|
|
214
|
+
break
|
|
215
|
+
return outcomes
|
|
@@ -0,0 +1,363 @@
|
|
|
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
|
+
"""``graph-agents-system.yaml``: the agents of a system, their projects and who calls whom.
|
|
16
|
+
|
|
17
|
+
The file is optional: every project stays self-contained (``peer add`` alone is
|
|
18
|
+
complete). It names each agent's project directory (relative to the file), the
|
|
19
|
+
client id other agents see it as (its actor id), and the agents it calls; the
|
|
20
|
+
environments the agents run in; the issuer that exchanges users' tokens; a
|
|
21
|
+
shared database's connection budget; and how many projects ``system deploy``
|
|
22
|
+
deploys at once. The models below are the schema: ``load`` validates a file with
|
|
23
|
+
them, and ``uv run python -m graph_agents_cli.system._model`` rewrites the
|
|
24
|
+
published JSON Schema (``schemas/graph-agents-system.schema.json``, which a test
|
|
25
|
+
keeps equal to the models).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Annotated, Any, Literal
|
|
33
|
+
|
|
34
|
+
import click
|
|
35
|
+
import yaml
|
|
36
|
+
from pydantic import (
|
|
37
|
+
BaseModel,
|
|
38
|
+
ConfigDict,
|
|
39
|
+
Field,
|
|
40
|
+
StringConstraints,
|
|
41
|
+
ValidationError,
|
|
42
|
+
model_validator,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
SYSTEM_FILENAME = "graph-agents-system.yaml"
|
|
46
|
+
EXIT_INVALID = 3
|
|
47
|
+
|
|
48
|
+
# An agent's name is the peer name its callers give it (`peer add NAME`), so it follows
|
|
49
|
+
# that rule: the model picks peers by it, and the API is `<name>_agent`.
|
|
50
|
+
AGENT_NAME_PATTERN = r"^[a-z][a-z0-9_]{0,25}$"
|
|
51
|
+
# A client id at the issuer: the actor id callees list in AUTH_ALLOWED_ACTORS (a comma
|
|
52
|
+
# list), so no comma, whitespace or control character.
|
|
53
|
+
CLIENT_ID_PATTERN = r"^[^\s,\x00-\x1f\x7f]{1,256}$"
|
|
54
|
+
# RFC 6749 scope tokens separated by single spaces.
|
|
55
|
+
SCOPE_PATTERN = r"^[\x21\x23-\x5b\x5d-\x7e]+(?: [\x21\x23-\x5b\x5d-\x7e]+)*$"
|
|
56
|
+
ENV_NAME_PATTERN = r"^[a-z][a-z0-9-]{0,30}$"
|
|
57
|
+
# The placeholders an environment's url template may use.
|
|
58
|
+
URL_PLACEHOLDERS = ("{agent}", "{project}", "{env}")
|
|
59
|
+
|
|
60
|
+
AgentName = Annotated[str, StringConstraints(pattern=AGENT_NAME_PATTERN)]
|
|
61
|
+
EnvName = Annotated[str, StringConstraints(pattern=ENV_NAME_PATTERN)]
|
|
62
|
+
|
|
63
|
+
CALL_KINDS = ("ask", "status", "cancel")
|
|
64
|
+
RELAY = "relay"
|
|
65
|
+
DENY = "deny"
|
|
66
|
+
DEFAULT_PARALLEL = 3
|
|
67
|
+
MAX_PARALLEL = 16
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class _Strict(BaseModel):
|
|
71
|
+
model_config = ConfigDict(extra="forbid")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Edge(_Strict):
|
|
75
|
+
"""One agent this agent calls, with what `peer add` would be told about it."""
|
|
76
|
+
|
|
77
|
+
agent: AgentName = Field(description="The agent called (a key of `agents`).")
|
|
78
|
+
approvals: Literal["relay", "deny"] = Field(
|
|
79
|
+
default=RELAY,
|
|
80
|
+
description=(
|
|
81
|
+
"relay: the person's decisions on the called agent's pending approvals go through "
|
|
82
|
+
"a gate here (requester); deny: this agent never sends them."
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
auth: Literal["exchange", "forward", "bearer"] | None = Field(
|
|
86
|
+
default=None,
|
|
87
|
+
description=(
|
|
88
|
+
"How the caller authenticates; default by its auth policy (jwt: exchange, custom: "
|
|
89
|
+
"forward, shared-bearer: bearer)."
|
|
90
|
+
),
|
|
91
|
+
)
|
|
92
|
+
scope: Annotated[str, StringConstraints(pattern=SCOPE_PATTERN)] | None = Field(
|
|
93
|
+
default=None, description="exchange: the scopes asked for (least privilege)."
|
|
94
|
+
)
|
|
95
|
+
allow_actorless: bool = Field(
|
|
96
|
+
default=False,
|
|
97
|
+
description=(
|
|
98
|
+
"exchange: accept exchanged tokens that name no actor (the called agent then lists "
|
|
99
|
+
"client:<client_id> in AUTH_ALLOWED_ACTORS)."
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
calls: list[Literal["ask", "status", "cancel"]] | None = Field(
|
|
103
|
+
default=None,
|
|
104
|
+
description="What the caller may send: ask (SendMessage), status (GetTask), cancel.",
|
|
105
|
+
)
|
|
106
|
+
description: Annotated[str, StringConstraints(max_length=300)] | None = Field(
|
|
107
|
+
default=None,
|
|
108
|
+
description=(
|
|
109
|
+
"What the called agent does, for the caller's model (default: the called agent's "
|
|
110
|
+
"A2A_DESCRIPTION)."
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
@model_validator(mode="after")
|
|
115
|
+
def _consistent(self) -> Edge:
|
|
116
|
+
if self.calls is not None and "ask" not in self.calls:
|
|
117
|
+
raise ValueError("calls must include ask")
|
|
118
|
+
if self.auth not in (None, "exchange") and (self.scope or self.allow_actorless):
|
|
119
|
+
raise ValueError("scope and allow_actorless go with auth: exchange")
|
|
120
|
+
return self
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class Agent(_Strict):
|
|
124
|
+
"""One agent of the system: a graph-agents-cli project."""
|
|
125
|
+
|
|
126
|
+
project: Annotated[str, StringConstraints(min_length=1)] = Field(
|
|
127
|
+
description="The project directory, relative to this file."
|
|
128
|
+
)
|
|
129
|
+
client_id: Annotated[str, StringConstraints(pattern=CLIENT_ID_PATTERN)] | None = Field(
|
|
130
|
+
default=None,
|
|
131
|
+
description=(
|
|
132
|
+
"The client id this agent exchanges tokens with at the issuer "
|
|
133
|
+
"(TOKEN_EXCHANGE_CLIENT_ID; default: the agent's name)."
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
actor_id: Annotated[str, StringConstraints(pattern=CLIENT_ID_PATTERN)] | None = Field(
|
|
137
|
+
default=None,
|
|
138
|
+
description=(
|
|
139
|
+
"The actor id the agents it calls see: the act.sub the issuer puts in this "
|
|
140
|
+
"agent's exchanged tokens, which their AUTH_ALLOWED_ACTORS and --relayers name "
|
|
141
|
+
"(default: client_id). Set it when the issuer names the client differently there."
|
|
142
|
+
),
|
|
143
|
+
)
|
|
144
|
+
calls: list[AgentName | Edge] = Field(
|
|
145
|
+
default_factory=list, description="The agents this agent calls (names, or edges)."
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class Identity(_Strict):
|
|
150
|
+
"""The issuer that exchanges users' tokens (RFC 8693) for the `auth: exchange` edges."""
|
|
151
|
+
|
|
152
|
+
issuer: Annotated[str, StringConstraints(min_length=1)] = Field(
|
|
153
|
+
description="Compared with each called agent's AUTH_JWT_ISSUER."
|
|
154
|
+
)
|
|
155
|
+
token_url: str | dict[EnvName, str] | None = Field(
|
|
156
|
+
default=None,
|
|
157
|
+
description=(
|
|
158
|
+
"The issuer's token endpoint (TOKEN_EXCHANGE_URL), one for every environment or one "
|
|
159
|
+
"per environment."
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class Environment(_Strict):
|
|
165
|
+
"""Where the agents run, and so the URL each is called at."""
|
|
166
|
+
|
|
167
|
+
port_base: Annotated[int, Field(ge=1024, le=65000)] | None = Field(
|
|
168
|
+
default=None,
|
|
169
|
+
description=(
|
|
170
|
+
"Local processes: the agent at position i of `agents` listens on "
|
|
171
|
+
"http://127.0.0.1:<port_base + i>."
|
|
172
|
+
),
|
|
173
|
+
)
|
|
174
|
+
url: Annotated[str, StringConstraints(min_length=1)] | None = Field(
|
|
175
|
+
default=None,
|
|
176
|
+
description=(
|
|
177
|
+
"Each agent's URL, from a template with {agent} (and optionally {project}, {env}), "
|
|
178
|
+
"e.g. https://{agent}.agents.example.com. Without url or port_base, the in-cluster "
|
|
179
|
+
"URL of each project's chart and manifest namespace."
|
|
180
|
+
),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
@model_validator(mode="after")
|
|
184
|
+
def _one_kind(self) -> Environment:
|
|
185
|
+
if self.port_base is not None and self.url is not None:
|
|
186
|
+
raise ValueError("give port_base (local processes) or url, not both")
|
|
187
|
+
if self.url is not None and "{agent}" not in self.url and "{project}" not in self.url:
|
|
188
|
+
raise ValueError("url must hold {agent} or {project}: every agent needs its own URL")
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class Database(_Strict):
|
|
193
|
+
"""A database server the agents share (enables check SC10)."""
|
|
194
|
+
|
|
195
|
+
max_connections: dict[EnvName, Annotated[int, Field(ge=1)]] = Field(
|
|
196
|
+
default_factory=dict,
|
|
197
|
+
description="The server's max_connections, per environment.",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class Deploy(_Strict):
|
|
202
|
+
"""How `system deploy` runs."""
|
|
203
|
+
|
|
204
|
+
parallel: Annotated[int, Field(ge=1, le=MAX_PARALLEL)] = Field(
|
|
205
|
+
default=DEFAULT_PARALLEL,
|
|
206
|
+
description="How many projects deploy at once (default 3).",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class SystemFile(_Strict):
|
|
211
|
+
"""A graph-agents-cli system: agent projects that call each other."""
|
|
212
|
+
|
|
213
|
+
version: Literal[1] = Field(description="The format version: 1.")
|
|
214
|
+
name: Annotated[str, StringConstraints(pattern=r"^[A-Za-z0-9][A-Za-z0-9._-]{0,62}$")] = Field(
|
|
215
|
+
description="The system's name."
|
|
216
|
+
)
|
|
217
|
+
agents: dict[AgentName, Agent] = Field(
|
|
218
|
+
min_length=1, description="The agents, by name (the name their callers give them)."
|
|
219
|
+
)
|
|
220
|
+
identity: Identity | None = Field(
|
|
221
|
+
default=None, description="Required when an edge uses auth: exchange."
|
|
222
|
+
)
|
|
223
|
+
environments: dict[EnvName, Environment | None] = Field(
|
|
224
|
+
default_factory=dict, description="The environments the agents run in."
|
|
225
|
+
)
|
|
226
|
+
database: Database | None = None
|
|
227
|
+
deploy: Deploy = Field(default_factory=Deploy)
|
|
228
|
+
|
|
229
|
+
def edges(self, name: str) -> list[Edge]:
|
|
230
|
+
"""The edges of agent ``name``, a bare name read as an edge with the defaults."""
|
|
231
|
+
return [
|
|
232
|
+
call if isinstance(call, Edge) else Edge(agent=call) for call in self.agents[name].calls
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
def environment(self, name: str) -> Environment:
|
|
236
|
+
return self.environments.get(name) or Environment()
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class SystemFileError(click.ClickException):
|
|
240
|
+
"""The system file is unreadable or invalid, or names an unknown project (exit 3)."""
|
|
241
|
+
|
|
242
|
+
exit_code = EXIT_INVALID
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def find(start: Path | None = None) -> Path | None:
|
|
246
|
+
"""``graph-agents-system.yaml`` in ``start`` (the working directory) or a parent."""
|
|
247
|
+
here = (start or Path.cwd()).resolve()
|
|
248
|
+
for directory in [here, *here.parents]:
|
|
249
|
+
candidate = directory / SYSTEM_FILENAME
|
|
250
|
+
if candidate.is_file():
|
|
251
|
+
return candidate
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _where(loc: tuple[Any, ...]) -> str:
|
|
256
|
+
parts: list[str] = []
|
|
257
|
+
for part in loc:
|
|
258
|
+
if isinstance(part, int):
|
|
259
|
+
parts.append(f"[{part}]")
|
|
260
|
+
elif part == "[key]":
|
|
261
|
+
parts.append(" (the name)")
|
|
262
|
+
elif part in ("str", "Edge", "dict[str,str]") or str(part).startswith(
|
|
263
|
+
("function-", "constrained-str", "literal[")
|
|
264
|
+
):
|
|
265
|
+
continue # a union branch, not a place in the file
|
|
266
|
+
else:
|
|
267
|
+
parts.append(("." if parts else "") + str(part))
|
|
268
|
+
return "".join(parts) or "(the file)"
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# A value that may be a name or an edge (a mapping) fails both branches of the union; only
|
|
272
|
+
# the branch of the value's own kind says anything useful.
|
|
273
|
+
_OTHER_BRANCH = {
|
|
274
|
+
dict: ("Input should be a valid string",),
|
|
275
|
+
str: ("Input should be a valid dictionary",),
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def validation_errors(data: Any) -> list[str]:
|
|
280
|
+
"""The schema errors of a parsed file (empty when valid)."""
|
|
281
|
+
try:
|
|
282
|
+
SystemFile.model_validate(data)
|
|
283
|
+
except ValidationError as exc:
|
|
284
|
+
seen: list[str] = []
|
|
285
|
+
for error in exc.errors():
|
|
286
|
+
noise = _OTHER_BRANCH.get(type(error.get("input")), ())
|
|
287
|
+
if any(str(error["msg"]).startswith(prefix) for prefix in noise):
|
|
288
|
+
continue
|
|
289
|
+
message = str(error["msg"]).removeprefix("Value error, ")
|
|
290
|
+
line = f"{_where(tuple(error['loc']))}: {message}"
|
|
291
|
+
if line not in seen:
|
|
292
|
+
seen.append(line)
|
|
293
|
+
return seen
|
|
294
|
+
return []
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def load(path: Path) -> SystemFile:
|
|
298
|
+
"""Read and validate a system file; ``SystemFileError`` (exit 3) when it cannot be used."""
|
|
299
|
+
from graph_agents_cli._api_policy import PolicyLoader
|
|
300
|
+
|
|
301
|
+
try:
|
|
302
|
+
text = path.read_text(encoding="utf-8")
|
|
303
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
304
|
+
raise SystemFileError(f"cannot read {path}: {exc}") from None
|
|
305
|
+
try:
|
|
306
|
+
data = yaml.load(text, Loader=PolicyLoader) # refuses a repeated key
|
|
307
|
+
except yaml.YAMLError as exc:
|
|
308
|
+
raise SystemFileError(f"{path} is not valid YAML: {exc}") from None
|
|
309
|
+
errors = validation_errors(data)
|
|
310
|
+
if errors:
|
|
311
|
+
lines = "\n".join(f" - {error}" for error in errors)
|
|
312
|
+
raise SystemFileError(f"{path} is invalid:\n{lines}")
|
|
313
|
+
return SystemFile.model_validate(data)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# ---------------------------------------------------------------------------
|
|
317
|
+
# The published JSON Schema
|
|
318
|
+
# ---------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
SCHEMA_FILE = "graph-agents-system.schema.json"
|
|
321
|
+
# Repo-only, like the extension schema: for editors and the published contract.
|
|
322
|
+
# <repo>/src/graph_agents_cli/system/_model.py -> <repo>/schemas/<file>
|
|
323
|
+
SCHEMA_PATH = Path(__file__).resolve().parents[3] / "schemas" / SCHEMA_FILE
|
|
324
|
+
SCHEMA_ID = f"https://raw.githubusercontent.com/ss7172/graph-agents-cli/main/schemas/{SCHEMA_FILE}"
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _closed(node: Any) -> None:
|
|
328
|
+
"""Mappings keyed by a pattern (agent and environment names) take no other key, as the
|
|
329
|
+
models refuse one (pydantic writes only ``patternProperties``)."""
|
|
330
|
+
if isinstance(node, dict):
|
|
331
|
+
if "patternProperties" in node and "additionalProperties" not in node:
|
|
332
|
+
node["additionalProperties"] = False
|
|
333
|
+
for value in node.values():
|
|
334
|
+
_closed(value)
|
|
335
|
+
elif isinstance(node, list):
|
|
336
|
+
for value in node:
|
|
337
|
+
_closed(value)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def build_json_schema() -> dict[str, Any]:
|
|
341
|
+
"""The file's schema as a JSON Schema 2020-12 document."""
|
|
342
|
+
schema = SystemFile.model_json_schema(by_alias=True)
|
|
343
|
+
_closed(schema)
|
|
344
|
+
return {
|
|
345
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
346
|
+
"$id": SCHEMA_ID,
|
|
347
|
+
**schema,
|
|
348
|
+
"title": "graph-agents-cli system (graph-agents-system.yaml, version 1)",
|
|
349
|
+
"description": (
|
|
350
|
+
"Agent projects that call each other over A2A, for `graph-agents-cli system`. "
|
|
351
|
+
"Generated from the CLI's own models: edit those, not this file."
|
|
352
|
+
),
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def render() -> str:
|
|
357
|
+
"""The schema as it is written to disk (trailing newline included)."""
|
|
358
|
+
return json.dumps(build_json_schema(), indent=2, sort_keys=False) + "\n"
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
if __name__ == "__main__":
|
|
362
|
+
SCHEMA_PATH.write_text(render(), encoding="utf-8")
|
|
363
|
+
print(f"wrote {SCHEMA_PATH}")
|