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,569 @@
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
+ """What an agent calling this one reads and sends over A2A (0.3), and what is never kept.
16
+
17
+ This agent plays `orders`, with a gated `cancelOrder` call to a backend that
18
+ records every request (`httpx.MockTransport`) and a tool that reports the user's
19
+ words its run was given. A test auth policy reads the caller from headers:
20
+ `X-User` is the person, `X-Actor` the agent presenting the request for them
21
+ (`concierge`), so the tests send what that agent would, over A2A JSON-RPC and
22
+ HTTP.
23
+
24
+ * The origin extension: the card declares it; the user's own words an agent
25
+ forwards reach the run (never a direct caller's metadata), and are never
26
+ stored with the task; too many hops fail the task. The run a relayed decision
27
+ resumes acts on the words of the request that paused it, whatever words (if
28
+ any) the decision carries.
29
+ * The approval request: `approval_json` keeps exact values, the text shows the body.
30
+ * A failed or refused task names why in an error part (`thread_busy`).
31
+ * Tasks waiting on an approval follow its outcome (KI-025): a decision sent on the
32
+ context alone, one the person takes over HTTP, a rejection, an expiry; another
33
+ owner's task on the same thread stays as it was.
34
+
35
+ Every test runs with the in-memory checkpointer, and on Postgres when
36
+ `TEST_POSTGRES_DSN` is set.
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ import asyncio
42
+ import json
43
+ import os
44
+ import uuid
45
+ from collections.abc import AsyncIterator
46
+ from datetime import timedelta
47
+ from pathlib import Path
48
+ from typing import Any
49
+ from urllib.parse import urlsplit
50
+
51
+ # The environment must be in place before the app (and the graph) is imported.
52
+ os.environ.update(
53
+ {
54
+ "MODEL_PROVIDER": "fake",
55
+ "MODEL_NAME": "fake",
56
+ "CHECKPOINTER": "memory",
57
+ "AUTH_POLICY": "shared-bearer",
58
+ "API_KEY": "test-key",
59
+ "APP_ENV": "dev",
60
+ "TRACING_ENABLED": "false",
61
+ "RUNTIME": "fastapi",
62
+ "APP_URL": "http://testserver",
63
+ }
64
+ )
65
+
66
+ import httpx
67
+ import pytest
68
+ from fastapi import HTTPException
69
+ from langchain.tools import ToolRuntime
70
+ from langchain_core.tools import tool
71
+ from starlette.requests import Request
72
+
73
+ from {{cookiecutter.agent_directory}}.app_utils import auth as auth_module
74
+ from {{cookiecutter.agent_directory}}.app_utils import chat as chat_module
75
+ from {{cookiecutter.agent_directory}}.app_utils.api_client import (
76
+ A2A_ORIGIN_EXTENSION,
77
+ get_client,
78
+ require_user_mentioned,
79
+ )
80
+ from {{cookiecutter.agent_directory}}.app_utils.api_client import (
81
+ reset_policy_cache as reset_api_policy,
82
+ )
83
+ from {{cookiecutter.agent_directory}}.app_utils.approvals import utcnow
84
+ from {{cookiecutter.agent_directory}}.app_utils.auth import ACTIONS, Actor, Principal
85
+ from {{cookiecutter.agent_directory}}.app_utils.chat import RUNTIME
86
+ from {{cookiecutter.agent_directory}}.fast_api_app import app
87
+
88
+ A2A_PATH = "/a2a/{{cookiecutter.agent_directory}}"
89
+ PROMPT = "Cancel the order for 7"
90
+ BODY = {"reason": "customer asked", "amount": 1, "big": 12345678901234567890}
91
+
92
+ POLICY = """
93
+ apis:
94
+ shop:
95
+ base_url_env: SHOP_API_BASE_URL
96
+ auth: none
97
+ allowed_methods: [GET, POST]
98
+ approval:
99
+ required_for:
100
+ operations:
101
+ - operationId: cancelOrder
102
+ path: /orders/{order_id}/cancel
103
+ methods: [POST]
104
+ approvers: [requester]
105
+ timeout_s: 60
106
+ decide_with: relayed
107
+ relayers: [concierge]
108
+ """
109
+
110
+ # What the backend received.
111
+ SENT: list[httpx.Request] = []
112
+
113
+
114
+ def _backend(request: httpx.Request) -> httpx.Response:
115
+ SENT.append(request)
116
+ return httpx.Response(200, json={"cancelled": request.url.path})
117
+
118
+
119
+ @tool
120
+ async def cancel_order(order_id: str, runtime: ToolRuntime[Any]) -> str:
121
+ """Cancel an order by its id."""
122
+ context = getattr(runtime, "context", None)
123
+ client = get_client("shop", context=context, transport=httpx.MockTransport(_backend))
124
+ data = await client.post(
125
+ "/orders/{order_id}/cancel",
126
+ operation_id="cancelOrder",
127
+ path_params={"order_id": order_id},
128
+ json_body=BODY,
129
+ )
130
+ return json.dumps(data)
131
+
132
+
133
+ # The user's words each run of `cancel_checked` was given (its `@origin`), in order.
134
+ SEEN_ORIGINS: list[Any] = []
135
+
136
+
137
+ @tool
138
+ async def cancel_checked(order_id: str, runtime: ToolRuntime[Any]) -> str:
139
+ """Cancel an order, once the user's own words name it (`require_user_mentioned`)."""
140
+ context = getattr(runtime, "context", None)
141
+ attributes = getattr(context, "attributes", None) or {}
142
+ SEEN_ORIGINS.append((attributes.get("credentials") or {}).get("@origin"))
143
+ require_user_mentioned(order_id, runtime)
144
+ client = get_client("shop", context=context, transport=httpx.MockTransport(_backend))
145
+ data = await client.post(
146
+ "/orders/{order_id}/cancel",
147
+ operation_id="cancelOrder",
148
+ path_params={"order_id": order_id},
149
+ json_body=BODY,
150
+ )
151
+ return json.dumps(data)
152
+
153
+
154
+ @tool
155
+ async def echo_origin(note: str, runtime: ToolRuntime[Any]) -> str:
156
+ """Echo the words of the user this run was given (the origin), if any."""
157
+ context = getattr(runtime, "context", None)
158
+ attributes = getattr(context, "attributes", None) or {}
159
+ origin = (attributes.get("credentials") or {}).get("@origin")
160
+ return "ORIGIN=" + json.dumps(origin)
161
+
162
+
163
+ class ActorHeaderPolicy:
164
+ """Test policy: `X-User` is the subject, `X-Actor` (when sent) the agent presenting it."""
165
+
166
+ async def authenticate(self, request: Request) -> Principal:
167
+ user = request.headers.get("x-user")
168
+ if not user:
169
+ raise HTTPException(401, "no user", headers={"WWW-Authenticate": "Bearer"})
170
+ actor = request.headers.get("x-actor")
171
+ return Principal(
172
+ id=user,
173
+ roles=["user"],
174
+ permissions=set(ACTIONS),
175
+ actor=Actor(id=actor) if actor else None,
176
+ )
177
+
178
+ async def authorize(self, principal: Principal, action: str, resource: str | None) -> None:
179
+ return None
180
+
181
+
182
+ ADMIN_DSN = os.environ.get("TEST_POSTGRES_DSN", "")
183
+
184
+
185
+ @pytest.fixture(params=["memory", "postgres"])
186
+ async def database(
187
+ request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch
188
+ ) -> AsyncIterator[str]:
189
+ if request.param == "memory":
190
+ yield "memory"
191
+ return
192
+ if not ADMIN_DSN:
193
+ pytest.skip("TEST_POSTGRES_DSN is not set")
194
+ import psycopg
195
+
196
+ name = f"gac_test_{uuid.uuid4().hex[:12]}"
197
+ async with await psycopg.AsyncConnection.connect(ADMIN_DSN, autocommit=True) as admin:
198
+ await admin.execute(f'CREATE DATABASE "{name}"')
199
+ monkeypatch.setenv("CHECKPOINTER", "postgres")
200
+ monkeypatch.setenv("POSTGRES_DSN", urlsplit(ADMIN_DSN)._replace(path=f"/{name}").geturl())
201
+ yield "postgres"
202
+ async with await psycopg.AsyncConnection.connect(ADMIN_DSN, autocommit=True) as admin:
203
+ await admin.execute(f'DROP DATABASE IF EXISTS "{name}" WITH (FORCE)')
204
+
205
+
206
+ @pytest.fixture
207
+ async def orders(
208
+ database: str,
209
+ tmp_path: Path,
210
+ monkeypatch: pytest.MonkeyPatch,
211
+ use_test_tools: Any,
212
+ ) -> AsyncIterator[dict[str, Any]]:
213
+ """The app as `orders`: clients for one person, directly and through the concierge."""
214
+ policy = tmp_path / "api-policy.yaml"
215
+ policy.write_text(POLICY, encoding="utf-8")
216
+ monkeypatch.setenv("API_POLICY_PATH", str(policy))
217
+ monkeypatch.setenv("SHOP_API_BASE_URL", "http://shop.test")
218
+ monkeypatch.setenv("AUTH_ALLOWED_ACTORS", "concierge")
219
+ monkeypatch.delenv("TRACE_CAPTURE", raising=False)
220
+ monkeypatch.setattr(auth_module, "get_policy", lambda: ActorHeaderPolicy())
221
+ reset_api_policy()
222
+ SENT.clear()
223
+ user = f"alice-{uuid.uuid4().hex[:8]}" # the A2A task store lives as long as the app
224
+ headers = {
225
+ "person": {"X-User": user},
226
+ "concierge": {"X-User": user, "X-Actor": "concierge"},
227
+ }
228
+ async with app.router.lifespan_context(app):
229
+ use_test_tools(cancel_order, echo_origin)
230
+ transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
231
+ clients = {
232
+ name: httpx.AsyncClient(
233
+ transport=transport, base_url="http://testserver", headers=value, timeout=30
234
+ )
235
+ for name, value in headers.items()
236
+ }
237
+ try:
238
+ started = asyncio.get_running_loop().time()
239
+ while (await clients["person"].get("/ready")).status_code != 200: # the schema
240
+ assert asyncio.get_running_loop().time() - started < 30, "not ready"
241
+ await asyncio.sleep(0.1)
242
+ yield {**clients, "database": database, "user": user}
243
+ finally:
244
+ for client in clients.values():
245
+ await client.aclose()
246
+ reset_api_policy()
247
+
248
+
249
+ async def _rpc(client: httpx.AsyncClient, method: str, params: dict[str, Any]) -> dict[str, Any]:
250
+ r = await client.post(
251
+ A2A_PATH,
252
+ json={"jsonrpc": "2.0", "id": uuid.uuid4().hex, "method": method, "params": params},
253
+ headers={"A2A-Version": "1.0"},
254
+ )
255
+ assert r.status_code == 200, r.text
256
+ return r.json()
257
+
258
+
259
+ def _message(
260
+ *parts: dict[str, Any], task: dict[str, Any] | None = None, **fields: Any
261
+ ) -> dict[str, Any]:
262
+ message: dict[str, Any] = {"messageId": uuid.uuid4().hex, "role": "ROLE_USER", "parts": parts}
263
+ if task is not None:
264
+ message.update(taskId=task["id"], contextId=task["contextId"])
265
+ message.update(fields)
266
+ return {"message": message}
267
+
268
+
269
+ def _data_parts(task: dict[str, Any], kind: str) -> list[dict[str, Any]]:
270
+ return [
271
+ p["data"]
272
+ for p in task["status"]["message"]["parts"]
273
+ if isinstance(p.get("data"), dict) and p["data"].get("type") == kind
274
+ ]
275
+
276
+
277
+ def _note(task: dict[str, Any]) -> str:
278
+ return "".join(p.get("text", "") for p in task["status"]["message"]["parts"])
279
+
280
+
281
+ async def _paused(orders: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
282
+ """The concierge's request to cancel, paused for the person's approval."""
283
+ sent = await _rpc(orders["concierge"], "SendMessage", _message({"text": PROMPT}))
284
+ task = sent["result"]["task"]
285
+ assert task["status"]["state"] == "TASK_STATE_INPUT_REQUIRED", task
286
+ [request] = _data_parts(task, "approval_request")
287
+ [approval] = json.loads(request["approval_json"])
288
+ assert approval["status"] == "pending" and SENT == []
289
+ return task, approval
290
+
291
+
292
+ def _decision(approval: dict[str, Any], decision: str = "approve") -> dict[str, Any]:
293
+ return {
294
+ "data": {
295
+ "approval_id": approval["approval_id"],
296
+ "decision": decision,
297
+ "digest": approval["digest"],
298
+ }
299
+ }
300
+
301
+
302
+ async def _stored_tasks(orders: dict[str, Any]) -> list[str]:
303
+ """Every stored task's JSON text (Postgres); [] under memory."""
304
+ if orders["database"] != "postgres":
305
+ return []
306
+ import psycopg
307
+
308
+ async with await psycopg.AsyncConnection.connect(os.environ["POSTGRES_DSN"]) as conn:
309
+ cursor = await conn.execute("SELECT task::text FROM a2a_tasks")
310
+ return [str(row[0]) for row in await cursor.fetchall()]
311
+
312
+
313
+ async def _stored_approvals(orders: dict[str, Any]) -> list[str]:
314
+ """Every stored approval's payload as JSON text (Postgres); [] under memory."""
315
+ if orders["database"] != "postgres":
316
+ return []
317
+ import psycopg
318
+
319
+ async with await psycopg.AsyncConnection.connect(os.environ["POSTGRES_DSN"]) as conn:
320
+ cursor = await conn.execute("SELECT payload::text FROM approvals")
321
+ return [str(row[0]) for row in await cursor.fetchall()]
322
+
323
+
324
+ # --- the origin extension --------------------------------------------------------------------
325
+
326
+
327
+ async def test_the_card_declares_the_origin_extension(orders: dict[str, Any]) -> None:
328
+ card = (await orders["person"].get(f"{A2A_PATH}/.well-known/agent-card.json")).json()
329
+ [extension] = card["capabilities"]["extensions"]
330
+ assert extension["uri"] == A2A_ORIGIN_EXTENSION
331
+ assert not extension.get("required") # an optional extension: clients may ignore it
332
+
333
+
334
+ async def test_the_users_words_reach_the_run_and_are_never_stored(orders: dict[str, Any]) -> None:
335
+ origin = {"text": "please echo the origin, it is ORD-7", "truncated": False, "hops": 1}
336
+ sent = await _rpc(
337
+ orders["concierge"],
338
+ "SendMessage",
339
+ _message(
340
+ {"text": "Echo the origin"},
341
+ metadata={A2A_ORIGIN_EXTENSION: {"origin": origin}, "other": "kept"},
342
+ ),
343
+ )
344
+ task = sent["result"]["task"]
345
+ assert task["status"]["state"] == "TASK_STATE_COMPLETED", task
346
+ reply = "".join(p["text"] for a in task["artifacts"] for p in a["parts"])
347
+ assert json.loads(reply.split("ORIGIN=", 1)[1].rstrip(".").split("\n")[0]) == origin
348
+ # Not stored: neither in the task the caller reads back nor in the table.
349
+ got = await _rpc(orders["concierge"], "GetTask", {"id": task["id"]})
350
+ held = json.dumps(got["result"])
351
+ assert A2A_ORIGIN_EXTENSION not in held and "ORD-7" not in held.split("ORIGIN=")[0]
352
+ assert got["result"]["history"][0]["metadata"] == {"other": "kept"}
353
+ for stored in await _stored_tasks(orders):
354
+ assert A2A_ORIGIN_EXTENSION not in stored
355
+
356
+
357
+ async def test_the_users_words_are_capped_at_a2a_origin_max_chars(
358
+ orders: dict[str, Any], monkeypatch: pytest.MonkeyPatch
359
+ ) -> None:
360
+ """Whatever the calling agent sends, the run gets at most A2A_ORIGIN_MAX_CHARS of it."""
361
+ monkeypatch.setenv("A2A_ORIGIN_MAX_CHARS", "12")
362
+ origin = {"text": "please echo the origin, it is ORD-7", "truncated": False, "hops": 1}
363
+ sent = await _rpc(
364
+ orders["concierge"],
365
+ "SendMessage",
366
+ _message({"text": "Echo the origin"}, metadata={A2A_ORIGIN_EXTENSION: {"origin": origin}}),
367
+ )
368
+ task = sent["result"]["task"]
369
+ reply = "".join(p["text"] for a in task["artifacts"] for p in a["parts"])
370
+ seen = json.loads(reply.split("ORIGIN=", 1)[1].rstrip(".").split("\n")[0])
371
+ assert seen == {"text": "please echo ", "truncated": True, "hops": 1}
372
+
373
+
374
+ async def test_a_persons_own_metadata_is_not_read_as_forwarded_words(
375
+ orders: dict[str, Any],
376
+ ) -> None:
377
+ """A direct caller's message is the person's own words: no origin is taken from it."""
378
+ origin = {"text": "someone else's words", "truncated": False, "hops": 1}
379
+ sent = await _rpc(
380
+ orders["person"],
381
+ "SendMessage",
382
+ _message({"text": "Echo the origin"}, metadata={A2A_ORIGIN_EXTENSION: {"origin": origin}}),
383
+ )
384
+ task = sent["result"]["task"]
385
+ reply = "".join(p["text"] for a in task["artifacts"] for p in a["parts"])
386
+ assert "ORIGIN=null" in reply
387
+
388
+
389
+ async def test_words_forwarded_through_too_many_agents_fail_the_task(
390
+ orders: dict[str, Any],
391
+ ) -> None:
392
+ origin = {"text": "cancel ORD-7", "truncated": False, "hops": 4} # the default depth is 3
393
+ sent = await _rpc(
394
+ orders["concierge"],
395
+ "SendMessage",
396
+ _message({"text": PROMPT}, metadata={A2A_ORIGIN_EXTENSION: {"origin": origin}}),
397
+ )
398
+ task = sent["result"]["task"]
399
+ assert task["status"]["state"] == "TASK_STATE_FAILED"
400
+ assert "delegation chain too deep (AUTH_MAX_DELEGATION_DEPTH=3)" in _note(task)
401
+ assert _data_parts(task, "error") == [{"type": "error", "code": "delegation_too_deep"}]
402
+ assert SENT == []
403
+
404
+
405
+ @pytest.mark.parametrize("approving_words", ["yes, go ahead", None])
406
+ async def test_the_run_a_decision_resumes_acts_on_the_words_of_the_request_that_paused(
407
+ orders: dict[str, Any], use_test_tools: Any, approving_words: str | None
408
+ ) -> None:
409
+ """A decision an agent relays carries the words the person said when approving there
410
+ ("yes, go ahead"), or none at all; the run it resumes still acts on the words of the
411
+ request that paused it. So a check against them holds on the resumed run too, and an
412
+ agent that relays an approval one level further rebuilds the very call the person saw.
413
+ The words are kept with the approval while it waits, and dropped once it is decided."""
414
+ use_test_tools(cancel_checked)
415
+ SEEN_ORIGINS.clear()
416
+ asked = {"text": "please cancel order 7", "truncated": False, "hops": 1}
417
+ sent = await _rpc(
418
+ orders["concierge"],
419
+ "SendMessage",
420
+ _message({"text": PROMPT}, metadata={A2A_ORIGIN_EXTENSION: {"origin": asked}}),
421
+ )
422
+ task = sent["result"]["task"]
423
+ assert task["status"]["state"] == "TASK_STATE_INPUT_REQUIRED", task
424
+ [request] = _data_parts(task, "approval_request")
425
+ [approval] = json.loads(request["approval_json"])
426
+ assert "origin" not in approval and asked["text"] not in json.dumps(request)
427
+ listed = await orders["concierge"].get(f"/threads/{task['contextId']}/approvals")
428
+ assert listed.status_code == 200 and asked["text"] not in listed.text
429
+ fields: dict[str, Any] = {"contextId": task["contextId"], "referenceTaskIds": [task["id"]]}
430
+ if approving_words is not None:
431
+ said = {"text": approving_words, "truncated": False, "hops": 1}
432
+ fields["metadata"] = {A2A_ORIGIN_EXTENSION: {"origin": said}}
433
+ decided = await _rpc(
434
+ orders["concierge"], "SendMessage", _message(_decision(approval), **fields)
435
+ )
436
+ carrier = decided["result"]["task"]
437
+ assert carrier["status"]["state"] == "TASK_STATE_COMPLETED", carrier
438
+ assert [(r.method, r.url.path) for r in SENT] == [("POST", "/orders/7/cancel")]
439
+ assert SEEN_ORIGINS == [asked, asked]
440
+ stored = await _stored_approvals(orders)
441
+ assert all(asked["text"] not in payload for payload in stored)
442
+ if orders["database"] == "postgres":
443
+ assert len(stored) == 1
444
+
445
+
446
+ # --- what an agent reads: exact values, and why a task failed ---------------------------------
447
+
448
+
449
+ async def test_the_approval_request_holds_exact_values_and_shows_the_body(
450
+ orders: dict[str, Any],
451
+ ) -> None:
452
+ task, approval = await _paused(orders)
453
+ assert approval["body"] == BODY # approval_json: exact, the big integer included
454
+ [request] = _data_parts(task, "approval_request")
455
+ assert request["approvals"][0]["body"]["amount"] == 1.0 # the Struct: a double
456
+ assert request["approvals"][0]["decide_with"] == "relayed"
457
+ assert '"big": 12345678901234567890' in _note(task)
458
+ assert "until" in _note(task) and approval["digest"] in json.dumps(request)
459
+
460
+
461
+ async def test_a_busy_thread_fails_the_task_with_an_error_part(
462
+ orders: dict[str, Any], monkeypatch: pytest.MonkeyPatch
463
+ ) -> None:
464
+ async def busy(self: Any, *args: Any, **kwargs: Any) -> Any:
465
+ yield chat_module.EVENT_ERROR, chat_module.thread_busy_error()
466
+
467
+ monkeypatch.setattr(chat_module.ChatRuntime, "stream", busy)
468
+ sent = await _rpc(orders["concierge"], "SendMessage", _message({"text": "hello"}))
469
+ task = sent["result"]["task"]
470
+ assert task["status"]["state"] == "TASK_STATE_FAILED"
471
+ assert _data_parts(task, "error") == [{"type": "error", "code": "thread_busy"}]
472
+
473
+
474
+ # --- tasks follow the approval's outcome (KI-025) ---------------------------------------------
475
+
476
+
477
+ async def test_a_task_follows_a_decision_sent_on_its_context(orders: dict[str, Any]) -> None:
478
+ """The relay's decision names the context and the task it answers, not the task: it runs
479
+ as a new task, and the task that waited ends as that run did, pointing at it."""
480
+ task, approval = await _paused(orders)
481
+ decided = await _rpc(
482
+ orders["concierge"],
483
+ "SendMessage",
484
+ _message(_decision(approval), contextId=task["contextId"], referenceTaskIds=[task["id"]]),
485
+ )
486
+ carrier = decided["result"]["task"]
487
+ assert carrier["id"] != task["id"]
488
+ assert carrier["status"]["state"] == "TASK_STATE_COMPLETED", carrier
489
+ assert [(r.method, r.url.path) for r in SENT] == [("POST", "/orders/7/cancel")]
490
+ waited = (await _rpc(orders["concierge"], "GetTask", {"id": task["id"]}))["result"]
491
+ assert waited["status"]["state"] == "TASK_STATE_COMPLETED"
492
+ assert _note(waited).startswith(f"Continued in task {carrier['id']}.")
493
+ # The old status (what it waited for) is kept in the history.
494
+ assert any("Waiting for approval" in json.dumps(m) for m in waited["history"])
495
+
496
+
497
+ async def test_a_task_follows_a_rejection(orders: dict[str, Any]) -> None:
498
+ task, approval = await _paused(orders)
499
+ decided = await _rpc(
500
+ orders["concierge"],
501
+ "SendMessage",
502
+ _message(_decision(approval, "reject"), contextId=task["contextId"]),
503
+ )
504
+ carrier = decided["result"]["task"]
505
+ waited = (await _rpc(orders["concierge"], "GetTask", {"id": task["id"]}))["result"]
506
+ assert waited["status"]["state"] == "TASK_STATE_COMPLETED"
507
+ assert _note(waited).startswith(
508
+ f"Approval {approval['approval_id']} was rejected. Continued in task {carrier['id']}."
509
+ )
510
+ assert SENT == []
511
+
512
+
513
+ async def test_a_task_follows_a_decision_the_person_takes_over_http(
514
+ orders: dict[str, Any],
515
+ ) -> None:
516
+ """The person approves at this agent directly (the KI-025 case): the agent's task, under
517
+ another owner key than the person's, ends too, with the run's reply."""
518
+ task, approval = await _paused(orders)
519
+ r = await orders["person"].post(
520
+ f"/threads/{task['contextId']}/approvals/{approval['approval_id']}",
521
+ json={"decision": "approve"},
522
+ )
523
+ assert r.status_code == 200 and "message.end" in r.text
524
+ waited = (await _rpc(orders["concierge"], "GetTask", {"id": task["id"]}))["result"]
525
+ assert waited["status"]["state"] == "TASK_STATE_COMPLETED"
526
+ note = _note(waited)
527
+ assert note.startswith(
528
+ f"Approval {approval['approval_id']} was approved outside this task; the run "
529
+ "continued there."
530
+ )
531
+ assert "Here is what I found" in note # the resumed run's reply
532
+
533
+
534
+ async def test_a_task_follows_an_expiry(orders: dict[str, Any]) -> None:
535
+ task, approval = await _paused(orders)
536
+ store = RUNTIME.approvals
537
+ assert store is not None
538
+ later = utcnow() + timedelta(hours=1)
539
+ original = store._clock
540
+ store._clock = lambda: later
541
+ try:
542
+ await RUNTIME.sweep_approvals()
543
+ finally:
544
+ store._clock = original
545
+ waited = (await _rpc(orders["concierge"], "GetTask", {"id": task["id"]}))["result"]
546
+ assert waited["status"]["state"] == "TASK_STATE_FAILED"
547
+ assert _note(waited) == f"Approval {approval['approval_id']} expired before anyone decided."
548
+ assert SENT == []
549
+
550
+
551
+ async def test_another_owners_task_on_the_thread_is_left_alone(orders: dict[str, Any]) -> None:
552
+ """The person asks on their agent's thread while it waits: their task waits too, under
553
+ their own key. The agent's decision ends the agent's task only."""
554
+ task, approval = await _paused(orders)
555
+ asked = await _rpc(
556
+ orders["person"], "SendMessage", _message({"text": "hello"}, contextId=task["contextId"])
557
+ )
558
+ persons = asked["result"]["task"]
559
+ assert persons["status"]["state"] == "TASK_STATE_INPUT_REQUIRED"
560
+ assert _data_parts(persons, "error") == [{"type": "error", "code": "approval_pending"}]
561
+ await _rpc(
562
+ orders["concierge"],
563
+ "SendMessage",
564
+ _message(_decision(approval), contextId=task["contextId"]),
565
+ )
566
+ waited = (await _rpc(orders["concierge"], "GetTask", {"id": task["id"]}))["result"]
567
+ assert waited["status"]["state"] == "TASK_STATE_COMPLETED"
568
+ left = (await _rpc(orders["person"], "GetTask", {"id": persons["id"]}))["result"]
569
+ assert left["status"]["state"] == "TASK_STATE_INPUT_REQUIRED"