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,681 @@
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
+ """Logging and tracing setup.
17
+
18
+ Logging (`setup_logging()`, fastapi runtime): one handler on the root
19
+ logger, level `LOG_LEVEL` (default INFO), format `LOG_FORMAT=json|text`
20
+ (default `json`, `text` under `APP_ENV=dev`). Every record carries the
21
+ request id, and inside a run the run id, the thread id and the hashed
22
+ principal, from context variables set by the HTTP middleware and the chat
23
+ runtime. Nothing logs headers, bodies or credentials:
24
+
25
+ * uvicorn's access lines keep the path and drop the query string (a client
26
+ that puts a token in the URL, `?access_token=...`, never gets it logged);
27
+ * the HTTP client libraries (`httpx`, `httpcore`, and `httpx2`/`httpcore2`
28
+ that model provider SDKs use) log at WARNING only, since their INFO lines
29
+ carry every outbound URL with its query and path values (tool arguments,
30
+ customer ids); `app_utils.api_client` logs each outbound call itself with
31
+ the API, method, operation id and path template instead;
32
+ * Python warnings are captured into the same handler (one JSON record each,
33
+ logger `py.warnings`), and the value a pydantic serializer warning echoes
34
+ (`input_value=...`, which can be a run context holding a forwarded
35
+ credential) is redacted.
36
+
37
+ LangGraph Server configures its own handlers, format and level (its
38
+ `LOG_LEVEL`, `LOG_JSON`). Under `langgraph-server`, `setup_server_logging()`
39
+ applies the same three rules to them: the server's access lines
40
+ (`langgraph_api.server`) lose their `query_string` field, the HTTP client
41
+ libraries log at WARNING only (`quiet_client_loggers()`; `api_client`
42
+ does the same when imported, in every process that runs the graph), and
43
+ warnings are captured with their values redacted.
44
+
45
+ Tracing: explicit opt-in, LangSmith or OpenTelemetry.
46
+
47
+ Nothing is configured unless `TRACING_ENABLED=true`. Then:
48
+
49
+ * with `LANGSMITH_API_KEY` set, LangChain's native LangSmith tracing is enabled
50
+ through the environment (`LANGSMITH_TRACING`, `LANGSMITH_PROJECT`,
51
+ `LANGSMITH_ENDPOINT`);
52
+ * otherwise spans go over OTLP/HTTP to `OTEL_EXPORTER_OTLP_ENDPOINT` using
53
+ OpenInference's LangChain instrumentation.
54
+
55
+ `TRACE_CAPTURE=metadata` (default) records structure, timing, model and tool
56
+ names, token counts, error types and identifiers (thread, run, hashed
57
+ principal) but no prompt or completion text, tool arguments or results, or
58
+ error messages. `TRACE_CAPTURE=full` records everything. The same policy is
59
+ applied to LangSmith (`hide_inputs`/`hide_outputs` plus a client anonymizer
60
+ that reduces a run's `error` to the exception class), to OpenInference (its
61
+ masking config plus an exporter that strips exception messages, stack traces
62
+ and the span status description) and to the run records the app keeps.
63
+
64
+ Correlation across services (`PROPAGATE_TRACE_HEADERS`, `trace_scope`: `peers`
65
+ by default, `all` or `off`; `true` reads as `peers`, `false` as `off`): under
66
+ `peers` every call of the policy client to another agent (`protocol: a2a`), or
67
+ to an `auth: forward` or `auth: exchange` API (reached with the caller's own
68
+ credential or a token exchanged for it), carries this request's `X-Request-ID`
69
+ and, when spans go over OTLP, the W3C trace context of the current span
70
+ (`outbound_trace_headers`, installed with `api_client.set_outbound_headers`;
71
+ `api_client.propagates` names the APIs), and a call to any other API carries
72
+ neither; an incoming `traceparent` on the A2A routes is attached to the
73
+ request's context (`attach_trace_context`, in
74
+ `middleware.RequestContextMiddleware`), so an agent this one calls over A2A
75
+ logs the same request id and its spans join this trace, while a caller of the
76
+ public routes (`/chat`) cannot choose its trace ids. `all` sends them to every
77
+ API and continues a trace on every path (behind a tracing gateway); `off`
78
+ neither. Under LangSmith tracing only the request id is passed on.
79
+ """
80
+
81
+ from __future__ import annotations
82
+
83
+ import json
84
+ import logging
85
+ import os
86
+ import re
87
+ import sys
88
+ from collections.abc import Iterable
89
+ from contextvars import ContextVar
90
+ from datetime import UTC, datetime
91
+ from pathlib import Path
92
+ from typing import Any
93
+
94
+ from {{cookiecutter.agent_directory}}.app_utils.limits import SettingsError
95
+
96
+ logger = logging.getLogger(__name__)
97
+
98
+ _initialized = False
99
+ # Set once spans are exported over OTLP: the W3C trace context is then passed on and taken.
100
+ _otlp_active = False
101
+
102
+ # ---------------------------------------------------------------------------
103
+ # Logging
104
+ # ---------------------------------------------------------------------------
105
+
106
+ # Correlation ids attached to every log record of the current request / run.
107
+ LOG_CONTEXT: dict[str, ContextVar[str | None]] = {
108
+ "request_id": ContextVar("request_id", default=None),
109
+ "run_id": ContextVar("run_id", default=None),
110
+ "thread_id": ContextVar("thread_id", default=None),
111
+ "principal_hash": ContextVar("principal_hash", default=None),
112
+ # The agent presenting a delegated request (a client name, not personal data).
113
+ "actor": ContextVar("actor", default=None),
114
+ }
115
+
116
+ LOG_LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
117
+ _HANDLER_FLAG = "_graph_agents_handler"
118
+ _STANDARD_ATTRS = set(logging.LogRecord("", 0, "", 0, "", None, None).__dict__) | {
119
+ "message",
120
+ "asctime",
121
+ "taskName",
122
+ "color_message", # uvicorn's ANSI-coloured duplicate of the message
123
+ "ids", # set by TextFormatter
124
+ }
125
+
126
+
127
+ def bind_log_context(**values: str | None) -> None:
128
+ """Set correlation ids (`request_id`, `run_id`, `thread_id`, `principal_hash`, `actor`)."""
129
+ for name, value in values.items():
130
+ LOG_CONTEXT[name].set(value)
131
+
132
+
133
+ def log_level() -> str:
134
+ """`LOG_LEVEL` (default INFO); an unknown level is a startup error."""
135
+ level = (os.environ.get("LOG_LEVEL") or "INFO").strip().upper()
136
+ if level not in LOG_LEVELS:
137
+ raise SettingsError(f"LOG_LEVEL={level!r} is not one of {', '.join(LOG_LEVELS)}.")
138
+ return level
139
+
140
+
141
+ def log_format() -> str:
142
+ """`LOG_FORMAT` (`json` or `text`); defaults to `text` under APP_ENV=dev, else `json`."""
143
+ fmt = (os.environ.get("LOG_FORMAT") or "").strip().lower()
144
+ if not fmt:
145
+ return "text" if os.environ.get("APP_ENV") == "dev" else "json"
146
+ if fmt not in ("json", "text"):
147
+ raise SettingsError(f"LOG_FORMAT={fmt!r} must be 'json' or 'text'.")
148
+ return fmt
149
+
150
+
151
+ class ContextFilter(logging.Filter):
152
+ """Copy the correlation ids of the current context onto each record."""
153
+
154
+ def filter(self, record: logging.LogRecord) -> bool:
155
+ for name, var in LOG_CONTEXT.items():
156
+ if getattr(record, name, None) is None:
157
+ setattr(record, name, var.get())
158
+ return True
159
+
160
+
161
+ class JsonFormatter(logging.Formatter):
162
+ """One JSON object per line: ts, level, logger, message, correlation ids, extras."""
163
+
164
+ def format(self, record: logging.LogRecord) -> str:
165
+ payload: dict[str, Any] = {
166
+ "ts": datetime.fromtimestamp(record.created, tz=UTC).isoformat(timespec="milliseconds"),
167
+ "level": record.levelname,
168
+ "logger": record.name,
169
+ "message": record.getMessage(),
170
+ }
171
+ for name in LOG_CONTEXT:
172
+ value = getattr(record, name, None)
173
+ if value:
174
+ payload[name] = value
175
+ for key, value in record.__dict__.items():
176
+ if key in _STANDARD_ATTRS or key in payload or key in LOG_CONTEXT or value is None:
177
+ continue
178
+ if key.startswith("_"):
179
+ continue
180
+ payload[key] = value if isinstance(value, str | int | float | bool) else repr(value)
181
+ if record.exc_info:
182
+ payload["exc_type"] = record.exc_info[0].__name__ if record.exc_info[0] else None
183
+ payload["exception"] = self.formatException(record.exc_info)
184
+ return json.dumps(payload, ensure_ascii=False, default=str)
185
+
186
+
187
+ class TextFormatter(logging.Formatter):
188
+ def __init__(self) -> None:
189
+ super().__init__("%(asctime)s %(levelname)s %(name)s%(ids)s: %(message)s")
190
+
191
+ def format(self, record: logging.LogRecord) -> str:
192
+ ids = " ".join(
193
+ f"{name}={getattr(record, name)}"
194
+ for name in ("request_id", "run_id")
195
+ if getattr(record, name, None)
196
+ )
197
+ record.ids = f" [{ids}]" if ids else ""
198
+ return super().format(record)
199
+
200
+
201
+ class _StderrHandler(logging.StreamHandler):
202
+ """Writes to whatever `sys.stderr` is at emit time (test runners swap it)."""
203
+
204
+ def __init__(self) -> None:
205
+ logging.Handler.__init__(self)
206
+
207
+ @property
208
+ def stream(self) -> Any: # type: ignore[override]
209
+ return sys.stderr
210
+
211
+
212
+ class AccessLogFilter(logging.Filter):
213
+ """Drop the query string from uvicorn's access lines.
214
+
215
+ uvicorn logs `'%s - "%s %s HTTP/%s" %d'` with the full path and query as
216
+ the third argument. Query strings carry whatever a client put there,
217
+ credentials included (`?access_token=...`), so only the path is kept.
218
+ """
219
+
220
+ def filter(self, record: logging.LogRecord) -> bool:
221
+ args = record.args
222
+ if isinstance(args, tuple) and len(args) >= 3 and isinstance(args[2], str):
223
+ path = args[2]
224
+ if "?" in path:
225
+ record.args = (*args[:2], path.split("?", 1)[0], *args[3:])
226
+ return True
227
+
228
+
229
+ # A pydantic serializer warning names the value it could not serialize:
230
+ # `[field_name='context', input_value=AgentContext(...), input_type=...]`. The
231
+ # value's repr can hold a forwarded credential (a run context's attributes),
232
+ # so everything between `input_value=` and the last `, input_type=` of the line goes.
233
+ _WARNING_VALUE = re.compile(r"input_value=.*, input_type=")
234
+
235
+
236
+ class WarningRedactionFilter(logging.Filter):
237
+ """Redact the values pydantic warnings echo (see `_WARNING_VALUE`)."""
238
+
239
+ def filter(self, record: logging.LogRecord) -> bool:
240
+ message = record.getMessage()
241
+ if "input_value=" in message:
242
+ record.msg = _WARNING_VALUE.sub("input_value=<redacted>, input_type=", message)
243
+ record.args = None
244
+ return True
245
+
246
+
247
+ # Loggers whose INFO lines carry full outbound URLs (query and path values):
248
+ # httpx, and httpx2, which the model provider SDKs use.
249
+ QUIET_LOGGERS = ("httpx", "httpcore", "httpx2", "httpcore2")
250
+ # LangGraph Server's access logger (a structlog event dict per request).
251
+ SERVER_ACCESS_LOGGER = "langgraph_api.server"
252
+
253
+
254
+ class ServerAccessLogFilter(logging.Filter):
255
+ """Drop the query string from LangGraph Server's access lines.
256
+
257
+ The server logs each request through structlog with a `query_string`
258
+ field holding the raw query (credentials a client put in the URL
259
+ included). Its records carry the event dict as `msg`; the field is
260
+ removed before any handler formats it. The line's text holds the path only.
261
+ """
262
+
263
+ def filter(self, record: logging.LogRecord) -> bool:
264
+ event = record.msg
265
+ if isinstance(event, dict) and "query_string" in event:
266
+ record.msg = {k: v for k, v in event.items() if k != "query_string"}
267
+ return True
268
+
269
+
270
+ def quiet_client_loggers() -> None:
271
+ """`QUIET_LOGGERS` at WARNING: their INFO lines carry full outbound URLs."""
272
+ for name in QUIET_LOGGERS:
273
+ logging.getLogger(name).setLevel(logging.WARNING)
274
+
275
+
276
+ def setup_server_logging() -> None:
277
+ """langgraph-server: the logging rules of `setup_logging()` on the server's own handlers."""
278
+ _add_filter_once(logging.getLogger(SERVER_ACCESS_LOGGER), ServerAccessLogFilter)
279
+ _add_filter_once(logging.getLogger("uvicorn.access"), AccessLogFilter)
280
+ quiet_client_loggers()
281
+ logging.captureWarnings(True)
282
+ _add_filter_once(logging.getLogger("py.warnings"), WarningRedactionFilter)
283
+
284
+
285
+ def setup_logging() -> None:
286
+ """Route every logger (uvicorn's included) through one handler; idempotent."""
287
+ root = logging.getLogger()
288
+ for handler in list(root.handlers):
289
+ if getattr(handler, _HANDLER_FLAG, False):
290
+ root.removeHandler(handler)
291
+ handler = _StderrHandler()
292
+ setattr(handler, _HANDLER_FLAG, True)
293
+ handler.addFilter(ContextFilter())
294
+ handler.setFormatter(JsonFormatter() if log_format() == "json" else TextFormatter())
295
+ root.addHandler(handler)
296
+ root.setLevel(log_level())
297
+ # uvicorn installs its own handlers before the app loads; send its records
298
+ # (startup, errors, access lines) through the same handler instead.
299
+ for name in ("uvicorn", "uvicorn.error", "uvicorn.access"):
300
+ uv_logger = logging.getLogger(name)
301
+ uv_logger.handlers = []
302
+ uv_logger.propagate = True
303
+ _add_filter_once(logging.getLogger("uvicorn.access"), AccessLogFilter)
304
+ quiet_client_loggers()
305
+ # Warnings become records of this handler (JSON under LOG_FORMAT=json)
306
+ # instead of raw multi-line text on stderr.
307
+ logging.captureWarnings(True)
308
+ _add_filter_once(logging.getLogger("py.warnings"), WarningRedactionFilter)
309
+
310
+
311
+ def _add_filter_once(target: logging.Logger, kind: type[logging.Filter]) -> None:
312
+ if not any(isinstance(f, kind) for f in target.filters):
313
+ target.addFilter(kind())
314
+
315
+
316
+ # ---------------------------------------------------------------------------
317
+ # Tracing
318
+ # ---------------------------------------------------------------------------
319
+
320
+
321
+ def tracing_enabled() -> bool:
322
+ return (os.environ.get("TRACING_ENABLED") or "false").strip().lower() in ("1", "true", "yes")
323
+
324
+
325
+ CAPTURE_MODES = ("metadata", "full")
326
+
327
+
328
+ def trace_capture() -> str:
329
+ """`TRACE_CAPTURE` (`metadata`, the default, or `full`); anything else is a startup error."""
330
+ mode = (os.environ.get("TRACE_CAPTURE") or "metadata").strip().lower()
331
+ if mode not in CAPTURE_MODES:
332
+ raise SettingsError(f"TRACE_CAPTURE={mode!r} must be 'metadata' or 'full'.")
333
+ return mode
334
+
335
+
336
+ def capture_mode() -> str:
337
+ """The capture mode for a trace. Startup refuses a bad `TRACE_CAPTURE`
338
+ (`trace_capture`); should one be read anyway, it records metadata only."""
339
+ mode = (os.environ.get("TRACE_CAPTURE") or "metadata").strip().lower()
340
+ return "full" if mode == "full" else "metadata"
341
+
342
+
343
+ def project_name() -> str:
344
+ return (
345
+ os.environ.get("LANGSMITH_PROJECT")
346
+ or os.environ.get("OTEL_SERVICE_NAME")
347
+ or Path.cwd().name
348
+ )
349
+
350
+
351
+ def setup_telemetry() -> None:
352
+ """Configure tracing once, per the environment. Safe to call repeatedly."""
353
+ global _initialized
354
+ if _initialized:
355
+ return
356
+ _initialized = True
357
+
358
+ if not tracing_enabled():
359
+ # A stray LANGSMITH_TRACING=true must not create a client: TRACING_ENABLED is the switch.
360
+ os.environ["LANGSMITH_TRACING"] = "false"
361
+ os.environ["LANGCHAIN_TRACING_V2"] = "false"
362
+ logger.info("Tracing disabled (TRACING_ENABLED != true).")
363
+ return
364
+
365
+ full = capture_mode() == "full"
366
+ if os.environ.get("LANGSMITH_API_KEY"):
367
+ _setup_langsmith(full)
368
+ else:
369
+ _setup_otlp(full)
370
+
371
+
372
+ _ERROR_TYPE = re.compile(r"\s*([A-Za-z_][\w.]*)")
373
+
374
+
375
+ def redact_error(payload: Any) -> Any:
376
+ """LangSmith anonymizer: keep the exception class, drop the message and the stack trace.
377
+
378
+ LangChain records `run.error` as `repr(exc)` plus the formatted traceback,
379
+ and `LANGSMITH_HIDE_*` never touch it; only a client anonymizer does. The
380
+ client applies it to the `{"error": <str>}` wrapper (and to run metadata,
381
+ which passes through untouched), so only that exact shape is rewritten.
382
+ """
383
+ if (
384
+ isinstance(payload, dict)
385
+ and set(payload) == {"error"}
386
+ and isinstance(payload["error"], str)
387
+ ):
388
+ m = _ERROR_TYPE.match(payload["error"])
389
+ return {"error": (m.group(1) if m else "Error") + ": <redacted>"}
390
+ return payload
391
+
392
+
393
+ def _setup_langsmith(full: bool) -> None:
394
+ os.environ["LANGSMITH_TRACING"] = "true"
395
+ os.environ.setdefault("LANGSMITH_PROJECT", project_name())
396
+ hide = "false" if full else "true"
397
+ # Kept for any client created elsewhere from the environment.
398
+ os.environ["LANGSMITH_HIDE_INPUTS"] = hide
399
+ os.environ["LANGSMITH_HIDE_OUTPUTS"] = hide
400
+ try:
401
+ import langsmith
402
+ except ImportError as exc: # pragma: no cover - dependency is in pyproject
403
+ logger.warning("LangSmith tracing unavailable: %s", exc)
404
+ return
405
+ # One process-wide client: every LangChainTracer (env-driven, /chat, A2A,
406
+ # the playground) picks it up, so the error redaction cannot be bypassed.
407
+ client = langsmith.Client(
408
+ hide_inputs=not full,
409
+ hide_outputs=not full,
410
+ anonymizer=None if full else redact_error,
411
+ )
412
+ langsmith.configure(client=client)
413
+ logger.info(
414
+ "Tracing to LangSmith project %r (capture=%s, endpoint=%s).",
415
+ os.environ["LANGSMITH_PROJECT"],
416
+ "full" if full else "metadata",
417
+ os.environ.get("LANGSMITH_ENDPOINT", "default"),
418
+ )
419
+
420
+
421
+ def _setup_otlp(full: bool) -> None:
422
+ global _otlp_active
423
+ endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT", "").strip()
424
+ if not endpoint:
425
+ logger.warning(
426
+ "TRACING_ENABLED=true but neither LANGSMITH_API_KEY nor OTEL_EXPORTER_OTLP_ENDPOINT is set; "
427
+ "no exporter configured."
428
+ )
429
+ return
430
+ try:
431
+ from openinference.instrumentation import TraceConfig
432
+ from openinference.instrumentation.langchain import LangChainInstrumentor
433
+ from opentelemetry import trace
434
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
435
+ from opentelemetry.sdk.resources import Resource
436
+ from opentelemetry.sdk.trace import TracerProvider
437
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
438
+ except ImportError as exc: # pragma: no cover - dependencies are in pyproject
439
+ logger.warning("OpenTelemetry tracing unavailable: %s", exc)
440
+ return
441
+
442
+ resource = Resource.create(
443
+ {
444
+ "service.name": os.environ.get("OTEL_SERVICE_NAME") or project_name(),
445
+ "service.version": os.environ.get("AGENT_VERSION", "0.1.0"),
446
+ "deployment.environment": os.environ.get("APP_ENV", "dev"),
447
+ }
448
+ )
449
+ provider = TracerProvider(resource=resource)
450
+ exporter: Any = (
451
+ OTLPSpanExporter()
452
+ ) # reads OTEL_EXPORTER_OTLP_ENDPOINT (+ /v1/traces) and headers
453
+ if not full:
454
+ exporter = _MetadataOnlyExporter(exporter)
455
+ provider.add_span_processor(BatchSpanProcessor(exporter))
456
+ trace.set_tracer_provider(provider)
457
+
458
+ hide = not full
459
+ config = TraceConfig(
460
+ hide_inputs=hide,
461
+ hide_outputs=hide,
462
+ hide_input_messages=hide,
463
+ hide_output_messages=hide,
464
+ hide_input_text=hide,
465
+ hide_output_text=hide,
466
+ hide_prompts=hide,
467
+ hide_choices=hide,
468
+ hide_llm_invocation_parameters=False,
469
+ hide_llm_tools=False,
470
+ )
471
+ LangChainInstrumentor().instrument(tracer_provider=provider, config=config)
472
+ _otlp_active = True
473
+ logger.info("Tracing over OTLP to %s (capture=%s).", endpoint, "full" if full else "metadata")
474
+
475
+
476
+ # ---------------------------------------------------------------------------
477
+ # Correlation across services
478
+ # ---------------------------------------------------------------------------
479
+
480
+ TRACE_CONTEXT_HEADERS = ("traceparent", "tracestate")
481
+ # `PROPAGATE_TRACE_HEADERS`: how far the request id and the trace context go.
482
+ TRACE_SCOPE_PEERS = "peers"
483
+ TRACE_SCOPE_ALL = "all"
484
+ TRACE_SCOPE_OFF = "off"
485
+ TRACE_SCOPES = (TRACE_SCOPE_PEERS, TRACE_SCOPE_ALL, TRACE_SCOPE_OFF)
486
+ DEFAULT_TRACE_SCOPE = TRACE_SCOPE_PEERS
487
+ # The 0.2 spellings: true (the 0.2 default) reads as peers, false as off.
488
+ _TRUE = ("1", "true", "yes", "on")
489
+ _FALSE = ("0", "false", "no", "off")
490
+ # Where an incoming `traceparent` is continued under the default scope (0.3:
491
+ # `PROPAGATE_TRACE_HEADERS=peers`): the A2A routes only, so a caller of the public routes
492
+ # (`/chat`) cannot choose this agent's trace ids. `all` continues it on every path.
493
+ PEERS_INBOUND_TRACE_PREFIX = "/a2a/"
494
+ _said_true_is_peers = False
495
+
496
+
497
+ def trace_scope() -> str:
498
+ """`PROPAGATE_TRACE_HEADERS`: `peers` (the default), `all` or `off`.
499
+
500
+ `peers`: the request id and trace context go to other agents (`protocol:
501
+ a2a`) and to `auth: forward` and `auth: exchange` APIs (`api_client.propagates`),
502
+ and an incoming `traceparent` is continued on the A2A routes only. `all`: to
503
+ every API, and continued on every path (behind a tracing gateway). `off`:
504
+ neither. `true` (the 0.2 default) reads as `peers`, logged once at INFO, and
505
+ `false` as `off`. Anything else raises `SettingsError` (startup refuses it).
506
+ """
507
+ global _said_true_is_peers
508
+ raw = (os.environ.get("PROPAGATE_TRACE_HEADERS") or "").strip().lower()
509
+ if not raw:
510
+ return DEFAULT_TRACE_SCOPE
511
+ if raw in _FALSE:
512
+ return TRACE_SCOPE_OFF
513
+ if raw in _TRUE:
514
+ if not _said_true_is_peers:
515
+ _said_true_is_peers = True
516
+ logger.info(
517
+ "PROPAGATE_TRACE_HEADERS=%s reads as peers: other agents and the APIs that act "
518
+ "for the user get the request id and trace context, and an incoming trace is "
519
+ "continued on the A2A routes only (all: every API and path)",
520
+ raw,
521
+ )
522
+ return TRACE_SCOPE_PEERS
523
+ if raw in TRACE_SCOPES:
524
+ return raw
525
+ raise SettingsError(
526
+ "PROPAGATE_TRACE_HEADERS must be peers, all or off (true reads as peers, false as off)."
527
+ )
528
+
529
+
530
+ def _scope_or_off() -> str:
531
+ """The scope, or `off` for a value startup would refuse (never propagate on a guess)."""
532
+ try:
533
+ return trace_scope()
534
+ except SettingsError:
535
+ return TRACE_SCOPE_OFF
536
+
537
+
538
+ def propagate_trace_headers() -> bool:
539
+ """Whether the request id and trace context go anywhere (`PROPAGATE_TRACE_HEADERS` is
540
+ not `off`): to the APIs `api_client.propagates` names, or every API under `all`."""
541
+ return _scope_or_off() != TRACE_SCOPE_OFF
542
+
543
+
544
+ def propagate_to_every_api() -> bool:
545
+ """`PROPAGATE_TRACE_HEADERS=all`: every API gets them, not only other agents and the APIs
546
+ that act for the user (`api_client.set_outbound_headers` asks it)."""
547
+ return _scope_or_off() == TRACE_SCOPE_ALL
548
+
549
+
550
+ def _trace_context_propagator() -> Any:
551
+ # W3C Trace Context only, whatever OTEL_PROPAGATORS says: no baggage leaves or enters.
552
+ from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
553
+
554
+ return TraceContextTextMapPropagator()
555
+
556
+
557
+ def outbound_trace_headers() -> dict[str, str]:
558
+ """The headers that carry this request's correlation to the agents it calls.
559
+
560
+ `X-Request-ID`, the id every log record of this request carries (a service
561
+ built from this template takes a caller's id as its own), and, when spans are
562
+ exported over OTLP, the W3C trace context of the current span (`traceparent`,
563
+ `tracestate`), so the callee's spans join this trace. The policy client adds
564
+ them only to calls of other agents (`protocol: a2a`) and of `auth: forward` and
565
+ `auth: exchange` APIs (`api_client.propagates`). Empty
566
+ under `PROPAGATE_TRACE_HEADERS=false`.
567
+ """
568
+ if not propagate_trace_headers():
569
+ return {}
570
+ headers: dict[str, str] = {}
571
+ request_id = LOG_CONTEXT["request_id"].get()
572
+ if request_id:
573
+ headers["X-Request-ID"] = request_id
574
+ if _otlp_active:
575
+ carrier: dict[str, str] = {}
576
+ _trace_context_propagator().inject(carrier, context=_current_span_context())
577
+ headers.update({k: v for k, v in carrier.items() if k in TRACE_CONTEXT_HEADERS})
578
+ return headers
579
+
580
+
581
+ def _current_span_context() -> Any:
582
+ """The OTel context to pass on: the span of the LangChain run in progress (a tool's).
583
+
584
+ OpenInference does not make its spans current in the OTel context (attaching
585
+ one from a callback could leak it to later spans), so inside a tool the OTel
586
+ current span is only what the request itself carried: nothing on a `/chat`
587
+ call that came without a `traceparent`. OpenInference keeps the run's span,
588
+ though, and the callee's spans then nest under the tool span that called it.
589
+ None (the OTel current context) outside a LangChain run.
590
+ """
591
+ try:
592
+ from openinference.instrumentation.langchain import get_current_span
593
+ from opentelemetry import trace
594
+
595
+ span = get_current_span()
596
+ except Exception:
597
+ return None
598
+ if span is None or not span.get_span_context().is_valid:
599
+ return None
600
+ return trace.set_span_in_context(span)
601
+
602
+
603
+ def attach_trace_context(headers: Iterable[tuple[bytes, bytes]], path: str | None = None) -> Any:
604
+ """Continue the caller's trace: attach the W3C trace context of an incoming request.
605
+
606
+ `headers` are the ASGI scope's, `path` the request's path (without the
607
+ app's root path). Returns the token `detach_trace_context` takes, or None
608
+ when nothing was attached: no `traceparent`, spans not exported over
609
+ OTLP, `PROPAGATE_TRACE_HEADERS=off`, or under `peers` (the default) a
610
+ path outside the A2A routes (`PEERS_INBOUND_TRACE_PREFIX`), so a caller
611
+ of the public routes cannot choose this agent's trace ids. A malformed
612
+ `traceparent` starts a new trace, as if there were none.
613
+ """
614
+ if not _otlp_active:
615
+ return None
616
+ scope = _scope_or_off()
617
+ if scope == TRACE_SCOPE_OFF:
618
+ return None
619
+ if scope == TRACE_SCOPE_PEERS and not (path or "").startswith(PEERS_INBOUND_TRACE_PREFIX):
620
+ return None
621
+ carrier: dict[str, str] = {}
622
+ for key, value in headers:
623
+ name = key.decode("latin-1").lower()
624
+ if name in TRACE_CONTEXT_HEADERS:
625
+ carrier[name] = value.decode("latin-1")
626
+ if "traceparent" not in carrier:
627
+ return None
628
+ from opentelemetry import context as otel_context
629
+
630
+ return otel_context.attach(_trace_context_propagator().extract(carrier))
631
+
632
+
633
+ def detach_trace_context(token: Any) -> None:
634
+ """Undo `attach_trace_context` (a None token: nothing was attached)."""
635
+ if token is not None:
636
+ from opentelemetry import context as otel_context
637
+
638
+ otel_context.detach(token)
639
+
640
+
641
+ class _MetadataOnlyExporter:
642
+ """Wraps an exporter and strips exception messages and stack traces from spans.
643
+
644
+ Under `metadata` capture only the error *type* leaves the process: the
645
+ `exception.message` / `exception.stacktrace` event attributes are dropped
646
+ (`exception.type` stays) and an ERROR status loses its description, which
647
+ OpenInference fills with `repr(exc)` plus the traceback (OTLP `status.message`).
648
+ """
649
+
650
+ _DROP = ("exception.message", "exception.stacktrace")
651
+
652
+ def __init__(self, inner: Any) -> None:
653
+ self._inner = inner
654
+
655
+ def export(self, spans: Any) -> Any:
656
+ return self._inner.export([self._redact(s) for s in spans])
657
+
658
+ def shutdown(self) -> None:
659
+ self._inner.shutdown()
660
+
661
+ def force_flush(self, timeout_millis: int = 30000) -> bool:
662
+ return bool(self._inner.force_flush(timeout_millis))
663
+
664
+ def _redact(self, span: Any) -> Any:
665
+ try:
666
+ from opentelemetry.sdk.trace import Event
667
+ from opentelemetry.trace import Status, StatusCode
668
+
669
+ status = getattr(span, "_status", None)
670
+ if status is not None and status.status_code is StatusCode.ERROR and status.description:
671
+ span._status = Status(StatusCode.ERROR)
672
+ events = getattr(span, "_events", None)
673
+ if events:
674
+ redacted = []
675
+ for ev in events:
676
+ attrs = {k: v for k, v in (ev.attributes or {}).items() if k not in self._DROP}
677
+ redacted.append(Event(name=ev.name, attributes=attrs, timestamp=ev.timestamp))
678
+ span._events = redacted
679
+ except Exception: # never fail export over redaction
680
+ logger.debug("span redaction skipped", exc_info=True)
681
+ return span