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.
Files changed (291) hide show
  1. graph_agents_cli/__init__.py +26 -0
  2. graph_agents_cli/_api_policy.py +2145 -0
  3. graph_agents_cli/_approvals.py +400 -0
  4. graph_agents_cli/_build.py +186 -0
  5. graph_agents_cli/_build_info.json +7 -0
  6. graph_agents_cli/_chat_client.py +462 -0
  7. graph_agents_cli/_click.py +157 -0
  8. graph_agents_cli/_defaults.py +139 -0
  9. graph_agents_cli/_experiments.py +64 -0
  10. graph_agents_cli/_http.py +192 -0
  11. graph_agents_cli/_output.py +83 -0
  12. graph_agents_cli/_project.py +462 -0
  13. graph_agents_cli/_remote.py +220 -0
  14. graph_agents_cli/_response_schema.py +264 -0
  15. graph_agents_cli/_runner.py +319 -0
  16. graph_agents_cli/_skills_check.py +274 -0
  17. graph_agents_cli/_tools.py +189 -0
  18. graph_agents_cli/_trust.py +66 -0
  19. graph_agents_cli/api/__init__.py +15 -0
  20. graph_agents_cli/api/_changes.py +506 -0
  21. graph_agents_cli/api/_files.py +658 -0
  22. graph_agents_cli/api/cmd_api.py +2480 -0
  23. graph_agents_cli/deploy/__init__.py +15 -0
  24. graph_agents_cli/deploy/_config.py +171 -0
  25. graph_agents_cli/deploy/_image.py +128 -0
  26. graph_agents_cli/deploy/_kube.py +286 -0
  27. graph_agents_cli/deploy/_modes.py +234 -0
  28. graph_agents_cli/deploy/_preflight.py +370 -0
  29. graph_agents_cli/deploy/_values.py +168 -0
  30. graph_agents_cli/deploy/cmd_deploy.py +1866 -0
  31. graph_agents_cli/deploy/gitops.py +562 -0
  32. graph_agents_cli/deploy/local_load.py +273 -0
  33. graph_agents_cli/dev/__init__.py +13 -0
  34. graph_agents_cli/dev/cmd_build.py +131 -0
  35. graph_agents_cli/dev/cmd_install.py +78 -0
  36. graph_agents_cli/dev/cmd_lint.py +119 -0
  37. graph_agents_cli/dev/cmd_playground.py +297 -0
  38. graph_agents_cli/dev/policy_check.py +1287 -0
  39. graph_agents_cli/eval/__init__.py +22 -0
  40. graph_agents_cli/eval/_client.py +670 -0
  41. graph_agents_cli/eval/_common.py +177 -0
  42. graph_agents_cli/eval/_judge.py +168 -0
  43. graph_agents_cli/eval/_judge_runner.py +238 -0
  44. graph_agents_cli/eval/_paths.py +212 -0
  45. graph_agents_cli/eval/checks.py +581 -0
  46. graph_agents_cli/eval/cmd_analyze.py +278 -0
  47. graph_agents_cli/eval/cmd_compare.py +284 -0
  48. graph_agents_cli/eval/cmd_eval_group.py +80 -0
  49. graph_agents_cli/eval/cmd_generate.py +558 -0
  50. graph_agents_cli/eval/cmd_grade.py +466 -0
  51. graph_agents_cli/eval/cmd_metric.py +156 -0
  52. graph_agents_cli/eval/cmd_run.py +370 -0
  53. graph_agents_cli/eval/cmd_submit.py +400 -0
  54. graph_agents_cli/eval/config.py +435 -0
  55. graph_agents_cli/eval/dataset.py +350 -0
  56. graph_agents_cli/eval/gate.py +420 -0
  57. graph_agents_cli/eval/transcript.py +192 -0
  58. graph_agents_cli/extension/__init__.py +13 -0
  59. graph_agents_cli/extension/_compat.py +86 -0
  60. graph_agents_cli/extension/_loader.py +293 -0
  61. graph_agents_cli/extension/_manifest.py +135 -0
  62. graph_agents_cli/extension/_overrides.py +195 -0
  63. graph_agents_cli/extension/_paths.py +91 -0
  64. graph_agents_cli/extension/_refs.py +193 -0
  65. graph_agents_cli/extension/_resolver.py +453 -0
  66. graph_agents_cli/extension/_schema.py +106 -0
  67. graph_agents_cli/extension/_spec.py +253 -0
  68. graph_agents_cli/extension/_sync.py +102 -0
  69. graph_agents_cli/extension/_trust.py +58 -0
  70. graph_agents_cli/extension/cmd_extension_add.py +259 -0
  71. graph_agents_cli/extension/cmd_extension_group.py +57 -0
  72. graph_agents_cli/extension/cmd_extension_list.py +56 -0
  73. graph_agents_cli/extension/cmd_extension_remove.py +61 -0
  74. graph_agents_cli/extension/cmd_extension_update.py +195 -0
  75. graph_agents_cli/info/__init__.py +13 -0
  76. graph_agents_cli/info/cmd_info.py +222 -0
  77. graph_agents_cli/infra/__init__.py +15 -0
  78. graph_agents_cli/infra/checks.py +1169 -0
  79. graph_agents_cli/infra/cmd_infra.py +103 -0
  80. graph_agents_cli/main.py +591 -0
  81. graph_agents_cli/peer/__init__.py +15 -0
  82. graph_agents_cli/peer/_generate.py +254 -0
  83. graph_agents_cli/peer/cmd_peer.py +1151 -0
  84. graph_agents_cli/run/__init__.py +13 -0
  85. graph_agents_cli/run/_local_server.py +1157 -0
  86. graph_agents_cli/run/_signals.py +141 -0
  87. graph_agents_cli/run/cmd_approvals.py +530 -0
  88. graph_agents_cli/run/cmd_run.py +1421 -0
  89. graph_agents_cli/scaffold/__init__.py +19 -0
  90. graph_agents_cli/scaffold/agents/README.md +24 -0
  91. graph_agents_cli/scaffold/agents/empty_py/.template/templateconfig.yaml +22 -0
  92. graph_agents_cli/scaffold/agents/langgraph/.env.example +292 -0
  93. graph_agents_cli/scaffold/agents/langgraph/.template/templateconfig.yaml +28 -0
  94. graph_agents_cli/scaffold/agents/langgraph/Dockerfile +59 -0
  95. graph_agents_cli/scaffold/agents/langgraph/Dockerfile.langgraph-server +59 -0
  96. graph_agents_cli/scaffold/agents/langgraph/README.md +571 -0
  97. graph_agents_cli/scaffold/agents/langgraph/api-policy.yaml +60 -0
  98. graph_agents_cli/scaffold/agents/langgraph/app/__init__.py +20 -0
  99. graph_agents_cli/scaffold/agents/langgraph/app/agent.py +174 -0
  100. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/__init__.py +15 -0
  101. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/a2a.py +2162 -0
  102. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/a2a_client.py +1167 -0
  103. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/api_client.py +4220 -0
  104. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/approvals.py +1349 -0
  105. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/auth.py +1986 -0
  106. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/chat.py +2962 -0
  107. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/checkpointer.py +432 -0
  108. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/content.py +569 -0
  109. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/db.py +580 -0
  110. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/limits.py +203 -0
  111. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/metrics.py +231 -0
  112. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/middleware.py +361 -0
  113. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/model.py +611 -0
  114. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/playground.py +230 -0
  115. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/run_locks.py +459 -0
  116. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/structured.py +755 -0
  117. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/telemetry.py +681 -0
  118. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/threads.py +493 -0
  119. graph_agents_cli/scaffold/agents/langgraph/app/app_utils/token_exchange.py +959 -0
  120. graph_agents_cli/scaffold/agents/langgraph/app/fast_api_app.py +770 -0
  121. graph_agents_cli/scaffold/agents/langgraph/app/policies/__init__.py +55 -0
  122. graph_agents_cli/scaffold/agents/langgraph/app/policies/custom.py +97 -0
  123. graph_agents_cli/scaffold/agents/langgraph/app/tools/__init__.py +46 -0
  124. graph_agents_cli/scaffold/agents/langgraph/app/tools/example_api.py +92 -0
  125. graph_agents_cli/scaffold/agents/langgraph/app/tools/weather.py +33 -0
  126. graph_agents_cli/scaffold/agents/langgraph/langgraph.json +14 -0
  127. graph_agents_cli/scaffold/agents/langgraph/pyproject.toml +78 -0
  128. graph_agents_cli/scaffold/agents/langgraph/tests/conftest.py +376 -0
  129. graph_agents_cli/scaffold/agents/langgraph/tests/eval/datasets/basic-dataset.json +53 -0
  130. graph_agents_cli/scaffold/agents/langgraph/tests/eval/eval_config.yaml +32 -0
  131. graph_agents_cli/scaffold/agents/langgraph/tests/integration/approval_graph.py +137 -0
  132. graph_agents_cli/scaffold/agents/langgraph/tests/integration/fake_issuer.py +216 -0
  133. graph_agents_cli/scaffold/agents/langgraph/tests/integration/fake_openai.py +357 -0
  134. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_a2a_outcomes.py +569 -0
  135. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_a2a_relay.py +479 -0
  136. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_api_surface.py +812 -0
  137. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_approvals.py +1367 -0
  138. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_approvals_server.py +794 -0
  139. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_cross_actor.py +497 -0
  140. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_cross_actor_server.py +247 -0
  141. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_history_repair.py +278 -0
  142. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_model_apis.py +242 -0
  143. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_postgres.py +637 -0
  144. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_resilience_postgres.py +770 -0
  145. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_runtime_guardrails.py +854 -0
  146. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_server_e2e.py +340 -0
  147. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_server_runtime.py +989 -0
  148. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_structured_answers.py +584 -0
  149. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_structured_server.py +222 -0
  150. graph_agents_cli/scaffold/agents/langgraph/tests/integration/test_token_exchange_issuer.py +650 -0
  151. graph_agents_cli/scaffold/agents/langgraph/tests/load_test/.results/.placeholder +0 -0
  152. graph_agents_cli/scaffold/agents/langgraph/tests/load_test/README.md +22 -0
  153. graph_agents_cli/scaffold/agents/langgraph/tests/load_test/conftest.py +21 -0
  154. graph_agents_cli/scaffold/agents/langgraph/tests/load_test/load_test.py +81 -0
  155. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_a2a_client.py +824 -0
  156. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_a2a_scoping.py +724 -0
  157. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_client.py +1214 -0
  158. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_client_hardening.py +716 -0
  159. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_api_policy_rpc.py +767 -0
  160. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_approval_ledger.py +1536 -0
  161. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_fake_model.py +115 -0
  162. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_jwt_policy.py +991 -0
  163. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_limits.py +310 -0
  164. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_logging.py +148 -0
  165. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_logging_hardening.py +271 -0
  166. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_policy.py +378 -0
  167. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_resilience.py +610 -0
  168. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_server_auth.py +702 -0
  169. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_structured.py +673 -0
  170. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_telemetry.py +404 -0
  171. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_thread_listing.py +255 -0
  172. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_threads.py +268 -0
  173. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_token_exchange.py +1320 -0
  174. graph_agents_cli/scaffold/agents/langgraph/tests/unit/test_untrusted_content.py +393 -0
  175. graph_agents_cli/scaffold/agents/langgraph/uv-fastapi.lock +2084 -0
  176. graph_agents_cli/scaffold/agents/langgraph/uv-langgraph-server.lock +2106 -0
  177. graph_agents_cli/scaffold/agents/langgraph/{{cookiecutter.agent_guidance_filename}} +129 -0
  178. graph_agents_cli/scaffold/base_templates/_shared/graph-agents-cli-manifest.yaml +36 -0
  179. graph_agents_cli/scaffold/base_templates/python/.dockerignore +32 -0
  180. graph_agents_cli/scaffold/base_templates/python/.github/CODEOWNERS +30 -0
  181. graph_agents_cli/scaffold/base_templates/python/.github/agent.env +7 -0
  182. graph_agents_cli/scaffold/base_templates/python/.github/workflows/pr_checks.yaml +214 -0
  183. graph_agents_cli/scaffold/base_templates/python/.gitignore +209 -0
  184. graph_agents_cli/scaffold/base_templates/python/tests/unit/test_dummy.py +23 -0
  185. graph_agents_cli/scaffold/base_templates/python/{{cookiecutter.agent_guidance_filename}} +35 -0
  186. graph_agents_cli/scaffold/cmd_scaffold_group.py +49 -0
  187. graph_agents_cli/scaffold/commands/__init__.py +13 -0
  188. graph_agents_cli/scaffold/commands/create.py +1424 -0
  189. graph_agents_cli/scaffold/commands/enhance.py +1652 -0
  190. graph_agents_cli/scaffold/commands/upgrade.py +570 -0
  191. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/agent.env +12 -0
  192. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/workflows/promote-to-prod.yaml +371 -0
  193. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/.github/workflows/staging.yaml +450 -0
  194. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-dev.yaml +43 -0
  195. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-prod.yaml +41 -0
  196. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/argocd/application-staging.yaml +43 -0
  197. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/.helmignore +14 -0
  198. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/Chart.yaml +21 -0
  199. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/examples/networkpolicy.yaml +103 -0
  200. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/NOTES.txt +48 -0
  201. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/_helpers.tpl +189 -0
  202. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/certificate.yaml +15 -0
  203. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/configmap.yaml +10 -0
  204. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/deployment.yaml +199 -0
  205. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/hpa.yaml +22 -0
  206. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/httproute.yaml +30 -0
  207. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/ingress.yaml +39 -0
  208. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/networkpolicy.yaml +48 -0
  209. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/pdb.yaml +13 -0
  210. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/postgresql-secret.yaml +37 -0
  211. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/service.yaml +15 -0
  212. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/serviceaccount.yaml +13 -0
  213. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/templates/servicemonitor.yaml +42 -0
  214. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-dev.yaml +22 -0
  215. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-prod.yaml +45 -0
  216. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values-staging.yaml +29 -0
  217. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/deployment/helm/{{cookiecutter.project_name}}/values.yaml +396 -0
  218. graph_agents_cli/scaffold/deployment_targets/kubernetes/python/tests/integration/test_chart.py +269 -0
  219. graph_agents_cli/scaffold/deployment_targets/none/README.md +5 -0
  220. graph_agents_cli/scaffold/deployment_targets/none/python/README.md +6 -0
  221. graph_agents_cli/scaffold/utils/__init__.py +13 -0
  222. graph_agents_cli/scaffold/utils/backup.py +212 -0
  223. graph_agents_cli/scaffold/utils/build_record.py +257 -0
  224. graph_agents_cli/scaffold/utils/cli_options.py +184 -0
  225. graph_agents_cli/scaffold/utils/fs.py +83 -0
  226. graph_agents_cli/scaffold/utils/generate_locks.py +214 -0
  227. graph_agents_cli/scaffold/utils/generation_metadata.py +88 -0
  228. graph_agents_cli/scaffold/utils/keyedit.py +768 -0
  229. graph_agents_cli/scaffold/utils/keymerge.py +537 -0
  230. graph_agents_cli/scaffold/utils/language.py +138 -0
  231. graph_agents_cli/scaffold/utils/lock_utils.py +94 -0
  232. graph_agents_cli/scaffold/utils/logging.py +77 -0
  233. graph_agents_cli/scaffold/utils/manifest.py +292 -0
  234. graph_agents_cli/scaffold/utils/merge.py +970 -0
  235. graph_agents_cli/scaffold/utils/merge3.py +216 -0
  236. graph_agents_cli/scaffold/utils/openapi_seed.py +199 -0
  237. graph_agents_cli/scaffold/utils/remote_template.py +376 -0
  238. graph_agents_cli/scaffold/utils/template.py +1352 -0
  239. graph_agents_cli/scaffold/utils/upgrade.py +894 -0
  240. graph_agents_cli/scaffold/utils/version.py +438 -0
  241. graph_agents_cli/secrets/__init__.py +15 -0
  242. graph_agents_cli/secrets/_apply.py +954 -0
  243. graph_agents_cli/secrets/_required.py +188 -0
  244. graph_agents_cli/secrets/cmd_secrets.py +211 -0
  245. graph_agents_cli/setup/__init__.py +13 -0
  246. graph_agents_cli/setup/_antigravity.py +221 -0
  247. graph_agents_cli/setup/cmd_auth.py +1030 -0
  248. graph_agents_cli/setup/cmd_dev_token.py +513 -0
  249. graph_agents_cli/setup/cmd_setup.py +428 -0
  250. graph_agents_cli/setup/cmd_update.py +140 -0
  251. graph_agents_cli/skills/__init__.py +13 -0
  252. graph_agents_cli/skills/_bundle.py +65 -0
  253. graph_agents_cli/skills/data/README.md +19 -0
  254. graph_agents_cli/skills/data/graph-agents-cli-deploy/SKILL.md +357 -0
  255. graph_agents_cli/skills/data/graph-agents-cli-deploy/references/github-settings.md +113 -0
  256. graph_agents_cli/skills/data/graph-agents-cli-deploy/references/gitops.md +137 -0
  257. graph_agents_cli/skills/data/graph-agents-cli-deploy/references/kubernetes.md +315 -0
  258. graph_agents_cli/skills/data/graph-agents-cli-deploy/references/secrets.md +160 -0
  259. graph_agents_cli/skills/data/graph-agents-cli-eval/SKILL.md +303 -0
  260. graph_agents_cli/skills/data/graph-agents-cli-eval/references/dataset_schema.md +282 -0
  261. graph_agents_cli/skills/data/graph-agents-cli-eval/references/metrics-guide.md +143 -0
  262. graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/SKILL.md +659 -0
  263. graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/langchain-models.md +124 -0
  264. graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/langgraph.md +235 -0
  265. graph_agents_cli/skills/data/graph-agents-cli-langgraph-code/references/template-contract.md +477 -0
  266. graph_agents_cli/skills/data/graph-agents-cli-observability/SKILL.md +231 -0
  267. graph_agents_cli/skills/data/graph-agents-cli-observability/references/langsmith.md +46 -0
  268. graph_agents_cli/skills/data/graph-agents-cli-observability/references/otel.md +59 -0
  269. graph_agents_cli/skills/data/graph-agents-cli-scaffold/SKILL.md +414 -0
  270. graph_agents_cli/skills/data/graph-agents-cli-scaffold/references/flags.md +134 -0
  271. graph_agents_cli/skills/data/graph-agents-cli-workflow/SKILL.md +478 -0
  272. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/brainstorming.md +118 -0
  273. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/commands.md +419 -0
  274. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/extension.md +156 -0
  275. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/internals.md +67 -0
  276. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/spec-template.md +56 -0
  277. graph_agents_cli/skills/data/graph-agents-cli-workflow/references/terminology.md +119 -0
  278. graph_agents_cli/system/__init__.py +15 -0
  279. graph_agents_cli/system/_apply.py +519 -0
  280. graph_agents_cli/system/_checks.py +1023 -0
  281. graph_agents_cli/system/_deploy.py +215 -0
  282. graph_agents_cli/system/_model.py +363 -0
  283. graph_agents_cli/system/_system.py +664 -0
  284. graph_agents_cli/system/_views.py +208 -0
  285. graph_agents_cli/system/cmd_system.py +423 -0
  286. graph_agents_cli-0.3.1.dist-info/METADATA +162 -0
  287. graph_agents_cli-0.3.1.dist-info/RECORD +291 -0
  288. graph_agents_cli-0.3.1.dist-info/WHEEL +4 -0
  289. graph_agents_cli-0.3.1.dist-info/entry_points.txt +2 -0
  290. graph_agents_cli-0.3.1.dist-info/licenses/LICENSE +201 -0
  291. graph_agents_cli-0.3.1.dist-info/licenses/NOTICE +19 -0
