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,584 @@
1
+ # Copyright 2026 graph-agents-cli contributors
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Structured final answers through the app, in-process (the fastapi runtime).
16
+
17
+ With a response schema (`RESPONSE_SCHEMA_PATH` here), a completed `/chat` run
18
+ ends with the answer in `message.end` (`structured_response`) after exactly
19
+ one `message.delta` holding its JSON text; the answer tool never shows as a
20
+ tool call; an answer that never fits ends the run with the error code
21
+ `invalid_structured_response` and leaves the thread usable. A run that pauses
22
+ for an approval gives its answer once resumed. Over A2A the reply's artifact
23
+ holds the JSON text and a data part with the answer, whether streamed or not,
24
+ and after an approval. Both strategies run on the fake model; every test runs
25
+ with the in-memory checkpointer, and on Postgres when `TEST_POSTGRES_DSN` is
26
+ set. `test_structured_server.py` runs the same under the langgraph-server
27
+ runtime.
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import asyncio
33
+ import json
34
+ import os
35
+ import uuid
36
+ from collections.abc import AsyncIterator
37
+ from pathlib import Path
38
+ from typing import Any
39
+ from urllib.parse import urlsplit
40
+
41
+ os.environ.update(
42
+ {
43
+ "MODEL_PROVIDER": "fake",
44
+ "MODEL_NAME": "fake",
45
+ "CHECKPOINTER": "memory",
46
+ "AUTH_POLICY": "shared-bearer",
47
+ "API_KEY": "test-key",
48
+ "APP_ENV": "dev",
49
+ "TRACING_ENABLED": "false",
50
+ "RUNTIME": "fastapi",
51
+ "APP_URL": "http://testserver",
52
+ }
53
+ )
54
+
55
+ import httpx
56
+ import pytest
57
+ from a2a.client import ClientConfig, create_client
58
+ from a2a.types import GetTaskRequest, Message, Part, Role, SendMessageRequest, TaskState
59
+ from fastapi import HTTPException
60
+ from google.protobuf import json_format
61
+ from langchain.tools import ToolRuntime
62
+ from langchain_core.tools import tool
63
+ from starlette.requests import Request
64
+
65
+ from {{cookiecutter.agent_directory}}.app_utils import a2a as a2a_module
66
+ from {{cookiecutter.agent_directory}}.app_utils import auth as auth_module
67
+ from {{cookiecutter.agent_directory}}.app_utils.api_client import get_client, reset_policy_cache
68
+ from {{cookiecutter.agent_directory}}.app_utils.auth import ACTIONS, Principal
69
+ from {{cookiecutter.agent_directory}}.app_utils.structured import ANSWER_TOOL, validate
70
+ from {{cookiecutter.agent_directory}}.fast_api_app import app
71
+
72
+ A2A_PATH = "/a2a/{{cookiecutter.agent_directory}}"
73
+ A2A_URL = f"http://testserver{A2A_PATH}"
74
+ SCHEMA: dict[str, Any] = {
75
+ "title": "Order answer",
76
+ "type": "object",
77
+ "properties": {
78
+ # The fake answers with its text reply, which holds the request when no tool
79
+ # ran: a request that says FAILME never fits.
80
+ "answer": {"type": "string", "pattern": "^(?![\\s\\S]*FAILME)"},
81
+ "done": {"type": "boolean"},
82
+ "items": {"type": "array", "items": {"type": "string"}},
83
+ },
84
+ "required": ["answer", "done"],
85
+ "additionalProperties": False,
86
+ }
87
+ POLICY = """
88
+ apis:
89
+ shop:
90
+ base_url_env: SHOP_API_BASE_URL
91
+ auth: none
92
+ allowed_methods: [GET, POST]
93
+ approval:
94
+ required_for:
95
+ operations:
96
+ - operationId: cancelOrder
97
+ path: /orders/{order_id}/cancel
98
+ methods: [POST]
99
+ approvers: [requester]
100
+ timeout_s: 60
101
+ """
102
+ SENT: list[httpx.Request] = []
103
+
104
+
105
+ def _upstream(request: httpx.Request) -> httpx.Response:
106
+ SENT.append(request)
107
+ return httpx.Response(200, json={"cancelled": request.url.path})
108
+
109
+
110
+ @tool
111
+ async def cancel_order(order_id: str, runtime: ToolRuntime[Any]) -> str:
112
+ """Cancel an order by its id."""
113
+ context = getattr(runtime, "context", None)
114
+ client = get_client("shop", context=context, transport=httpx.MockTransport(_upstream))
115
+ data = await client.post(
116
+ "/orders/{order_id}/cancel",
117
+ operation_id="cancelOrder",
118
+ path_params={"order_id": order_id},
119
+ json_body={"reason": "asked"},
120
+ )
121
+ return json.dumps(data)
122
+
123
+
124
+ @tool
125
+ def probe(query: str) -> str:
126
+ """Run the probe for a place."""
127
+ return f"{query}: 42"
128
+
129
+
130
+ class HeaderPolicy:
131
+ """Test policy: `X-User` is the principal id."""
132
+
133
+ async def authenticate(self, request: Request) -> Principal:
134
+ user = request.headers.get("x-user")
135
+ if not user:
136
+ raise HTTPException(401, "no user", headers={"WWW-Authenticate": "Bearer"})
137
+ return Principal(id=user, roles=["user"], permissions=set(ACTIONS))
138
+
139
+ async def authorize(self, principal: Principal, action: str, resource: str | None) -> None:
140
+ return None
141
+
142
+
143
+ def _as(user: str) -> dict[str, str]:
144
+ return {"X-User": user}
145
+
146
+
147
+ def parse_sse(text: str) -> list[tuple[str, dict[str, Any]]]:
148
+ events: list[tuple[str, dict[str, Any]]] = []
149
+ event = None
150
+ for line in text.splitlines():
151
+ if line.startswith("event:"):
152
+ event = line[6:].strip()
153
+ elif line.startswith("data:") and event:
154
+ events.append((event, json.loads(line[5:].strip())))
155
+ event = None
156
+ return events
157
+
158
+
159
+ ADMIN_DSN = os.environ.get("TEST_POSTGRES_DSN", "")
160
+
161
+
162
+ @pytest.fixture(params=["memory", "postgres"])
163
+ async def database(
164
+ request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch
165
+ ) -> AsyncIterator[str]:
166
+ if request.param == "memory":
167
+ yield "memory"
168
+ return
169
+ if not ADMIN_DSN:
170
+ pytest.skip("TEST_POSTGRES_DSN is not set")
171
+ import psycopg
172
+
173
+ name = f"gac_test_{uuid.uuid4().hex[:12]}"
174
+ async with await psycopg.AsyncConnection.connect(ADMIN_DSN, autocommit=True) as admin:
175
+ await admin.execute(f'CREATE DATABASE "{name}"')
176
+ monkeypatch.setenv("CHECKPOINTER", "postgres")
177
+ monkeypatch.setenv("POSTGRES_DSN", urlsplit(ADMIN_DSN)._replace(path=f"/{name}").geturl())
178
+ yield "postgres"
179
+ async with await psycopg.AsyncConnection.connect(ADMIN_DSN, autocommit=True) as admin:
180
+ await admin.execute(f'DROP DATABASE IF EXISTS "{name}" WITH (FORCE)')
181
+
182
+
183
+ @pytest.fixture(params=["tool", "provider"])
184
+ async def client(
185
+ request: pytest.FixtureRequest,
186
+ database: str,
187
+ tmp_path: Path,
188
+ monkeypatch: pytest.MonkeyPatch,
189
+ use_test_tools,
190
+ ) -> AsyncIterator[httpx.AsyncClient]:
191
+ schema = tmp_path / "response_schema.json"
192
+ schema.write_text(json.dumps(SCHEMA), encoding="utf-8")
193
+ monkeypatch.setenv("RESPONSE_SCHEMA_PATH", str(schema))
194
+ monkeypatch.setenv("RESPONSE_FORMAT_STRATEGY", request.param)
195
+ policy = tmp_path / "api-policy.yaml"
196
+ policy.write_text(POLICY, encoding="utf-8")
197
+ reset_policy_cache()
198
+ monkeypatch.setenv("API_POLICY_PATH", str(policy))
199
+ monkeypatch.setenv("SHOP_API_BASE_URL", "http://shop.test")
200
+ monkeypatch.setattr(auth_module, "get_policy", lambda: HeaderPolicy())
201
+ SENT.clear()
202
+ async with app.router.lifespan_context(app):
203
+ use_test_tools(cancel_order, probe)
204
+ transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
205
+ async with httpx.AsyncClient(
206
+ transport=transport, base_url="http://testserver", timeout=30
207
+ ) as c:
208
+ started = asyncio.get_running_loop().time()
209
+ while (await c.get("/ready")).status_code != 200: # postgres: the schema set up
210
+ assert asyncio.get_running_loop().time() - started < 30, "not ready"
211
+ await asyncio.sleep(0.1)
212
+ yield c
213
+ reset_policy_cache()
214
+
215
+
216
+ async def _chat(
217
+ client: httpx.AsyncClient, message: str, thread_id: str | None = None
218
+ ) -> list[tuple[str, dict[str, Any]]]:
219
+ body: dict[str, Any] = {"message": message}
220
+ if thread_id:
221
+ body["thread_id"] = thread_id
222
+ r = await client.post("/chat", json=body, headers=_as("alice"))
223
+ assert r.status_code == 200, r.text
224
+ return parse_sse(r.text)
225
+
226
+
227
+ def _answered(events: list[tuple[str, dict[str, Any]]]) -> dict[str, Any]:
228
+ """The run's answer, checking how it was delivered: one delta, its JSON text, then the end."""
229
+ names = [e for e, _ in events]
230
+ assert names[-2:] == ["message.delta", "message.end"], names
231
+ assert names.count("message.delta") == 1, names
232
+ end = events[-1][1]
233
+ assert end["status"] == "ok", end
234
+ answer = end["structured_response"]
235
+ assert json.loads(events[-2][1]["text"]) == answer
236
+ assert validate(SCHEMA, answer) == []
237
+ # The answer tool is how the model answers, never a tool call a client sees.
238
+ assert ANSWER_TOOL not in {d.get("name") for e, d in events if e.startswith("tool.")}
239
+ return answer
240
+
241
+
242
+ async def test_a_completed_run_ends_with_the_answer(client: httpx.AsyncClient) -> None:
243
+ events = await _chat(client, "Run the probe for Paris")
244
+ names = [e for e, _ in events]
245
+ assert names[:3] == ["message.start", "tool.call", "tool.result"], names
246
+ answer = _answered(events)
247
+ assert answer["answer"] == "Here is what I found: Paris: 42" and answer["done"] is False
248
+
249
+
250
+ async def test_every_turn_of_a_thread_has_its_own_answer(client: httpx.AsyncClient) -> None:
251
+ first = await _chat(client, "hello")
252
+ thread_id = first[0][1]["thread_id"]
253
+ assert _answered(first)["answer"] == "Hello! How can I help you today?"
254
+ second = await _chat(client, "Run the probe for Oslo", thread_id)
255
+ assert _answered(second)["answer"] == "Here is what I found: Oslo: 42"
256
+ r = await client.get(f"/threads/{thread_id}/messages", headers=_as("alice"))
257
+ assert r.status_code == 200 and [m["role"] for m in r.json()].count("user") == 2
258
+
259
+
260
+ async def test_an_answer_that_never_fits_fails_the_run_and_the_thread_goes_on(
261
+ client: httpx.AsyncClient,
262
+ ) -> None:
263
+ events = await _chat(client, "FAILME please")
264
+ names = [e for e, _ in events]
265
+ assert names == ["message.start", "error"], events
266
+ error = events[-1][1]
267
+ assert error["code"] == "invalid_structured_response", error
268
+ assert "did not fit the response schema" in error["message"] and error["error_id"]
269
+ # Nothing of the failed step was kept: the thread takes the next message.
270
+ again = await _chat(client, "hello", error.get("thread_id") or events[0][1]["thread_id"])
271
+ assert _answered(again)["answer"] == "Hello! How can I help you today?"
272
+
273
+
274
+ async def test_a_run_paused_for_approval_answers_once_resumed(client: httpx.AsyncClient) -> None:
275
+ paused = await _chat(client, "Cancel the order for 7")
276
+ names = [e for e, _ in paused]
277
+ assert "message.delta" not in names, names # no answer yet
278
+ end = paused[-1][1]
279
+ assert end["status"] == "awaiting_approval" and "structured_response" not in end
280
+ approval = end["approval"]
281
+ r = await client.post(
282
+ f"/threads/{end['thread_id']}/approvals/{approval['approval_id']}",
283
+ json={"decision": "approve"},
284
+ headers=_as("alice"),
285
+ )
286
+ assert r.status_code == 200, r.text
287
+ resumed = parse_sse(r.text)
288
+ answer = _answered(resumed)
289
+ assert "/orders/7/cancel" in answer["answer"] and len(SENT) == 1
290
+
291
+
292
+ def _serve_model(monkeypatch: pytest.MonkeyPatch, model: Any, tools: list[Any]) -> None:
293
+ """Serve a graph on `model` with `tools`, built as agent.py builds it.
294
+
295
+ The answer check is last in the middleware even when the project's agent.py
296
+ predates it: this tests the runtime, not the project's wiring.
297
+ """
298
+ from langchain.agents import create_agent
299
+
300
+ from {{cookiecutter.agent_directory}} import agent
301
+ from {{cookiecutter.agent_directory}}.app_utils.structured import (
302
+ StructuredAnswer,
303
+ response_format,
304
+ )
305
+
306
+ middleware = agent.middleware()
307
+ if not any(isinstance(m, StructuredAnswer) for m in middleware):
308
+ middleware.append(StructuredAnswer())
309
+ graph = create_agent(
310
+ model=model,
311
+ tools=tools,
312
+ system_prompt=agent.SYSTEM_PROMPT,
313
+ middleware=middleware,
314
+ context_schema=agent.AgentContext,
315
+ response_format=response_format(model, tools),
316
+ )
317
+ graph.checkpointer = agent.graph.checkpointer
318
+ monkeypatch.setattr(agent, "graph", graph)
319
+
320
+
321
+ async def test_an_answer_given_beside_a_gated_call_waits_for_the_decision(
322
+ client: httpx.AsyncClient, openai_compatible: Any, monkeypatch: pytest.MonkeyPatch
323
+ ) -> None:
324
+ # A model answers and calls a gated API in one message (parallel tool calls, on by
325
+ # default at OpenAI): nothing is sent before the decision, and the resumed run answers.
326
+ fake = openai_compatible
327
+ _serve_model(monkeypatch, fake.model(), [cancel_order, probe])
328
+ tool_strategy = os.environ["RESPONSE_FORMAT_STRATEGY"] == "tool"
329
+ early = json.dumps({"answer": "I cancelled order 7", "done": True})
330
+ cancel = ("call_cancel", "cancel_order", '{"order_id": "7"}')
331
+ if tool_strategy:
332
+ # The try goes back (its cancel did not run); the model then calls the tool alone.
333
+ fake.queue = [
334
+ (None, [("call_answer", ANSWER_TOOL, early), cancel]),
335
+ (None, [("call_again", "cancel_order", '{"order_id": "7"}')]),
336
+ ]
337
+ else:
338
+ # The provider strategy: a reply with tool calls is no answer, and its calls run.
339
+ fake.queue = [(early, [cancel])]
340
+ paused = await _chat(client, "Cancel the order for 7")
341
+ end = paused[-1][1]
342
+ assert end["status"] == "awaiting_approval" and "structured_response" not in end, paused
343
+ assert [e for e, _ in paused].count("message.delta") == 0 and SENT == []
344
+ done = {"answer": "Cancelled order 7", "done": True}
345
+ fake.queue = [(None, [("call_done", ANSWER_TOOL, json.dumps(done))])]
346
+ if not tool_strategy:
347
+ fake.queue = [(json.dumps(done), [])]
348
+ r = await client.post(
349
+ f"/threads/{end['thread_id']}/approvals/{end['approval']['approval_id']}",
350
+ json={"decision": "approve"},
351
+ headers=_as("alice"),
352
+ )
353
+ assert r.status_code == 200, r.text
354
+ assert _answered(parse_sse(r.text)) == done
355
+ # The cancel was sent once, and every call the model made got a result.
356
+ assert len(SENT) == 1 and fake.refusals == [] and fake.queue == []
357
+
358
+
359
+ async def test_an_agent_not_built_for_its_schema_ends_every_run_with_the_error(
360
+ client: httpx.AsyncClient, monkeypatch: pytest.MonkeyPatch
361
+ ) -> None:
362
+ # A project that added the schema file but not `response_format=` in agent.py (one
363
+ # created before structured answers, say): the runtime still expects an answer.
364
+ from langchain.agents import create_agent
365
+
366
+ from {{cookiecutter.agent_directory}} import agent
367
+ from {{cookiecutter.agent_directory}}.app_utils.model import get_model
368
+
369
+ graph = create_agent(
370
+ model=get_model(),
371
+ tools=[probe],
372
+ system_prompt=agent.SYSTEM_PROMPT,
373
+ middleware=agent.middleware(),
374
+ context_schema=agent.AgentContext,
375
+ )
376
+ graph.checkpointer = agent.graph.checkpointer
377
+ monkeypatch.setattr(agent, "graph", graph)
378
+ events = await _chat(client, "Run the probe for Kyiv")
379
+ # The tool ran and shows; the model's text is not the answer, so nothing else is sent.
380
+ assert [e for e, _ in events] == ["message.start", "tool.call", "tool.result", "error"]
381
+ error = events[-1][1]
382
+ assert error["code"] == "invalid_structured_response", error
383
+ assert "ended without an answer" in error["message"]
384
+
385
+
386
+ async def test_an_unchecked_answer_that_does_not_fit_is_never_delivered(
387
+ client: httpx.AsyncClient, monkeypatch: pytest.MonkeyPatch
388
+ ) -> None:
389
+ # A project whose agent.py passes `response_format=` but has no `StructuredAnswer()`
390
+ # in its middleware (half wired, an upgraded one say): nothing sends a bad answer
391
+ # back to the model, and the runtime refuses to deliver it.
392
+ from langchain.agents import create_agent
393
+
394
+ from {{cookiecutter.agent_directory}} import agent
395
+ from {{cookiecutter.agent_directory}}.app_utils.model import get_model
396
+ from {{cookiecutter.agent_directory}}.app_utils.structured import (
397
+ StructuredAnswer,
398
+ response_format,
399
+ )
400
+
401
+ model = get_model()
402
+ graph = create_agent(
403
+ model=model,
404
+ tools=[probe],
405
+ system_prompt=agent.SYSTEM_PROMPT,
406
+ middleware=[m for m in agent.middleware() if not isinstance(m, StructuredAnswer)],
407
+ context_schema=agent.AgentContext,
408
+ response_format=response_format(model, [probe]),
409
+ )
410
+ graph.checkpointer = agent.graph.checkpointer
411
+ monkeypatch.setattr(agent, "graph", graph)
412
+ events = await _chat(client, "FAILME please")
413
+ assert [e for e, _ in events] == ["message.start", "error"], events
414
+ error = events[-1][1]
415
+ assert error["code"] == "invalid_structured_response", error
416
+ assert "did not fit the response schema" in error["message"] and error["error_id"]
417
+ # An answer that fits is delivered as ever.
418
+ again = await _chat(client, "hello", events[0][1]["thread_id"])
419
+ assert _answered(again)["answer"] == "Hello! How can I help you today?"
420
+
421
+
422
+ # --- A2A ------------------------------------------------------------------------------
423
+
424
+
425
+ async def _a2a(streaming: bool) -> tuple[httpx.AsyncClient, Any]:
426
+ http = httpx.AsyncClient(
427
+ transport=httpx.ASGITransport(app=app),
428
+ base_url="http://testserver",
429
+ headers=_as("alice"),
430
+ timeout=30,
431
+ )
432
+ return http, await create_client(A2A_URL, ClientConfig(streaming=streaming, httpx_client=http))
433
+
434
+
435
+ def _answer_parts(parts: Any) -> tuple[str, dict[str, Any]]:
436
+ """The reply's JSON text and its data part (the answer), checking the pair."""
437
+ text, data = parts
438
+ assert text.HasField("text") and data.HasField("data")
439
+ assert data.media_type == "application/json"
440
+ answer = json_format.MessageToDict(data.data)
441
+ assert json.loads(text.text) == answer
442
+ return text.text, answer
443
+
444
+
445
+ async def test_a2a_send_message_returns_the_answer_as_text_and_data(
446
+ client: httpx.AsyncClient,
447
+ ) -> None:
448
+ http, alice = await _a2a(streaming=False)
449
+ try:
450
+ request = SendMessageRequest(
451
+ message=Message(
452
+ message_id=uuid.uuid4().hex, role=Role.ROLE_USER, parts=[Part(text="hello")]
453
+ )
454
+ )
455
+ task = None
456
+ async for chunk in alice.send_message(request):
457
+ if chunk.HasField("task"):
458
+ task = chunk.task
459
+ assert task is not None and task.status.state == TaskState.TASK_STATE_COMPLETED
460
+ (artifact,) = task.artifacts
461
+ _, answer = _answer_parts(artifact.parts)
462
+ assert answer == {"answer": "Hello! How can I help you today?", "done": False, "items": []}
463
+ finally:
464
+ await http.aclose()
465
+
466
+
467
+ async def test_a2a_streamed_reply_ends_with_the_answer_and_is_stored_whole(
468
+ client: httpx.AsyncClient,
469
+ ) -> None:
470
+ http, alice = await _a2a(streaming=True)
471
+ try:
472
+ request = SendMessageRequest(
473
+ message=Message(
474
+ message_id=uuid.uuid4().hex,
475
+ role=Role.ROLE_USER,
476
+ parts=[Part(text="Run the probe for Lima")],
477
+ )
478
+ )
479
+ updates = [
480
+ chunk.artifact_update
481
+ async for chunk in alice.send_message(request)
482
+ if chunk.HasField("artifact_update")
483
+ ]
484
+ assert updates and updates[-1].last_chunk
485
+ text, answer = _answer_parts(updates[-1].artifact.parts)
486
+ assert answer["answer"] == "Here is what I found: Lima: 42"
487
+ stored = await alice.get_task(GetTaskRequest(id=updates[0].task_id))
488
+ (artifact,) = stored.artifacts
489
+ assert _answer_parts(artifact.parts) == (text, answer)
490
+ finally:
491
+ await http.aclose()
492
+
493
+
494
+ async def test_a2a_task_paused_for_approval_completes_with_the_answer(
495
+ client: httpx.AsyncClient,
496
+ ) -> None:
497
+ async def rpc(params: dict[str, Any]) -> dict[str, Any]:
498
+ r = await client.post(
499
+ A2A_PATH,
500
+ json={"jsonrpc": "2.0", "id": 1, "method": "SendMessage", "params": params},
501
+ headers={**_as("alice"), "A2A-Version": "1.0"},
502
+ )
503
+ assert r.status_code == 200, r.text
504
+ return r.json()["result"]["task"]
505
+
506
+ message = {"messageId": uuid.uuid4().hex, "role": "ROLE_USER"}
507
+ task = await rpc({"message": {**message, "parts": [{"text": "Cancel the order for 9"}]}})
508
+ assert task["status"]["state"] == "TASK_STATE_INPUT_REQUIRED", task
509
+ [request] = [p["data"] for p in task["status"]["message"]["parts"] if "data" in p]
510
+ [approval] = json.loads(request["approval_json"])
511
+ decision = {"approval_id": approval["approval_id"], "decision": "approve"}
512
+ done = await rpc(
513
+ {
514
+ "message": {
515
+ "messageId": uuid.uuid4().hex,
516
+ "role": "ROLE_USER",
517
+ "taskId": task["id"],
518
+ "contextId": task["contextId"],
519
+ "parts": [{"data": decision}],
520
+ }
521
+ }
522
+ )
523
+ assert done["status"]["state"] == "TASK_STATE_COMPLETED", done
524
+ # The paused run's reply (empty) and the resumed run's: the answer is the last one.
525
+ text, data = done["artifacts"][-1]["parts"]
526
+ assert data["mediaType"] == "application/json"
527
+ assert json.loads(text["text"]) == data["data"]
528
+ assert "/orders/9/cancel" in data["data"]["answer"] and len(SENT) == 1
529
+
530
+
531
+ async def test_a2a_task_decided_over_http_completes_with_the_whole_answer(
532
+ client: httpx.AsyncClient, openai_compatible: Any, monkeypatch: pytest.MonkeyPatch
533
+ ) -> None:
534
+ # The person decides outside the task, over HTTP with their own credentials (what
535
+ # `decide_with: direct` asks of an agent's request): the task that waited takes the
536
+ # answer whole, as its last `response` artifact, like a decision sent on the task.
537
+ fake = openai_compatible
538
+ _serve_model(monkeypatch, fake.model(), [cancel_order, probe])
539
+ tool_strategy = os.environ["RESPONSE_FORMAT_STRATEGY"] == "tool"
540
+
541
+ async def rpc(method: str, params: dict[str, Any]) -> dict[str, Any]:
542
+ r = await client.post(
543
+ A2A_PATH,
544
+ json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params},
545
+ headers={**_as("alice"), "A2A-Version": "1.0"},
546
+ )
547
+ assert r.status_code == 200, r.text
548
+ return r.json()["result"]
549
+
550
+ fake.queue = [(None, [("call_cancel", "cancel_order", '{"order_id": "12"}')])]
551
+ message = {"messageId": uuid.uuid4().hex, "role": "ROLE_USER"}
552
+ sent = await rpc("SendMessage", {"message": {**message, "parts": [{"text": "Cancel 12"}]}})
553
+ task = sent["task"]
554
+ assert task["status"]["state"] == "TASK_STATE_INPUT_REQUIRED", task
555
+ [request] = [p["data"] for p in task["status"]["message"]["parts"] if "data" in p]
556
+ [approval] = json.loads(request["approval_json"])
557
+ # Longer than the 2,000 characters of a reply a followed task's status would quote.
558
+ long = {"answer": "Cancelled order 12. " + "x" * 3000, "done": True, "items": ["12"]}
559
+ fake.queue = [(None, [("call_done", ANSWER_TOOL, json.dumps(long))])]
560
+ if not tool_strategy:
561
+ fake.queue = [(json.dumps(long), [])]
562
+ r = await client.post(
563
+ f"/threads/{task['contextId']}/approvals/{approval['approval_id']}",
564
+ json={"decision": "approve"},
565
+ headers=_as("alice"),
566
+ )
567
+ assert r.status_code == 200, r.text
568
+ assert _answered(parse_sse(r.text)) == long and len(SENT) == 1
569
+ followed = await rpc("GetTask", {"id": task["id"]})
570
+ assert followed["status"]["state"] == "TASK_STATE_COMPLETED", followed
571
+ [note] = followed["status"]["message"]["parts"]
572
+ assert note["text"] == (
573
+ f"Approval {approval['approval_id']} was approved outside this task; "
574
+ "the run continued there."
575
+ )
576
+ text, data = followed["artifacts"][-1]["parts"]
577
+ assert followed["artifacts"][-1]["name"] == "response"
578
+ assert data["mediaType"] == "application/json"
579
+ assert json.loads(text["text"]) == data["data"] == long
580
+
581
+
582
+ async def test_the_card_lists_json_among_the_output_modes(client: httpx.AsyncClient) -> None:
583
+ card = a2a_module.agent_card()
584
+ assert list(card.default_output_modes) == ["text/plain", "application/json"]