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,637 @@
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
+ """Postgres behaviour: schema setup across replicas, run locks, atomic claims, pool healing,
16
+ and the A2A task store (shared by replicas, kept across restarts).
17
+
18
+ Opt-in: set `TEST_POSTGRES_DSN` to the URL of a server where the user may
19
+ create databases (each test gets a fresh one, dropped afterwards), for example
20
+ `postgresql://agent:agent@localhost:5432/postgres`.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import asyncio
26
+ import os
27
+ import uuid
28
+ from collections.abc import AsyncIterator
29
+ from typing import Any
30
+
31
+ import pytest
32
+ from a2a.server.context import ServerCallContext
33
+ from a2a.types import ListTasksRequest, Task, TaskState
34
+ from google.protobuf import json_format
35
+
36
+ from {{cookiecutter.agent_directory}}.app_utils import chat as chat_module
37
+ from {{cookiecutter.agent_directory}}.app_utils.a2a import (
38
+ INTERRUPTED_TASK_TEXT,
39
+ ExpiringTaskStore,
40
+ PostgresTaskStore,
41
+ PrincipalUser,
42
+ )
43
+ from {{cookiecutter.agent_directory}}.app_utils.approvals import ApprovalStore, may_decide
44
+ from {{cookiecutter.agent_directory}}.app_utils.auth import Actor, Principal
45
+ from {{cookiecutter.agent_directory}}.app_utils.checkpointer import POSTGRES, get_checkpointer
46
+ from {{cookiecutter.agent_directory}}.app_utils.db import Database, RunRecord, RunStore
47
+ from {{cookiecutter.agent_directory}}.app_utils.threads import ThreadBusy, ThreadLocks, ThreadStore
48
+
49
+ ADMIN_DSN = os.environ.get("TEST_POSTGRES_DSN", "")
50
+ pytestmark = pytest.mark.skipif(not ADMIN_DSN, reason="TEST_POSTGRES_DSN is not set")
51
+
52
+
53
+ def _with_database(dsn: str, name: str) -> str:
54
+ """The same server URL, pointing at database `name`."""
55
+ from urllib.parse import urlsplit
56
+
57
+ return urlsplit(dsn)._replace(path=f"/{name}").geturl()
58
+
59
+
60
+ @pytest.fixture
61
+ async def dsn(monkeypatch: pytest.MonkeyPatch) -> AsyncIterator[str]:
62
+ import psycopg
63
+
64
+ name = f"gac_test_{uuid.uuid4().hex[:12]}"
65
+ async with await psycopg.AsyncConnection.connect(ADMIN_DSN, autocommit=True) as admin:
66
+ await admin.execute(f'CREATE DATABASE "{name}"')
67
+ url = _with_database(ADMIN_DSN, name)
68
+ monkeypatch.setenv("CHECKPOINTER", "postgres")
69
+ monkeypatch.setenv("POSTGRES_DSN", url)
70
+ yield url
71
+ async with await psycopg.AsyncConnection.connect(ADMIN_DSN, autocommit=True) as admin:
72
+ await admin.execute(f'DROP DATABASE IF EXISTS "{name}" WITH (FORCE)')
73
+
74
+
75
+ async def _terminate_other_sessions(dsn: str) -> int:
76
+ import psycopg
77
+
78
+ async with await psycopg.AsyncConnection.connect(dsn, autocommit=True) as conn:
79
+ cur = await conn.execute(
80
+ "SELECT count(pg_terminate_backend(pid)) FROM pg_stat_activity "
81
+ "WHERE datname = current_database() AND pid <> pg_backend_pid()"
82
+ )
83
+ row = await cur.fetchone()
84
+ return int(row[0]) if row else 0
85
+
86
+
87
+ async def test_replicas_starting_together_set_up_the_schema_once(dsn: str) -> None:
88
+ async def replica() -> None:
89
+ db = Database(POSTGRES, dsn)
90
+ await db.open()
91
+ try:
92
+ async with get_checkpointer(db.pool) as saver:
93
+ await saver.setup()
94
+ finally:
95
+ await db.close()
96
+
97
+ await asyncio.gather(*(replica() for _ in range(4)))
98
+ db = Database(POSTGRES, dsn)
99
+ await db.open()
100
+ try:
101
+ row = await db.fetchone("SELECT count(*) AS n FROM checkpoint_migrations")
102
+ assert row is not None and row["n"] > 0
103
+ finally:
104
+ await db.close()
105
+
106
+
107
+ async def test_an_older_schema_is_migrated_in_place(dsn: str) -> None:
108
+ import psycopg
109
+
110
+ async with await psycopg.AsyncConnection.connect(dsn, autocommit=True) as conn:
111
+ await conn.execute(
112
+ "CREATE TABLE runs (run_id TEXT PRIMARY KEY, thread_id TEXT NOT NULL, "
113
+ "principal_hash TEXT NOT NULL, model TEXT, status TEXT NOT NULL, "
114
+ "input_tokens INTEGER, output_tokens INTEGER, latency_ms INTEGER, "
115
+ "error_type TEXT, payload JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT now())"
116
+ )
117
+ await conn.execute(
118
+ "CREATE TABLE threads (thread_id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, "
119
+ "tenant TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now())"
120
+ )
121
+ await conn.execute("INSERT INTO threads (thread_id, principal_id) VALUES ('old', 'a')")
122
+ db = Database(POSTGRES, dsn)
123
+ await db.open()
124
+ try:
125
+ store = ThreadStore(db)
126
+ record = await store.get("old")
127
+ assert record is not None and record.updated_at
128
+ runs = RunStore(db)
129
+ await runs.record(
130
+ RunRecord(
131
+ run_id="r1",
132
+ thread_id="old",
133
+ principal_hash="h",
134
+ model="m",
135
+ status="ok",
136
+ metadata={"source": "web"},
137
+ )
138
+ )
139
+ stored = await runs.get("r1")
140
+ assert stored is not None and stored.metadata == {"source": "web"}
141
+ finally:
142
+ await db.close()
143
+
144
+
145
+ async def test_the_run_lock_spans_replicas(dsn: str) -> None:
146
+ db = Database(POSTGRES, dsn)
147
+ await db.open() # creates the lease table
148
+ await db.close()
149
+ replica_a, replica_b = ThreadLocks(dsn), ThreadLocks(dsn)
150
+ try:
151
+ lease = await replica_a.acquire("t1")
152
+ with pytest.raises(ThreadBusy):
153
+ await replica_b.acquire("t1")
154
+ other = await replica_b.acquire("t2")
155
+ await lease.release()
156
+ again = await replica_b.acquire("t1")
157
+ await again.release()
158
+ await other.release()
159
+ finally:
160
+ await replica_a.close()
161
+ await replica_b.close()
162
+
163
+
164
+ async def test_claims_are_atomic_across_connections(dsn: str) -> None:
165
+ db = Database(POSTGRES, dsn)
166
+ await db.open()
167
+ try:
168
+ store = ThreadStore(db)
169
+ alice, bob = Principal(id="alice"), Principal(id="bob")
170
+ for i in range(20):
171
+ thread_id = f"race-{i}"
172
+ results = await asyncio.gather(
173
+ store.claim(thread_id, alice), store.claim(thread_id, bob)
174
+ )
175
+ assert sorted(created for _, created in results) == [False, True]
176
+ owners = {record.principal_id for record, _ in results}
177
+ assert len(owners) == 1 # both calls see the same, single owner
178
+ finally:
179
+ await db.close()
180
+
181
+
182
+ async def test_pool_and_run_locks_heal_after_connections_are_killed(dsn: str) -> None:
183
+ """What a Postgres restart or failover does to live connections, without the restart."""
184
+ db = Database(POSTGRES, dsn)
185
+ await db.open()
186
+ locks = ThreadLocks(dsn)
187
+ try:
188
+ await asyncio.gather(*(db.fetchone("SELECT pg_sleep(0.05)") for _ in range(5)))
189
+ lease = await locks.acquire("t1")
190
+ await lease.release()
191
+ assert await _terminate_other_sessions(dsn) >= 2
192
+ for _ in range(5):
193
+ assert await db.fetchone("SELECT 1 AS ok") == {"ok": 1}
194
+ lease = await locks.acquire("t1")
195
+ await lease.release()
196
+ finally:
197
+ await locks.close()
198
+ await db.close()
199
+
200
+
201
+ async def test_server_runtime_run_records_use_their_own_table(
202
+ dsn: str, monkeypatch: pytest.MonkeyPatch
203
+ ) -> None:
204
+ monkeypatch.setenv("DATABASE_URI", dsn)
205
+ db = Database.for_server()
206
+ await db.open()
207
+ try:
208
+ tables = await db.fetchall(
209
+ "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'"
210
+ )
211
+ names = {t["table_name"] for t in tables}
212
+ assert "agent_runs" in names and "threads" not in names and "runs" not in names
213
+ runs = RunStore(db)
214
+ await runs.record(
215
+ RunRecord(run_id="r1", thread_id="t", principal_hash="h", model="m", status="ok")
216
+ )
217
+ await runs.delete_for_thread("t")
218
+ assert await runs.get("r1") is None
219
+ finally:
220
+ await db.close()
221
+
222
+
223
+ async def test_run_record_pages_reach_every_thread(dsn: str) -> None:
224
+ """`thread_ids_before` pages in id order, so a sweep reaches ids past the first page."""
225
+ db = Database(POSTGRES, dsn)
226
+ await db.open()
227
+ try:
228
+ runs = RunStore(db)
229
+ old = "2000-01-01T00:00:00+00:00"
230
+ ids = [f"t-{i:02d}" for i in range(7)]
231
+ for n, thread_id in enumerate(ids + ids): # two records per thread
232
+ await runs.record(
233
+ RunRecord(
234
+ run_id=f"r{n}",
235
+ thread_id=thread_id,
236
+ principal_hash="h",
237
+ model="m",
238
+ status="ok",
239
+ created_at=old,
240
+ )
241
+ )
242
+ await runs.record(
243
+ RunRecord(run_id="new", thread_id="t-new", principal_hash="h", model="m", status="ok")
244
+ )
245
+ pages: list[list[str]] = []
246
+ after = None
247
+ while True:
248
+ page = await runs.thread_ids_before("2020-01-01T00:00:00+00:00", after=after, limit=3)
249
+ pages.append(page)
250
+ if len(page) < 3:
251
+ break
252
+ after = page[-1]
253
+ assert [t for page in pages for t in page] == ids # each once, in order, no new one
254
+ finally:
255
+ await db.close()
256
+
257
+
258
+ async def test_an_id_with_state_but_no_owner_row_is_never_claimed(dsn: str) -> None:
259
+ db = Database(POSTGRES, dsn)
260
+ await db.open()
261
+ try:
262
+ store = ThreadStore(db)
263
+ alice, bob = Principal(id="alice"), Principal(id="bob")
264
+
265
+ async def orphan_only(thread_id: str) -> bool:
266
+ return thread_id == "orphan"
267
+
268
+ async def always(thread_id: str) -> bool:
269
+ return True
270
+
271
+ with pytest.raises(Exception) as exc:
272
+ await store.ensure("orphan", bob, has_state=orphan_only)
273
+ assert getattr(exc.value, "status_code", None) == 403
274
+ assert await store.get("orphan") is None
275
+ assert (await store.ensure("fresh", alice, has_state=orphan_only)).principal_id == "alice"
276
+ # A thread with an owner row is its owner's to continue, state or not.
277
+ assert (await store.ensure("fresh", alice, has_state=always)).principal_id == "alice"
278
+ with pytest.raises(Exception) as exc:
279
+ await store.ensure("fresh", bob, has_state=always)
280
+ assert getattr(exc.value, "status_code", None) == 403
281
+ finally:
282
+ await db.close()
283
+
284
+
285
+ # --- A2A tasks ----------------------------------------------------------------------------
286
+
287
+
288
+ def _ctx(owner: str) -> ServerCallContext:
289
+ return ServerCallContext(user=PrincipalUser(owner))
290
+
291
+
292
+ def _a2a_task(
293
+ task_id: str, context_id: str = "c1", state: int = TaskState.TASK_STATE_COMPLETED, **extra: Any
294
+ ) -> Task:
295
+ task = Task(id=task_id, context_id=context_id)
296
+ task.status.state = state
297
+ if "second" in extra:
298
+ task.status.timestamp.FromSeconds(1_800_000_000 + extra["second"])
299
+ if "text" in extra:
300
+ artifact = task.artifacts.add()
301
+ artifact.artifact_id = "a1"
302
+ artifact.parts.add().text = extra["text"]
303
+ return task
304
+
305
+
306
+ async def _opened(dsn: str) -> Database:
307
+ db = Database(POSTGRES, dsn)
308
+ await db.open() # creates the tables, the A2A task table included
309
+ return db
310
+
311
+
312
+ async def test_a2a_tasks_are_shared_by_replicas_and_kept_across_restarts(dsn: str) -> None:
313
+ replica_a, replica_b = await _opened(dsn), await _opened(dsn)
314
+ try:
315
+ store_a = PostgresTaskStore(3600, replica_a)
316
+ store_b = PostgresTaskStore(3600, replica_b)
317
+ task = _a2a_task("t1", "orders-thread.billing" * 3, second=5, text="Order 1 is on its way.")
318
+ await store_a.save(task, _ctx("alice"))
319
+ seen = await store_b.get("t1", _ctx("alice"))
320
+ assert seen is not None
321
+ assert json_format.MessageToDict(seen) == json_format.MessageToDict(task)
322
+ assert await store_b.get("t1", _ctx("bob")) is None # another principal: not found
323
+ assert (await store_b.list(ListTasksRequest(), _ctx("alice"))).total_size == 1
324
+ assert (await store_b.list(ListTasksRequest(), _ctx("bob"))).total_size == 0
325
+ finally:
326
+ await replica_a.close()
327
+ restarted = await _opened(dsn)
328
+ try:
329
+ assert await PostgresTaskStore(3600, restarted).get("t1", _ctx("alice")) is not None
330
+ await replica_b.close()
331
+ finally:
332
+ await restarted.close()
333
+
334
+
335
+ async def test_a2a_task_listing_matches_the_in_memory_store(dsn: str) -> None:
336
+ """Filters, order (newest status first, no timestamp last), pages and totals as the SDK's."""
337
+ db = await _opened(dsn)
338
+ try:
339
+ postgres = PostgresTaskStore(0, db)
340
+ memory = ExpiringTaskStore(0)
341
+ tasks = [
342
+ _a2a_task("t1", "c1", TaskState.TASK_STATE_COMPLETED, second=10),
343
+ _a2a_task("t2", "c1", TaskState.TASK_STATE_INPUT_REQUIRED, second=30),
344
+ _a2a_task("t3", "c2", TaskState.TASK_STATE_COMPLETED, second=20),
345
+ _a2a_task("t4", "c2", TaskState.TASK_STATE_FAILED),
346
+ _a2a_task("t5", "c1", TaskState.TASK_STATE_COMPLETED, second=30),
347
+ _a2a_task("t6", "c1", TaskState.TASK_STATE_COMPLETED),
348
+ ]
349
+ for task in tasks:
350
+ for store in (postgres, memory):
351
+ await store.save(task, _ctx("alice"))
352
+ await postgres.save(_a2a_task("t7", "c1", second=40), _ctx("bob"))
353
+ after = _a2a_task("x", second=20).status.timestamp
354
+ requests = [
355
+ ListTasksRequest(),
356
+ ListTasksRequest(context_id="c1"),
357
+ ListTasksRequest(status=TaskState.TASK_STATE_COMPLETED),
358
+ ListTasksRequest(status_timestamp_after=after),
359
+ ]
360
+ for request in requests:
361
+ for page_size in (2, 3, 50):
362
+ pages: dict[str, list[list[str]]] = {}
363
+ for name, store in (("postgres", postgres), ("memory", memory)):
364
+ got: list[list[str]] = []
365
+ token = ""
366
+ while True:
367
+ page_request = ListTasksRequest()
368
+ page_request.CopyFrom(request)
369
+ page_request.page_size = page_size
370
+ page_request.page_token = token
371
+ page = await store.list(page_request, _ctx("alice"))
372
+ got.append([t.id for t in page.tasks] + [f"total={page.total_size}"])
373
+ token = page.next_page_token
374
+ if not token:
375
+ break
376
+ pages[name] = got
377
+ assert pages["postgres"] == pages["memory"], (request, page_size)
378
+ finally:
379
+ await db.close()
380
+
381
+
382
+ async def test_a_persons_listing_spans_their_agents_keys_as_the_in_memory_store_does(
383
+ dsn: str,
384
+ ) -> None:
385
+ """A person (a direct caller) lists their own tasks and those their agents started for
386
+ them, filtered, ordered and paged as the in-memory store does; no other subject's keys
387
+ (a longer or shorter id, an agent named like the person) come in."""
388
+ db = await _opened(dsn)
389
+ try:
390
+ postgres = PostgresTaskStore(0, db)
391
+ memory = ExpiringTaskStore(0)
392
+ rows = [
393
+ ("alice", _a2a_task("t1", "c1", TaskState.TASK_STATE_COMPLETED, second=10)),
394
+ ("alice\x1fconcierge", _a2a_task("t2", "c1", TaskState.TASK_STATE_WORKING, second=30)),
395
+ ("alice\x1fbilling", _a2a_task("t3", "c2", TaskState.TASK_STATE_COMPLETED, second=20)),
396
+ ("alice\x1fclient:web", _a2a_task("t4", "c2", TaskState.TASK_STATE_FAILED)),
397
+ (
398
+ "alice\x1fconcierge",
399
+ _a2a_task("t5", "c1", TaskState.TASK_STATE_COMPLETED, second=30),
400
+ ),
401
+ ("alicex", _a2a_task("x1", "c1", second=40)),
402
+ ("alicex\x1fconcierge", _a2a_task("x2", "c1", second=40)),
403
+ ("alic", _a2a_task("x3", "c1", second=40)),
404
+ ("bob\x1falice", _a2a_task("x4", "c1", second=40)),
405
+ ]
406
+ for owner, task in rows:
407
+ for store in (postgres, memory):
408
+ await store.save(task, _ctx(owner))
409
+ person = ServerCallContext(user=PrincipalUser("alice", subject="alice"))
410
+ after = _a2a_task("x", second=20).status.timestamp
411
+ requests = [
412
+ ListTasksRequest(),
413
+ ListTasksRequest(context_id="c1"),
414
+ ListTasksRequest(status=TaskState.TASK_STATE_COMPLETED),
415
+ ListTasksRequest(status_timestamp_after=after),
416
+ ]
417
+ for request in requests:
418
+ for page_size in (1, 2, 50):
419
+ pages: dict[str, list[list[str]]] = {}
420
+ for name, store in (("postgres", postgres), ("memory", memory)):
421
+ got: list[list[str]] = []
422
+ token = ""
423
+ while True:
424
+ page_request = ListTasksRequest()
425
+ page_request.CopyFrom(request)
426
+ page_request.page_size = page_size
427
+ page_request.page_token = token
428
+ page = await store.list(page_request, person)
429
+ got.append([t.id for t in page.tasks] + [f"total={page.total_size}"])
430
+ token = page.next_page_token
431
+ if not token:
432
+ break
433
+ pages[name] = got
434
+ assert pages["postgres"] == pages["memory"], (request, page_size)
435
+ whole = await postgres.list(ListTasksRequest(), person)
436
+ assert [t.id for t in whole.tasks] == ["t5", "t2", "t3", "t1", "t4"]
437
+ # The same key without the person's scope: its own tasks only, as before.
438
+ assert [t.id for t in (await postgres.list(ListTasksRequest(), _ctx("alice"))).tasks] == [
439
+ "t1"
440
+ ]
441
+ # Whose task is it, among the person's keys (their own first)?
442
+ for store in (postgres, memory):
443
+ assert await store.owner_for_subject("t2", "alice") == "alice\x1fconcierge"
444
+ assert await store.owner_for_subject("t1", "alice") == "alice"
445
+ for other in ("x1", "x2", "x3", "x4", "missing"):
446
+ assert await store.owner_for_subject(other, "alice") is None
447
+ assert await store.owner_for_subject("t2", "alicex") is None
448
+ finally:
449
+ await db.close()
450
+
451
+
452
+ async def test_a2a_tasks_expire_and_go_with_their_thread(dsn: str) -> None:
453
+ db = await _opened(dsn)
454
+ try:
455
+ store = PostgresTaskStore(1, db)
456
+ await store.save(_a2a_task("old", "c1"), _ctx("alice"))
457
+ await asyncio.sleep(1.2)
458
+ assert await store.get("old", _ctx("alice")) is None # expired on the database clock
459
+ assert await store.sweep() == (1, 0) # and deleted by the sweep
460
+ store = PostgresTaskStore(3600, db)
461
+ owned = (("alice", "a", "c1"), ("bob", "b", "c1"), ("bob", "c", "c2"))
462
+ for owner, task_id, context in owned:
463
+ await store.save(_a2a_task(task_id, context), _ctx(owner))
464
+ assert await store.delete_context("c1") == 2 # every owner's tasks of that thread
465
+ assert await store.get("c", _ctx("bob")) is not None
466
+ finally:
467
+ await db.close()
468
+
469
+
470
+ async def test_a2a_tasks_of_a_dead_run_are_failed_not_left_working(
471
+ dsn: str, monkeypatch: pytest.MonkeyPatch
472
+ ) -> None:
473
+ """A task `working` in a process that died is failed once its thread's lease is gone."""
474
+ monkeypatch.setattr(chat_module, "RECONCILE_GRACE_S", 0.0)
475
+ db = await _opened(dsn)
476
+ locks = ThreadLocks(dsn)
477
+ try:
478
+ store = PostgresTaskStore(3600, db)
479
+ await store.save(_a2a_task("dead", "t-dead", TaskState.TASK_STATE_WORKING), _ctx("alice"))
480
+ await store.save(_a2a_task("live", "t-live", TaskState.TASK_STATE_WORKING), _ctx("alice"))
481
+ await store.save(
482
+ _a2a_task("paused", "t-paused", TaskState.TASK_STATE_INPUT_REQUIRED), _ctx("alice")
483
+ )
484
+ lease = await locks.acquire("t-live") # its run goes on in some replica
485
+ await asyncio.sleep(0.05)
486
+ assert await store.sweep() == (0, 1)
487
+ dead = await store.get("dead", _ctx("alice"))
488
+ assert dead is not None and dead.status.state == TaskState.TASK_STATE_FAILED
489
+ assert dead.status.message.parts[0].text == INTERRUPTED_TASK_TEXT
490
+ live = await store.get("live", _ctx("alice"))
491
+ assert live is not None and live.status.state == TaskState.TASK_STATE_WORKING
492
+ # The lease is not this app's: a CancelTask here is refused (see a2a.py).
493
+ assert await store.running_elsewhere(live) is True
494
+ await lease.release()
495
+ assert await store.running_elsewhere(live) is False
496
+ paused = await store.get("paused", _ctx("alice"))
497
+ assert paused is not None and paused.status.state == TaskState.TASK_STATE_INPUT_REQUIRED
498
+ finally:
499
+ await locks.close()
500
+ await db.close()
501
+
502
+
503
+ async def test_a2a_tasks_holding_a_nul_are_saved_with_a_replacement_character(
504
+ dsn: str, monkeypatch: pytest.MonkeyPatch
505
+ ) -> None:
506
+ """Postgres stores no U+0000 (jsonb and TEXT refuse it): the store saves U+FFFD instead.
507
+
508
+ A save that failed on one (a tool's output the reply repeats, say) lost the
509
+ task while its run went on; the sweep then failed it as if its process had died.
510
+ """
511
+ monkeypatch.setattr(chat_module, "RECONCILE_GRACE_S", 0.0)
512
+ db = await _opened(dsn)
513
+ try:
514
+ store = PostgresTaskStore(3600, db)
515
+ task = _a2a_task("t1", "ab\x00cd", TaskState.TASK_STATE_WORKING, text="reading\x00 42")
516
+ message = task.history.add()
517
+ message.message_id = "m1"
518
+ message.parts.add().text = "tell me\x00 more"
519
+ json_format.ParseDict({"key\x00": ["value\x00"]}, message.parts.add().data)
520
+ json_format.ParseDict({"note": "x\x00"}, message.metadata)
521
+ await store.save(task, _ctx("alice"))
522
+ seen = await store.get("t1", _ctx("alice"))
523
+ assert seen is not None
524
+ assert seen.context_id == "ab\ufffdcd"
525
+ assert seen.artifacts[0].parts[0].text == "reading\ufffd 42"
526
+ assert seen.history[0].parts[0].text == "tell me\ufffd more"
527
+ assert json_format.MessageToDict(seen.history[0].parts[1].data) == {
528
+ "key\ufffd": ["value\ufffd"]
529
+ }
530
+ assert json_format.MessageToDict(seen.history[0].metadata) == {"note": "x\ufffd"}
531
+ # The contextId a caller names finds it; an id holding a U+0000 is not an error.
532
+ listed = await store.list(ListTasksRequest(context_id="ab\x00cd"), _ctx("alice"))
533
+ assert [t.id for t in listed.tasks] == ["t1"]
534
+ assert await store.get("t\x00", _ctx("alice")) is None
535
+ # Its thread has no run: the sweep fails it, rewriting the stored task.
536
+ assert await store.sweep() == (0, 1)
537
+ failed = await store.get("t1", _ctx("alice"))
538
+ assert failed is not None and failed.status.state == TaskState.TASK_STATE_FAILED
539
+ assert failed.artifacts[0].parts[0].text == "reading\ufffd 42"
540
+ finally:
541
+ await db.close()
542
+
543
+
544
+ # --- agents calling agents (0.3): the tables upgraded in place, owner-keyed tasks ----------
545
+
546
+ # The approvals table as 0.2 created it.
547
+ APPROVALS_02 = """
548
+ CREATE TABLE approvals (
549
+ approval_id TEXT PRIMARY KEY, thread_id TEXT NOT NULL, run_id TEXT NOT NULL,
550
+ interrupt_id TEXT NOT NULL, requester_hash TEXT NOT NULL, requester_context JSONB,
551
+ api TEXT NOT NULL, method TEXT NOT NULL, path TEXT NOT NULL, operation_id TEXT,
552
+ call_hash TEXT NOT NULL, tool_call_id TEXT, message_id TEXT, approvers JSONB NOT NULL,
553
+ payload JSONB NOT NULL, status TEXT NOT NULL, decided_by TEXT, decided_at TIMESTAMPTZ,
554
+ comment TEXT, used_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
555
+ expires_at TIMESTAMPTZ NOT NULL
556
+ )
557
+ """
558
+
559
+
560
+ async def test_a_02_schema_gains_the_actor_columns_in_place(dsn: str) -> None:
561
+ """0.2 tables with rows: after the upgrade the rows read as direct, and the pending
562
+ approval is decided exactly as before."""
563
+ import psycopg
564
+
565
+ alice = Principal(id="alice")
566
+ async with await psycopg.AsyncConnection.connect(dsn, autocommit=True) as conn:
567
+ await conn.execute(APPROVALS_02)
568
+ await conn.execute(
569
+ "INSERT INTO approvals (approval_id, thread_id, run_id, interrupt_id, "
570
+ "requester_hash, api, method, path, call_hash, approvers, payload, status, "
571
+ "expires_at) VALUES ('a1', 't1', 'r1', 'i1', %s, 'shop', 'POST', '/orders/7/cancel', "
572
+ "'h1', '[\"requester\"]', '{\"query\": {}, \"body\": {}}', 'pending', "
573
+ "now() + interval '1 hour')",
574
+ (alice.hashed_id(),),
575
+ )
576
+ await conn.execute(
577
+ "CREATE TABLE threads (thread_id TEXT PRIMARY KEY, principal_id TEXT NOT NULL, "
578
+ "tenant TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), "
579
+ "updated_at TIMESTAMPTZ NOT NULL DEFAULT now())"
580
+ )
581
+ await conn.execute("INSERT INTO threads (thread_id, principal_id) VALUES ('t1', 'alice')")
582
+ await conn.execute(
583
+ "CREATE TABLE runs (run_id TEXT PRIMARY KEY, thread_id TEXT NOT NULL, "
584
+ "principal_hash TEXT NOT NULL, model TEXT, status TEXT NOT NULL, "
585
+ "input_tokens INTEGER, output_tokens INTEGER, latency_ms INTEGER, "
586
+ "error_type TEXT, metadata JSONB, payload JSONB, "
587
+ "created_at TIMESTAMPTZ NOT NULL DEFAULT now())"
588
+ )
589
+ db = await _opened(dsn)
590
+ try:
591
+ thread = await ThreadStore(db).get("t1")
592
+ assert thread is not None and thread.actor == ""
593
+ store = ApprovalStore(db)
594
+ record = await store.get("a1")
595
+ assert record is not None
596
+ assert (record.decide_with, record.relayers, record.requester_actor) == ("direct", [], "")
597
+ assert record.decided_via is None and record.display_digest is None
598
+ assert may_decide(alice, thread, record.approvers)
599
+ agent = Principal(id="alice", actor=Actor(id="concierge"))
600
+ assert not may_decide(agent, thread, record.approvers)
601
+ decided = await store.decide("a1", "approved", alice.hashed_id(), None)
602
+ assert decided is not None and decided.status == "approved" and decided.decided_via is None
603
+ runs = RunStore(db)
604
+ await runs.start(
605
+ RunRecord(
606
+ run_id="r2",
607
+ thread_id="t1",
608
+ principal_hash="h",
609
+ model="m",
610
+ status="ok",
611
+ actor="concierge",
612
+ )
613
+ )
614
+ stored = await runs.get("r2")
615
+ assert stored is not None and stored.actor == "concierge"
616
+ finally:
617
+ await db.close()
618
+
619
+
620
+ async def test_a2a_tasks_are_keyed_by_the_owner_key(dsn: str) -> None:
621
+ """A delegated owner key (subject, U+001F, actor) is its own owner; direct keys are as
622
+ in 0.2."""
623
+ db = await _opened(dsn)
624
+ try:
625
+ store = PostgresTaskStore(3600, db)
626
+ agent_key = Principal(id="alice", actor=Actor(id="concierge")).owner_key()
627
+ assert agent_key == "alice\x1fconcierge"
628
+ await store.save(_a2a_task("t-agent", "ctx-agent", second=1), _ctx(agent_key))
629
+ await store.save(_a2a_task("t-alice", "ctx-alice", second=2), _ctx("alice"))
630
+ assert await store.get("t-agent", _ctx(agent_key)) is not None
631
+ assert await store.get("t-agent", _ctx("alice")) is None
632
+ assert await store.get("t-agent", _ctx("alice\x1fbilling")) is None
633
+ assert await store.get("t-alice", _ctx(agent_key)) is None
634
+ listed = await store.list(ListTasksRequest(), _ctx(agent_key))
635
+ assert [t.id for t in listed.tasks] == ["t-agent"]
636
+ finally:
637
+ await db.close()