@@ -0,0 +1,770 @@
1
+ # Copyright 2026 Google LLC
2
+ # Modifications Copyright 2026 graph-agents-cli contributors
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # https://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ """FastAPI application: `app` (the chat API, threads, health, metrics, playground and A2A).
17
+
18
+ Routes:
19
+ * `POST /chat` (SSE): `message.start`, `message.delta`, `tool.call`,
20
+ `tool.result`, `message.end`, `error`; `thread_id` continues a thread
21
+ (omit it to start one: the server generates a random id).
22
+ 409 `{"code": "thread_busy"}` while the thread has a run in progress, and
23
+ 409 `{"code": "approval_pending", "approvals": [...]}` while it waits for
24
+ the approval of a gated API call. A failed tool call's `tool.result`
25
+ carries an `error_id` and, outside `APP_ENV=dev`, a generic `result` (the
26
+ error text is for the model only). A run that pauses before a gated call
27
+ ends with `message.end` status `awaiting_approval`, with `approval` (and
28
+ `approvals`: every one the run waits for). With a response schema
29
+ (`app_utils/structured.py`), a completed run's `message.end` carries the
30
+ answer as `structured_response`, its only `message.delta` is the answer's
31
+ JSON text, and a run whose answer never fits ends with the `error` code
32
+ `invalid_structured_response`.
33
+ * `GET /threads/{thread_id}/approvals` (`approval.read`): the thread's
34
+ approvals (owner and read-across roles: all; a decider: the ones it may
35
+ decide). `GET /approvals?status=&limit=&offset=`: across threads, the
36
+ caller's own and the ones a role of theirs may decide (read-across: all).
37
+ * `POST /threads/{thread_id}/approvals/{approval_id}` (`approval.decide`),
38
+ body `{"decision": "approve" | "reject", "comment": "..."}`: 404, 403 for
39
+ a principal the approvers do not name, 409 `approval_not_pending`, 410
40
+ `approval_expired`, 409 `thread_busy`; else the resumed run streams with
41
+ the `/chat` events (`message.start` names `approval_id` and `decision`).
42
+ * `GET /threads`: the caller's threads, most recent first (`limit`, `offset`);
43
+ `scope=all` lists every principal's (read-across roles only). Each row
44
+ names its owner hashed (`owner`).
45
+ * `GET /threads/{thread_id}/messages`: ordered messages, ownership enforced.
46
+ * `DELETE /threads/{thread_id}`: the thread, its checkpoints, run records and
47
+ approvals (owner only), and the A2A tasks of that conversation. Under
48
+ langgraph-server this is the server's native route (owner-only through the
49
+ auth handler); once it succeeds, the app drops the thread's run records,
50
+ approvals and A2A tasks (`ThreadDeleteHookMiddleware`).
51
+ * `GET /health`: liveness, process only: `{"status", "runtime", "checkpointer"}`.
52
+ * `GET /ready`: readiness: 200 when the database answers within 2 s, else
53
+ 503 `{"status": "not_ready"}`.
54
+ * `GET /metrics`: Prometheus text (`METRICS_ENABLED`, default true); with
55
+ `METRICS_TOKEN` set, only for `Authorization: Bearer <METRICS_TOKEN>`.
56
+ * `GET /playground`: dev-only chat page (`APP_ENV=dev`).
57
+ * `GET /openapi.json`, `GET /docs`: dev-only as well (`APP_ENV=dev`); off otherwise.
58
+ * A2A: card at `/a2a/<agent_directory>/.well-known/agent-card.json`, JSON-RPC at `/a2a/<agent_directory>`.
59
+
60
+ Under the fastapi runtime the lifespan binds the checkpointer chosen by
61
+ `CHECKPOINTER` to the graph and opens the app tables. Under langgraph-server
62
+ the same file is mounted as the custom app (`langgraph.json` `http.app`),
63
+ the server owns persistence, and the routes proxy to it (see `app_utils/chat.py`).
64
+ Every route except `/health`, `/ready`, `/metrics`, `/playground` and the
65
+ dev-only docs passes through the policy of `app_utils/auth.py`: the routes
66
+ through the `require(action)` dependency, the A2A endpoints through the ASGI
67
+ middleware below. Under langgraph-server the server's own meta routes
68
+ (`/docs`, `/openapi.json`, `/info`, `/metrics`) come ahead of this app's
69
+ routes unless they are disabled (the server image sets `disable_meta`), and
70
+ the server's native API gets its auth errors from `AuthErrorMiddleware`.
71
+
72
+ Request limits: bodies over `MAX_REQUEST_BYTES` get 413, a message over
73
+ `MAX_MESSAGE_CHARS` (the same cap A2A applies) and `/chat` metadata outside
74
+ `MAX_METADATA_KEYS` / `MAX_METADATA_VALUE_CHARS` get 422 (see
75
+ `app_utils/limits.py`). `CORS_ALLOW_ORIGINS` (comma list; empty = no CORS)
76
+ enables CORS for those origins under fastapi; LangGraph Server reads the same
77
+ variable itself. Unhandled errors answer 500 with an `error_id` that names the
78
+ logged detail (and the request's `X-Request-ID`). A 422 never echoes the
79
+ request's values (`input`), so it cannot fail on text that is not valid
80
+ UTF-8 (an unpaired surrogate) or on the NaN or Infinity Python's JSON parser
81
+ lets through, and never returns a long message back to its sender.
82
+
83
+ `.env` is read (below the process environment) as this module is imported,
84
+ before any setting is: the A2A card, the docs switch, CORS and the auth
85
+ policy's startup check are fixed at import. Under the fastapi runtime
86
+ logging is configured on import too, so import-time warnings and uvicorn's
87
+ startup lines follow `LOG_FORMAT`.
88
+
89
+ No ``from __future__ import annotations`` here: LangGraph Server loads this
90
+ file as ``user_router_module`` without registering it in ``sys.modules``, and
91
+ pydantic cannot resolve string annotations for a module it cannot find (the
92
+ ``ChatBody`` schema would then be "not fully defined" and the server's OpenAPI
93
+ generation fails at startup). Real annotations need no lookup.
94
+ """
95
+
96
+ import contextlib
97
+ import json
98
+ import logging
99
+ import math
100
+ import os
101
+ from collections.abc import AsyncIterator, Awaitable, Callable
102
+ from contextlib import aclosing, asynccontextmanager
103
+ from typing import Any, Literal
104
+
105
+ from dotenv import dotenv_values
106
+ from fastapi import Depends, FastAPI, HTTPException, Query, Request, Response
107
+ from fastapi.encoders import jsonable_encoder
108
+ from fastapi.exceptions import RequestValidationError
109
+ from fastapi.middleware.cors import CORSMiddleware
110
+ from fastapi.responses import HTMLResponse, JSONResponse
111
+ from psycopg import OperationalError
112
+ from psycopg_pool import PoolTimeout
113
+ from pydantic import BaseModel, Field, field_validator
114
+ from starlette.types import ASGIApp, Receive, Scope, Send
115
+
116
+ # `.env` first, below the process environment (as `load_dotenv()` does): the
117
+ # modules imported next, and this one, read settings as they load (A2A_NAME,
118
+ # the A2A card's auth scheme, APP_ENV for the docs, CORS_ALLOW_ORIGINS).
119
+ # PYTHON_DOTENV_DISABLED switches it off, as it does `load_dotenv()`: the
120
+ # project's tests set it so a developer's `.env` never reaches them.
121
+ os.environ.update(
122
+ {
123
+ key: value
124
+ for key, value in dotenv_values().items()
125
+ if value is not None and key not in os.environ
126
+ }
127
+ if os.environ.get("PYTHON_DOTENV_DISABLED", "").strip().lower()
128
+ not in {"1", "true", "t", "yes", "y"}
129
+ else {}
130
+ )
131
+
132
+ from {{cookiecutter.agent_directory}}.app_utils.a2a import (
133
+ A2A_RPC_PATH,
134
+ add_a2a_routes,
135
+ forget_context,
136
+ legacy_request_error,
137
+ task_ttl_s,
138
+ )
139
+ from {{cookiecutter.agent_directory}}.app_utils.a2a_client import client_settings
140
+ from {{cookiecutter.agent_directory}}.app_utils.api_client import set_outbound_headers
141
+ from {{cookiecutter.agent_directory}}.app_utils.approvals import CODE_APPROVAL_PENDING, STATUSES
142
+ from {{cookiecutter.agent_directory}}.app_utils.auth import (
143
+ Principal,
144
+ authenticate_and_authorize,
145
+ caller_note_enabled,
146
+ delegated_mentions,
147
+ delegation_settings,
148
+ require,
149
+ )
150
+ from {{cookiecutter.agent_directory}}.app_utils.chat import (
151
+ EVENT_TOOL_RESULT,
152
+ FASTAPI,
153
+ LANGGRAPH_SERVER,
154
+ RUNTIME,
155
+ ApprovalError,
156
+ ApprovalPending,
157
+ ChatRequest,
158
+ detect_runtime,
159
+ dev_mode,
160
+ forward_header_names,
161
+ log_unavailable,
162
+ new_error_id,
163
+ select_forward_headers,
164
+ sse_encode,
165
+ unavailable,
166
+ unavailable_detail,
167
+ validate_thread_id,
168
+ )
169
+ from {{cookiecutter.agent_directory}}.app_utils.checkpointer import pool_sizes
170
+ from {{cookiecutter.agent_directory}}.app_utils.content import client_message, client_tool_result
171
+ from {{cookiecutter.agent_directory}}.app_utils.db import StorageNotReady, is_database_unavailable
172
+ from {{cookiecutter.agent_directory}}.app_utils.limits import (
173
+ THREAD_ID_PATTERN,
174
+ SettingsError,
175
+ check_metadata,
176
+ check_settings,
177
+ )
178
+ from {{cookiecutter.agent_directory}}.app_utils.metrics import (
179
+ metrics_authorized,
180
+ metrics_enabled,
181
+ render,
182
+ )
183
+ from {{cookiecutter.agent_directory}}.app_utils.middleware import (
184
+ REQUEST_ID_HEADER,
185
+ AuthErrorMiddleware,
186
+ BodySizeLimitMiddleware,
187
+ RequestContextMiddleware,
188
+ RunStreamingResponse,
189
+ ThreadDeleteHookMiddleware,
190
+ max_message_chars,
191
+ read_body,
192
+ replay_body,
193
+ )
194
+ from {{cookiecutter.agent_directory}}.app_utils.model import model_limits, model_options
195
+ from {{cookiecutter.agent_directory}}.app_utils.playground import PLAYGROUND_HTML
196
+ from {{cookiecutter.agent_directory}}.app_utils.structured import structured_settings
197
+ from {{cookiecutter.agent_directory}}.app_utils.telemetry import (
198
+ bind_log_context,
199
+ log_format,
200
+ log_level,
201
+ outbound_trace_headers,
202
+ propagate_to_every_api,
203
+ setup_logging,
204
+ setup_server_logging,
205
+ setup_telemetry,
206
+ trace_capture,
207
+ trace_scope,
208
+ )
209
+ from {{cookiecutter.agent_directory}}.app_utils.threads import (
210
+ SCOPE_ALL,
211
+ SCOPE_OWN,
212
+ THREAD_BUSY,
213
+ ThreadBusy,
214
+ check_scope,
215
+ own_view,
216
+ search_server_threads,
217
+ )
218
+ from {{cookiecutter.agent_directory}}.app_utils.token_exchange import exchange_settings
219
+
220
+ logger = logging.getLogger(__name__)
221
+
222
+ # Calls of another agent (`protocol: a2a`) and of `auth: forward` and `auth: exchange` APIs
223
+ # carry this request's id and trace context (PROPAGATE_TRACE_HEADERS=peers, the default);
224
+ # other APIs receive neither, unless PROPAGATE_TRACE_HEADERS=all; off sends them nowhere.
225
+ set_outbound_headers(outbound_trace_headers, everywhere=propagate_to_every_api)
226
+
227
+ if detect_runtime() == FASTAPI:
228
+ # Now rather than in the lifespan: what is logged before it (the A2A
229
+ # card's APP_URL warning below, uvicorn's startup lines) follows
230
+ # LOG_FORMAT too. A bad LOG_LEVEL or LOG_FORMAT is left to the lifespan's
231
+ # settings check, which reports every bad setting at once.
232
+ with contextlib.suppress(SettingsError):
233
+ setup_logging()
234
+ else:
235
+ # LangGraph Server configures logging itself; keep query strings and
236
+ # outbound URLs out of its lines.
237
+ setup_server_logging()
238
+
239
+
240
+ @asynccontextmanager
241
+ async def lifespan(app_instance: FastAPI) -> AsyncIterator[None]:
242
+ # A bad limit, log, metrics, pool, model, trace-capture, A2A or response-schema
243
+ # setting stops startup instead of being guessed.
244
+ check_settings(
245
+ extra=(
246
+ log_level,
247
+ log_format,
248
+ metrics_enabled,
249
+ pool_sizes,
250
+ model_limits,
251
+ model_options,
252
+ trace_capture,
253
+ task_ttl_s,
254
+ max_message_chars,
255
+ delegation_settings,
256
+ delegated_mentions,
257
+ caller_note_enabled,
258
+ exchange_settings,
259
+ trace_scope,
260
+ client_settings,
261
+ structured_settings,
262
+ )
263
+ )
264
+ if RUNTIME.runtime == FASTAPI:
265
+ setup_logging()
266
+ else:
267
+ setup_server_logging() # LangGraph Server configures logging itself
268
+ setup_telemetry()
269
+ await RUNTIME.start()
270
+ try:
271
+ yield
272
+ finally:
273
+ await RUNTIME.stop()
274
+
275
+
276
+ class A2APolicyMiddleware:
277
+ """Authenticate and authorize the A2A endpoints with the selected policy.
278
+
279
+ `card.read` for the agent card, `a2a.invoke` for JSON-RPC. The principal
280
+ is stored in the request state for the executor. An A2A 0.3 request that
281
+ fails the SDK's validation, holds text that is not valid Unicode, or sends
282
+ a message that cannot run (no text, an empty text part, over
283
+ `MAX_MESSAGE_CHARS`) is answered here with a JSON-RPC error naming fields,
284
+ never values: past this point the SDK's 0.3 layer would log the values at
285
+ ERROR or turn it into an internal error (1.0 requests are checked by the
286
+ handler).
287
+ """
288
+
289
+ def __init__(self, app: ASGIApp, prefix: str) -> None:
290
+ self.app = app
291
+ self.prefix = prefix
292
+
293
+ async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
294
+ if scope["type"] != "http" or not scope["path"].startswith(self.prefix):
295
+ await self.app(scope, receive, send)
296
+ return
297
+ request = Request(scope, receive)
298
+ action = "card.read" if scope["path"].endswith("agent-card.json") else "a2a.invoke"
299
+ try:
300
+ principal = await authenticate_and_authorize(request, action)
301
+ except HTTPException as exc:
302
+ response = JSONResponse(
303
+ {"detail": exc.detail}, status_code=exc.status_code, headers=exc.headers
304
+ )
305
+ await response(scope, receive, send)
306
+ return
307
+ scope.setdefault("state", {})["principal"] = principal
308
+ if action == "a2a.invoke" and scope.get("method") == "POST":
309
+ try:
310
+ body = await read_body(receive)
311
+ except HTTPException as exc: # the body cap, while reading a chunked body
312
+ await JSONResponse({"detail": exc.detail}, status_code=exc.status_code)(
313
+ scope, receive, send
314
+ )
315
+ return
316
+ refusal = _legacy_refusal(body)
317
+ if refusal is not None:
318
+ await JSONResponse(refusal)(scope, receive, send)
319
+ return
320
+ receive = replay_body(body, receive)
321
+ await self.app(scope, receive, send)
322
+
323
+
324
+ def _legacy_refusal(body: bytes) -> dict[str, Any] | None:
325
+ """The JSON-RPC error answer to an A2A 0.3 request that cannot run, or None.
326
+
327
+ The body is parsed, not searched: a method name may be JSON-escaped
328
+ (`"message\\/send"`, as PHP's json_encode writes it).
329
+ """
330
+ try:
331
+ payload = json.loads(body)
332
+ except ValueError:
333
+ return None # the SDK answers a parse error itself
334
+ error = legacy_request_error(payload)
335
+ if error is None:
336
+ return None
337
+ request_id = payload.get("id")
338
+ if not isinstance(request_id, str | int) or isinstance(request_id, bool):
339
+ request_id = None
340
+ return {"jsonrpc": "2.0", "id": request_id, "error": error}
341
+
342
+
343
+ def _dev_mode() -> bool:
344
+ """`APP_ENV` is exactly `dev` (as `auth.dev_mode`)."""
345
+ return os.environ.get("APP_ENV") == "dev"
346
+
347
+
348
+ def cors_origins() -> list[str]:
349
+ """`CORS_ALLOW_ORIGINS` as a list; empty means no CORS middleware."""
350
+ return [o.strip() for o in (os.environ.get("CORS_ALLOW_ORIGINS") or "").split(",") if o.strip()]
351
+
352
+
353
+ # The schema and Swagger UI are unauthenticated FastAPI defaults; like
354
+ # /playground they exist only under APP_ENV=dev. app.openapi() still works
355
+ # (LangGraph Server builds its spec from it).
356
+ app = FastAPI(
357
+ title="{{cookiecutter.project_name}}",
358
+ description="LangGraph agent: chat SSE API and A2A protocol",
359
+ version=os.environ.get("AGENT_VERSION", "0.1.0"),
360
+ lifespan=lifespan,
361
+ openapi_url="/openapi.json" if _dev_mode() else None,
362
+ docs_url="/docs" if _dev_mode() else None,
363
+ redoc_url=None,
364
+ )
365
+ # Starlette runs the last-added middleware first: request context, then the
366
+ # body cap, then (langgraph-server) the native API's auth errors and thread
367
+ # deletes, then CORS (answers preflights before auth), then the A2A policy.
368
+ app.add_middleware(A2APolicyMiddleware, prefix=A2A_RPC_PATH)
369
+ if detect_runtime() == FASTAPI and cors_origins():
370
+ _origins = cors_origins()
371
+ app.add_middleware(
372
+ CORSMiddleware,
373
+ allow_origins=_origins,
374
+ # Cookies (custom policies) only with explicit origins, never with "*".
375
+ allow_credentials="*" not in _origins,
376
+ allow_methods=["GET", "POST", "DELETE", "OPTIONS"],
377
+ allow_headers=["authorization", "content-type", "x-request-id", *forward_header_names()],
378
+ expose_headers=["x-request-id"],
379
+ )
380
+
381
+
382
+ async def _server_thread_deleted(thread_id: str) -> None:
383
+ """langgraph-server: after the server deleted a thread, its run records, approvals and
384
+ A2A tasks."""
385
+ try:
386
+ await RUNTIME.forget_thread_runs(thread_id)
387
+ finally:
388
+ await forget_context(thread_id)
389
+
390
+
391
+ if detect_runtime() != FASTAPI:
392
+ app.add_middleware(ThreadDeleteHookMiddleware, on_deleted=_server_thread_deleted)
393
+ app.add_middleware(AuthErrorMiddleware)
394
+ app.add_middleware(BodySizeLimitMiddleware)
395
+ app.add_middleware(RequestContextMiddleware)
396
+ add_a2a_routes(app)
397
+
398
+
399
+ @app.exception_handler(ThreadBusy)
400
+ async def thread_busy_handler(request: Request, exc: ThreadBusy) -> JSONResponse:
401
+ return JSONResponse(status_code=409, content={"code": THREAD_BUSY, "detail": str(exc)})
402
+
403
+
404
+ @app.exception_handler(ApprovalPending)
405
+ async def approval_pending_handler(request: Request, exc: ApprovalPending) -> JSONResponse:
406
+ return JSONResponse(
407
+ status_code=409,
408
+ content={"code": CODE_APPROVAL_PENDING, "detail": str(exc), "approvals": exc.approvals},
409
+ )
410
+
411
+
412
+ @app.exception_handler(ApprovalError)
413
+ async def approval_error_handler(request: Request, exc: ApprovalError) -> JSONResponse:
414
+ return JSONResponse(status_code=exc.status_code, content=exc.body())
415
+
416
+
417
+ def _request_id_header(request: Request) -> dict[str, str] | None:
418
+ request_id = getattr(request.state, "request_id", None)
419
+ return {REQUEST_ID_HEADER: request_id} if isinstance(request_id, str) else None
420
+
421
+
422
+ async def database_unavailable_handler(request: Request, exc: Exception) -> JSONResponse:
423
+ """503 with an error id for an unreachable database that reached no route's own handling.
424
+
425
+ Every chat route already turns these into a 503; this covers any other
426
+ route. Registered for the exception classes (not `Exception`), so it
427
+ answers inside the app: one WARNING line, no traceback, nothing re-raised
428
+ for uvicorn to log again.
429
+ """
430
+ error_id = log_unavailable("Database", exc)
431
+ return JSONResponse(
432
+ status_code=503,
433
+ content={"detail": unavailable_detail("Database", error_id), "error_id": error_id},
434
+ headers=_request_id_header(request),
435
+ )
436
+
437
+
438
+ for _unreachable in (OperationalError, PoolTimeout, StorageNotReady):
439
+ app.add_exception_handler(_unreachable, database_unavailable_handler)
440
+
441
+
442
+ @app.exception_handler(Exception)
443
+ async def unhandled_error_handler(request: Request, exc: Exception) -> JSONResponse:
444
+ """500 (503 when the database is unreachable) with an id naming the logged detail."""
445
+ if is_database_unavailable(exc):
446
+ return await database_unavailable_handler(request, exc)
447
+ error_id = new_error_id()
448
+ logger.error("unhandled error (error_id=%s)", error_id, exc_info=exc)
449
+ # This handler answers from outside RequestContextMiddleware, which adds
450
+ # the header to every other response; it left the request id in the state.
451
+ return JSONResponse(
452
+ status_code=500,
453
+ content={"detail": f"Internal server error. Reference: {error_id}.", "error_id": error_id},
454
+ headers=_request_id_header(request),
455
+ )
456
+
457
+
458
+ def _json_safe(value: Any) -> Any:
459
+ """`value` encodable as UTF-8 JSON: NaN and infinite floats spelled as strings (JSON
460
+ has neither), unpaired surrogates as `\\udXXX` escapes (UTF-8 has none)."""
461
+ if isinstance(value, float) and not math.isfinite(value):
462
+ return str(value)
463
+ if isinstance(value, str):
464
+ return value.encode("utf-8", "backslashreplace").decode("utf-8")
465
+ if isinstance(value, dict):
466
+ return {_json_safe(k): _json_safe(v) for k, v in value.items()}
467
+ if isinstance(value, list | tuple):
468
+ return [_json_safe(v) for v in value]
469
+ return value
470
+
471
+
472
+ @app.exception_handler(RequestValidationError)
473
+ async def validation_error_handler(request: Request, exc: RequestValidationError) -> JSONResponse:
474
+ """FastAPI's 422 without the offending values.
475
+
476
+ The default handler echoes each value back in `input`: a 32,000-character
477
+ message, text with an unpaired surrogate (which then fails to encode: a
478
+ 500, logged with the message in its traceback), or a NaN. Only where
479
+ (`loc`), what (`type`, `msg`) and the rule's parameters (`ctx`) are kept.
480
+ """
481
+ errors = [
482
+ {key: value for key, value in error.items() if key not in ("input", "url")}
483
+ for error in exc.errors()
484
+ ]
485
+ return JSONResponse(status_code=422, content={"detail": _json_safe(jsonable_encoder(errors))})
486
+
487
+
488
+ def _has_surrogate(value: Any) -> bool:
489
+ """Whether a string (or a key or value of a mapping) holds an unpaired surrogate."""
490
+ if isinstance(value, str):
491
+ try:
492
+ value.encode("utf-8")
493
+ except UnicodeEncodeError:
494
+ return True
495
+ return False
496
+ if isinstance(value, dict):
497
+ return any(_has_surrogate(k) or _has_surrogate(v) for k, v in value.items())
498
+ return False
499
+
500
+
501
+ class ChatBody(BaseModel):
502
+ # Omit thread_id to start a thread: the server generates a random id. Ids
503
+ # are one namespace shared by every caller, so an id a client picks must
504
+ # be unguessable (an id another principal used first is theirs: 403).
505
+ thread_id: str | None = Field(default=None, pattern=THREAD_ID_PATTERN)
506
+ message: str = Field(min_length=1)
507
+ metadata: dict[str, Any] = Field(default_factory=dict)
508
+
509
+ @field_validator("message")
510
+ @classmethod
511
+ def _cap_message(cls, value: str) -> str:
512
+ cap = max_message_chars()
513
+ if len(value) > cap:
514
+ raise ValueError(f"message is longer than {cap} characters (MAX_MESSAGE_CHARS).")
515
+ if _has_surrogate(value):
516
+ raise ValueError("message is not valid Unicode text (an unpaired surrogate).")
517
+ return value
518
+
519
+ @field_validator("metadata")
520
+ @classmethod
521
+ def _cap_metadata(cls, value: dict[str, Any]) -> dict[str, Any]:
522
+ if _has_surrogate(value):
523
+ raise ValueError("metadata is not valid Unicode text (an unpaired surrogate).")
524
+ return check_metadata(value)
525
+
526
+
527
+ def _forward_headers(request: Request) -> dict[str, str]:
528
+ return select_forward_headers(request.headers)
529
+
530
+
531
+ def principal_for(action: str) -> Callable[[Request], Awaitable[Principal]]:
532
+ """`require(action)`, plus the caller's hashed id on every log record of the request."""
533
+ check = require(action)
534
+
535
+ async def dependency(request: Request) -> Principal:
536
+ principal = await check(request)
537
+ bind_log_context(
538
+ principal_hash=principal.hashed_id(),
539
+ actor=principal.actor.id if principal.actor is not None else None,
540
+ )
541
+ return principal
542
+
543
+ dependency.__name__ = f"principal_for_{action.replace('.', '_')}"
544
+ return dependency
545
+
546
+
547
+ @app.post("/chat")
548
+ async def chat(
549
+ body: ChatBody,
550
+ request: Request,
551
+ principal: Principal = Depends(principal_for("chat.send")),
552
+ ) -> RunStreamingResponse:
553
+ req = ChatRequest(
554
+ message=body.message,
555
+ thread_id=body.thread_id,
556
+ metadata=body.metadata,
557
+ forward_headers=_forward_headers(request),
558
+ )
559
+ thread_id = await RUNTIME.resolve_thread(principal, req)
560
+ # ThreadBusy -> 409 before streaming; the owner is checked again under the lock.
561
+ lease = await RUNTIME.acquire_thread(thread_id, principal)
562
+ try:
563
+ # A thread waiting for an approval takes no new message: 409 approval_pending.
564
+ await RUNTIME.assert_no_pending_approval(thread_id)
565
+ except BaseException:
566
+ await lease.release()
567
+ raise
568
+ return _run_stream(RUNTIME.stream(principal, req, thread_id, lease=lease), thread_id, lease)
569
+
570
+
571
+ def _run_stream(
572
+ stream: AsyncIterator[tuple[str, dict[str, Any]]], thread_id: str, lease: Any
573
+ ) -> RunStreamingResponse:
574
+ """The SSE response of one run (`/chat`, or a decision's resumed run)."""
575
+ dev = dev_mode()
576
+
577
+ async def events() -> AsyncIterator[str]:
578
+ async with aclosing(stream) as run:
579
+ async for event, data in run:
580
+ if event == EVENT_TOOL_RESULT:
581
+ # A failed call's text (policy rules, limits, upstream
582
+ # errors) is for the model; the client gets an error id.
583
+ data = client_tool_result(data, thread_id=thread_id, dev=dev)
584
+ yield sse_encode(event, data)
585
+
586
+ return RunStreamingResponse(
587
+ events(),
588
+ on_close=lease.release,
589
+ media_type="text/event-stream",
590
+ headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
591
+ )
592
+
593
+
594
+ class DecisionBody(BaseModel):
595
+ decision: Literal["approve", "reject"]
596
+ comment: str | None = Field(default=None, max_length=1000)
597
+ # The approval's `digest`: an agent relaying the person's decision must send it.
598
+ digest: str | None = Field(default=None, max_length=80)
599
+
600
+ @field_validator("comment")
601
+ @classmethod
602
+ def _plain_comment(cls, value: str | None) -> str | None:
603
+ if value is not None and _has_surrogate(value):
604
+ raise ValueError("comment is not valid Unicode text (an unpaired surrogate).")
605
+ return value
606
+
607
+
608
+ @app.get("/threads/{thread_id}/approvals")
609
+ async def thread_approvals(
610
+ thread_id: str,
611
+ request: Request,
612
+ principal: Principal = Depends(principal_for("approval.read")),
613
+ ) -> list[dict[str, Any]]:
614
+ """The thread's approvals of gated API calls, newest first.
615
+
616
+ The owner and read-across roles see them all, a decider the ones it may
617
+ decide (anyone else: 403). `query` and `body` are shown to the owner and
618
+ the deciders while an approval is pending.
619
+ """
620
+ return await RUNTIME.thread_approvals(principal, thread_id, _forward_headers(request))
621
+
622
+
623
+ @app.post("/threads/{thread_id}/approvals/{approval_id}")
624
+ async def decide_approval(
625
+ thread_id: str,
626
+ approval_id: str,
627
+ body: DecisionBody,
628
+ request: Request,
629
+ principal: Principal = Depends(principal_for("approval.decide")),
630
+ ) -> RunStreamingResponse:
631
+ """Approve or reject a pending approval; stream the resumed run (the `/chat` events).
632
+
633
+ 404 `approval_not_found`, 403 `not_an_approver` or `approval_direct_only`,
634
+ 409 `approval_not_pending` (decided already), 409 `approval_digest_mismatch`,
635
+ 410 `approval_expired`, 409 `thread_busy`.
636
+ """
637
+ lease, resume, acting = await RUNTIME.decide(
638
+ principal,
639
+ thread_id,
640
+ approval_id,
641
+ body.decision,
642
+ body.comment,
643
+ _forward_headers(request),
644
+ digest=body.digest,
645
+ )
646
+ req = ChatRequest(
647
+ message="", thread_id=lease.thread_id, forward_headers=_forward_headers(request)
648
+ )
649
+ stream = RUNTIME.stream(acting, req, lease.thread_id, lease=lease, resume=resume)
650
+ return _run_stream(stream, lease.thread_id, lease)
651
+
652
+
653
+ @app.get("/approvals")
654
+ async def list_approvals(
655
+ status: str | None = Query(default=None, pattern="^(" + "|".join(STATUSES) + ")$"),
656
+ limit: int = Query(default=20, ge=1, le=100),
657
+ offset: int = Query(default=0, ge=0, le=100_000),
658
+ principal: Principal = Depends(principal_for("approval.read")),
659
+ ) -> list[dict[str, Any]]:
660
+ """Approvals across threads: the caller's own, the ones it may decide (a role
661
+ in their approvers), and every one for a read-across role; newest first."""
662
+ return await RUNTIME.visible_approvals(principal, status=status, limit=limit, offset=offset)
663
+
664
+
665
+ @app.get("/health")
666
+ async def health() -> dict[str, str]:
667
+ return {"status": "ok", "runtime": RUNTIME.runtime, "checkpointer": RUNTIME.checkpointer_kind()}
668
+
669
+
670
+ @app.get("/ready")
671
+ async def ready() -> JSONResponse:
672
+ if await RUNTIME.ready():
673
+ return JSONResponse({"status": "ready"})
674
+ return JSONResponse({"status": "not_ready"}, status_code=503)
675
+
676
+
677
+ @app.get("/metrics", include_in_schema=False)
678
+ async def prometheus_metrics(request: Request) -> Response:
679
+ if not metrics_enabled():
680
+ raise HTTPException(status_code=404, detail="Not found")
681
+ if not metrics_authorized(request.headers.get("authorization")):
682
+ raise HTTPException(
683
+ status_code=401,
684
+ detail="Missing or invalid metrics token.",
685
+ headers={"WWW-Authenticate": "Bearer"},
686
+ )
687
+ payload, content_type = render()
688
+ return Response(content=payload, media_type=content_type)
689
+
690
+
691
+ @app.get("/threads")
692
+ async def list_threads(
693
+ request: Request,
694
+ limit: int = Query(default=20, ge=1, le=100),
695
+ offset: int = Query(default=0, ge=0, le=100_000),
696
+ scope: str = Query(default=SCOPE_OWN, pattern="^(own|all)$"),
697
+ principal: Principal = Depends(principal_for("thread.list")),
698
+ ) -> list[dict[str, Any]]:
699
+ """The caller's threads, or with `scope=all` every principal's (read-across roles only).
700
+
701
+ Each row is `{thread_id, owner, created_at, updated_at}`, `owner` being the
702
+ owner's hashed principal id.
703
+ """
704
+ check_scope(principal, scope)
705
+ headers = _forward_headers(request)
706
+ if scope == SCOPE_ALL and RUNTIME.runtime == LANGGRAPH_SERVER:
707
+ try:
708
+ return await search_server_threads(
709
+ RUNTIME._sdk_client(headers), principal, scope=scope, limit=limit, offset=offset
710
+ )
711
+ except Exception as exc:
712
+ raise unavailable("LangGraph Server", exc) from exc
713
+ rows = await RUNTIME.list_threads(
714
+ principal if scope == SCOPE_ALL else own_view(principal),
715
+ limit=limit,
716
+ offset=offset,
717
+ forward_headers=headers,
718
+ )
719
+ if scope == SCOPE_OWN:
720
+ owner = principal.hashed_id()
721
+ rows = [{**row, "owner": owner} for row in rows]
722
+ return rows
723
+
724
+
725
+ @app.get("/threads/{thread_id}/messages")
726
+ async def thread_messages(
727
+ thread_id: str,
728
+ request: Request,
729
+ principal: Principal = Depends(principal_for("thread.read")),
730
+ ) -> list[dict[str, Any]]:
731
+ messages = await RUNTIME.messages(principal, thread_id, _forward_headers(request))
732
+ # A failed tool call reads as in the stream: an error id (derived from the
733
+ # thread id in its canonical form, as the stream has it), not the error text.
734
+ canonical = validate_thread_id(thread_id, RUNTIME.runtime)
735
+ dev = dev_mode()
736
+ return [client_message(m, thread_id=canonical, dev=dev) for m in messages]
737
+
738
+
739
+ async def delete_thread(
740
+ thread_id: str,
741
+ request: Request,
742
+ principal: Principal = Depends(principal_for("thread.delete")),
743
+ ) -> Response:
744
+ await RUNTIME.delete_thread(principal, thread_id, _forward_headers(request))
745
+ return Response(status_code=204)
746
+
747
+
748
+ # Under langgraph-server the server owns `DELETE /threads/{thread_id}` (its
749
+ # native API, owner-only through the auth handler); a route of this app would
750
+ # shadow it, the app's own loopback delete included.
751
+ if detect_runtime() == FASTAPI:
752
+ app.delete("/threads/{thread_id}", status_code=204)(delete_thread)
753
+
754
+
755
+ @app.get("/playground", response_class=HTMLResponse, include_in_schema=False)
756
+ async def playground() -> HTMLResponse:
757
+ if not _dev_mode():
758
+ raise HTTPException(status_code=404, detail="Not found")
759
+ return HTMLResponse(PLAYGROUND_HTML)
760
+
761
+
762
+ if __name__ == "__main__":
763
+ import uvicorn
764
+
765
+ uvicorn.run(
766
+ app,
767
+ host=os.environ.get("HOST", "127.0.0.1"),
768
+ port=int(os.environ.get("PORT", "8000")),
769
+ log_config=None, # keep the logging configured above (LOG_FORMAT)
770
+ )