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,247 @@
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
+ """Threads agents start for a user, under a real LangGraph dev server (the native API filters).
16
+
17
+ The server runs this app and its auth handler as `test_approvals_server.py`
18
+ does, with `AUTH_ALLOWED_ACTORS` set. Tokens carry the RFC 8693 `act` claim of
19
+ the agents presenting them. The server's own thread filters are containment
20
+ filters on the thread metadata: the user's (direct) filter must match the
21
+ threads their agents started, an agent's must match only its own, and a thread
22
+ without `actor` (the user's own, or one created before 0.3) must match no
23
+ agent's. A run the native API starts acts for the authenticated caller, with
24
+ its roles and actor, whatever run context the request sends. Skipped when the
25
+ LangGraph CLI and its in-memory server are not installed.
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import importlib.util
31
+ import json
32
+ import sys
33
+ import threading
34
+ import time
35
+ import uuid
36
+ from collections.abc import Iterator
37
+ from pathlib import Path
38
+ from typing import Any
39
+
40
+ import httpx
41
+ import jwt
42
+ import pytest
43
+ from test_approvals_server import (
44
+ AUDIENCE,
45
+ BODY,
46
+ ISSUER,
47
+ KEY,
48
+ POLICY,
49
+ Upstream,
50
+ _free_port,
51
+ _server_env,
52
+ _start_dev,
53
+ _stop,
54
+ _write_config,
55
+ parse_sse,
56
+ )
57
+
58
+ LANGGRAPH = Path(sys.executable).with_name("langgraph")
59
+ pytestmark = pytest.mark.skipif(
60
+ not LANGGRAPH.exists() or importlib.util.find_spec("langgraph_api") is None,
61
+ reason="needs the LangGraph CLI with its in-memory server (langgraph-cli[inmem])",
62
+ )
63
+
64
+
65
+ def _token(user: str, *actors: str) -> dict[str, str]:
66
+ """`user`'s token, presented by `actors` (current first) when any."""
67
+ now = int(time.time())
68
+ claims: dict[str, Any] = {
69
+ "sub": user,
70
+ "iss": ISSUER,
71
+ "aud": AUDIENCE,
72
+ "iat": now,
73
+ "nbf": now,
74
+ "exp": now + 600,
75
+ "roles": ["user"],
76
+ }
77
+ act: dict[str, Any] | None = None
78
+ for actor in reversed(actors):
79
+ act = {"sub": actor} if act is None else {"sub": actor, "act": act}
80
+ if act is not None:
81
+ claims["act"] = act
82
+ return {"Authorization": f"Bearer {jwt.encode(claims, KEY, algorithm='RS256')}"}
83
+
84
+
85
+ @pytest.fixture(scope="module")
86
+ def server(tmp_path_factory: pytest.TempPathFactory) -> Iterator[str]:
87
+ work = tmp_path_factory.mktemp("cross-actor-server")
88
+ upstream = Upstream()
89
+ threading.Thread(target=upstream.serve_forever, daemon=True).start()
90
+ body_file = work / "body.json"
91
+ body_file.write_text(json.dumps(BODY), encoding="utf-8")
92
+ policy = work / "api-policy.yaml"
93
+ policy.write_text(POLICY, encoding="utf-8")
94
+ _write_config(work)
95
+ env = {
96
+ **_server_env(policy, body_file, upstream),
97
+ "AUTH_ALLOWED_ACTORS": "concierge,billing",
98
+ }
99
+ port = _free_port()
100
+ proc = None
101
+ try:
102
+ proc = _start_dev(work, env, port, work / "server.log")
103
+ yield f"http://127.0.0.1:{port}"
104
+ finally:
105
+ if proc is not None:
106
+ _stop(proc)
107
+ upstream.shutdown()
108
+ upstream.server_close()
109
+
110
+
111
+ def _get(server: str, thread: str, headers: dict[str, str]) -> httpx.Response:
112
+ return httpx.get(f"{server}/threads/{thread}", headers=headers, timeout=30)
113
+
114
+
115
+ def _listed(server: str, headers: dict[str, str]) -> set[str]:
116
+ r = httpx.post(f"{server}/threads/search", json={"limit": 100}, headers=headers, timeout=30)
117
+ assert r.status_code == 200, r.text
118
+ return {t["thread_id"] for t in r.json()}
119
+
120
+
121
+ def _patch(server: str, thread: str, metadata: dict[str, Any], headers: dict[str, str]) -> Any:
122
+ return httpx.patch(
123
+ f"{server}/threads/{thread}", json={"metadata": metadata}, headers=headers, timeout=30
124
+ )
125
+
126
+
127
+ def test_native_threads_follow_the_owner_key(server: str) -> None:
128
+ user = f"alice-{uuid.uuid4().hex[:8]}"
129
+ alice, concierge = _token(user), _token(user, "concierge")
130
+ billing = _token(user, "billing", "concierge")
131
+ # The concierge's thread is stamped with its actor, whatever it sends.
132
+ created = httpx.post(
133
+ f"{server}/threads",
134
+ json={"metadata": {"actor": "billing", "topic": "x"}},
135
+ headers=concierge,
136
+ timeout=30,
137
+ )
138
+ assert created.status_code == 200, created.text
139
+ thread = created.json()["thread_id"]
140
+ assert created.json()["metadata"]["actor"] == "concierge"
141
+ assert created.json()["metadata"]["principal_id"] == user
142
+ # The person reads and lists it; another agent of the same user does neither.
143
+ assert _get(server, thread, alice).status_code == 200
144
+ assert thread in _listed(server, alice) and thread in _listed(server, concierge)
145
+ assert _get(server, thread, billing).status_code in (403, 404)
146
+ assert thread not in _listed(server, billing)
147
+ # The person's own thread (as any thread from before 0.3: no actor) is no agent's.
148
+ direct = httpx.post(
149
+ f"{server}/threads", json={"metadata": {"actor": "concierge"}}, headers=alice, timeout=30
150
+ )
151
+ assert direct.status_code == 200 and "actor" not in direct.json()["metadata"]
152
+ mine = direct.json()["thread_id"]
153
+ assert _get(server, mine, concierge).status_code in (403, 404)
154
+ assert mine not in _listed(server, concierge) and mine in _listed(server, alice)
155
+ # Nobody moves or strips the actor.
156
+ moved = _patch(server, thread, {"actor": "billing"}, concierge)
157
+ assert moved.status_code == 200 and moved.json()["metadata"]["actor"] == "concierge"
158
+ stripped = _patch(server, thread, {"actor": ""}, alice)
159
+ assert stripped.status_code == 200 and stripped.json()["metadata"]["actor"] == "concierge"
160
+ assert _patch(server, thread, {"topic": "y"}, billing).status_code in (403, 404)
161
+
162
+
163
+ def test_the_apps_routes_stamp_the_actor_under_this_runtime(server: str) -> None:
164
+ user = f"alice-{uuid.uuid4().hex[:8]}"
165
+ alice, concierge = _token(user), _token(user, "concierge")
166
+ r = httpx.post(f"{server}/chat", json={"message": "hello"}, headers=concierge, timeout=60)
167
+ assert r.status_code == 200, r.text
168
+ thread = parse_sse(r.text)[0][1]["thread_id"]
169
+ assert _get(server, thread, alice).json()["metadata"]["actor"] == "concierge"
170
+ for who, allowed in ((alice, True), (concierge, True), (_token(user, "billing"), False)):
171
+ messages = httpx.get(f"{server}/threads/{thread}/messages", headers=who, timeout=30)
172
+ assert (messages.status_code == 200) is allowed, messages.text
173
+ # An agent the server does not list is refused before anything runs.
174
+ other = httpx.post(
175
+ f"{server}/chat", json={"message": "hello"}, headers=_token(user, "stranger"), timeout=30
176
+ )
177
+ assert other.status_code == 403
178
+
179
+
180
+ def _native_whoami(
181
+ server: str,
182
+ headers: dict[str, str],
183
+ *,
184
+ context: dict[str, Any] | None = None,
185
+ config: dict[str, Any] | None = None,
186
+ ) -> dict[str, Any]:
187
+ """Who the `whoami` tool of a native run (`POST /threads/{id}/runs/wait`) acts for."""
188
+ created = httpx.post(f"{server}/threads", json={}, headers=headers, timeout=30)
189
+ assert created.status_code == 200, created.text
190
+ body: dict[str, Any] = {
191
+ "assistant_id": "agent",
192
+ "input": {"messages": [{"role": "user", "content": "whoami for me"}]},
193
+ }
194
+ if context is not None:
195
+ body["context"] = context
196
+ if config is not None:
197
+ body["config"] = config
198
+ thread = created.json()["thread_id"]
199
+ r = httpx.post(f"{server}/threads/{thread}/runs/wait", json=body, headers=headers, timeout=60)
200
+ assert r.status_code == 200, r.text
201
+ results = [m for m in r.json().get("messages", []) if m.get("type") == "tool"]
202
+ assert results, r.text
203
+ return json.loads(results[-1]["content"])
204
+
205
+
206
+ def test_a_native_run_acts_for_the_authenticated_caller_whatever_it_sends(server: str) -> None:
207
+ """The native run API takes a run context in the request (`context`, or
208
+ `config.configurable`, which the server copies into it); tools read who they act
209
+ for from it. The auth handler puts the caller's own there instead."""
210
+ user = f"alice-{uuid.uuid4().hex[:8]}"
211
+ alice, concierge = _token(user), _token(user, "concierge")
212
+ as_alice = {
213
+ "principal_id": user,
214
+ "roles": ["user"],
215
+ "actor": None,
216
+ "actor_chain": [],
217
+ "direct_only": "allowed",
218
+ }
219
+ # A person names another principal and roles, or poses as an agent: the tools
220
+ # still act for them, with their own roles.
221
+ bob = {"principal_id": "bob", "roles": ["ops"], "attributes": {}}
222
+ posing = {
223
+ "principal_id": user,
224
+ "roles": ["user"],
225
+ "attributes": {"@actor": {"id": "concierge", "chain": ["concierge"], "client": None}},
226
+ }
227
+ assert _native_whoami(server, alice, context=bob) == as_alice
228
+ assert _native_whoami(server, alice, config={"configurable": bob}) == as_alice
229
+ assert _native_whoami(server, alice, context=posing) == as_alice
230
+ assert _native_whoami(server, alice) == as_alice # nothing sent: still the caller's
231
+ # An agent cannot drop its actor (a tool only a person may trigger refuses it),
232
+ # name another one, or take roles it was not lent.
233
+ for sent in (
234
+ None,
235
+ {"principal_id": user, "roles": [], "attributes": {}},
236
+ {"principal_id": user, "roles": ["ops"], "attributes": {"@actor": {"id": "billing"}}},
237
+ ):
238
+ seen = _native_whoami(server, concierge, context=sent)
239
+ assert seen["principal_id"] == user and seen["roles"] == [], seen
240
+ assert seen["actor"] == "concierge" and seen["actor_chain"] == ["concierge"], seen
241
+ refusal = "only the user directly may ask for this, not agent 'concierge'"
242
+ assert refusal in seen["direct_only"], seen
243
+ # The whole chain of agents reaches the tools, as through /chat.
244
+ seen = _native_whoami(
245
+ server, _token(user, "billing", "concierge"), context={"principal_id": user}
246
+ )
247
+ assert seen["actor"] == "billing" and seen["actor_chain"] == ["billing", "concierge"], seen
@@ -0,0 +1,278 @@
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
+ """A thread whose tool-call history a stopped run left invalid is repaired by the next run.
16
+
17
+ Model providers reject an assistant tool call that is not followed right away
18
+ by its result. A process that dies mid tool call (a crash, an OOM kill, a lost
19
+ node, a database outage) never writes the result; older versions also wrote
20
+ the repair after the next user turn. Each run puts the history right before it
21
+ appends its own turn (in-process, fake model, memory checkpointer).
22
+
23
+ A model's tool call whose arguments are not valid JSON needs a result too
24
+ (LangChain sends it back to the provider as a call): the agent answers it in
25
+ the same run, and the repair answers one an older version left open. Those
26
+ tests run the agent on `ChatOpenAI` against an in-process OpenAI-compatible
27
+ server that refuses invalid histories as OpenAI does (`openai_compatible`).
28
+ """
29
+
30
+ from __future__ import annotations
31
+
32
+ import os
33
+ import uuid
34
+ from collections.abc import AsyncIterator
35
+ from typing import Any
36
+
37
+ os.environ.update(
38
+ {
39
+ "MODEL_PROVIDER": "fake",
40
+ "MODEL_NAME": "fake",
41
+ "CHECKPOINTER": "memory",
42
+ "AUTH_POLICY": "shared-bearer",
43
+ "API_KEY": "test-key",
44
+ "APP_ENV": "dev",
45
+ "TRACING_ENABLED": "false",
46
+ "RUNTIME": "fastapi",
47
+ "APP_URL": "http://testserver",
48
+ }
49
+ )
50
+
51
+ import httpx
52
+ import pytest
53
+ from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
54
+
55
+ from {{cookiecutter.agent_directory}} import agent as agent_module
56
+ from {{cookiecutter.agent_directory}}.app_utils.chat import repair_tool_history
57
+ from {{cookiecutter.agent_directory}}.app_utils.content import INVALID_TOOL_CALL_RESULT
58
+ from {{cookiecutter.agent_directory}}.fast_api_app import app
59
+
60
+ AUTH = {"Authorization": "Bearer test-key"}
61
+
62
+
63
+ @pytest.fixture
64
+ async def client(monkeypatch: pytest.MonkeyPatch) -> AsyncIterator[httpx.AsyncClient]:
65
+ for name in ("RUN_TIMEOUT_S", "RECURSION_LIMIT", "RETENTION_DAYS", "TRACE_CAPTURE"):
66
+ monkeypatch.delenv(name, raising=False)
67
+ async with app.router.lifespan_context(app):
68
+ transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
69
+ async with httpx.AsyncClient(
70
+ transport=transport, base_url="http://testserver", timeout=30
71
+ ) as c:
72
+ yield c
73
+
74
+
75
+ async def chat(client: httpx.AsyncClient, message: str, thread_id: str) -> str:
76
+ r = await client.post("/chat", json={"message": message, "thread_id": thread_id}, headers=AUTH)
77
+ assert r.status_code == 200, r.text
78
+ assert "event: message.end" in r.text, r.text
79
+ return r.text
80
+
81
+
82
+ async def history(thread_id: str) -> list[Any]:
83
+ state = await agent_module.graph.aget_state({"configurable": {"thread_id": thread_id}})
84
+ return list(state.values.get("messages", []))
85
+
86
+
87
+ def roles(messages: list[Any]) -> list[str]:
88
+ out = []
89
+ for m in messages:
90
+ if isinstance(m, ToolMessage):
91
+ out.append("tool!" if m.status == "error" else "tool")
92
+ elif isinstance(m, AIMessage):
93
+ out.append("ai(tc)" if m.tool_calls else "ai")
94
+ else:
95
+ out.append("user")
96
+ return out
97
+
98
+
99
+ def open_call(call_id: str) -> AIMessage:
100
+ return AIMessage(
101
+ content="",
102
+ id=f"ai-{call_id}",
103
+ tool_calls=[{"id": call_id, "name": "get_weather", "args": {"query": "Paris"}}],
104
+ )
105
+
106
+
107
+ async def seed(thread_id: str, messages: list[Any], as_node: str = "model") -> None:
108
+ """Write `messages` into the thread as a stopped run would have left them."""
109
+ await agent_module.graph.aupdate_state(
110
+ {"configurable": {"thread_id": thread_id}}, {"messages": messages}, as_node=as_node
111
+ )
112
+
113
+
114
+ async def test_a_tool_call_left_open_by_a_dead_run_is_answered_before_the_next_turn(
115
+ client: httpx.AsyncClient,
116
+ ) -> None:
117
+ thread_id = str(uuid.uuid4())
118
+ await chat(client, "hello", thread_id) # the thread and its owner exist
119
+ # The process died after the model asked for the tool, before its result.
120
+ await seed(thread_id, [HumanMessage(content="weather?", id="u2"), open_call("c1")])
121
+ assert roles(await history(thread_id)) == ["user", "ai", "user", "ai(tc)"]
122
+ await chat(client, "are you there?", thread_id)
123
+ messages = await history(thread_id)
124
+ assert roles(messages) == ["user", "ai", "user", "ai(tc)", "tool!", "user", "ai"]
125
+ assert messages[4].tool_call_id == "c1" and "interrupted" in messages[4].content
126
+ assert repair_tool_history(messages, lambda call: None) is None # valid for providers
127
+
128
+
129
+ async def test_a_result_written_after_the_next_turn_is_moved_back(
130
+ client: httpx.AsyncClient,
131
+ ) -> None:
132
+ """The shape older versions left: every later turn failed with a provider 400."""
133
+ thread_id = str(uuid.uuid4())
134
+ await chat(client, "hello", thread_id)
135
+ await seed(
136
+ thread_id,
137
+ [
138
+ HumanMessage(content="weather?", id="u2"),
139
+ open_call("c1"),
140
+ HumanMessage(content="hello?", id="u3"),
141
+ ToolMessage(content="stopped", tool_call_id="c1", status="error", id="t1"),
142
+ ],
143
+ as_node="tools",
144
+ )
145
+ await chat(client, "still there?", thread_id)
146
+ messages = await history(thread_id)
147
+ assert roles(messages) == ["user", "ai", "user", "ai(tc)", "tool!", "user", "user", "ai"]
148
+ assert [m.id for m in messages[2:6]] == ["u2", "ai-c1", "t1", "u3"]
149
+ assert repair_tool_history(messages, lambda call: None) is None
150
+ # Nothing more to repair on the turn after.
151
+ before = [m.id for m in await history(thread_id)]
152
+ await chat(client, "and again", thread_id)
153
+ assert [m.id for m in await history(thread_id)][: len(before)] == before
154
+
155
+
156
+ async def test_turns_that_reuse_a_tool_call_id_keep_every_result(
157
+ client: httpx.AsyncClient, use_test_tools, caplog: pytest.LogCaptureFixture
158
+ ) -> None:
159
+ """The fake model calls `call_probe` every turn (models may number ids per message):
160
+ a healthy thread is never rewritten, and each turn keeps its own result."""
161
+ from langchain_core.tools import tool
162
+
163
+ @tool
164
+ def probe(query: str) -> str:
165
+ """Test-only tool: reports what it was asked about."""
166
+ return f"probe reading for {query}"
167
+
168
+ use_test_tools(probe)
169
+ thread_id = str(uuid.uuid4())
170
+ with caplog.at_level("INFO"):
171
+ await chat(client, "Run the probe for SF", thread_id)
172
+ await chat(client, "Run the probe for Paris", thread_id)
173
+ await chat(client, "thanks", thread_id)
174
+ messages = await history(thread_id)
175
+ assert roles(messages) == ["user", "ai(tc)", "tool", "ai"] * 2 + ["user", "ai"]
176
+ calls = [m.tool_calls[0]["id"] for m in messages if isinstance(m, AIMessage) and m.tool_calls]
177
+ assert calls[0] == calls[1] # the same id in both turns
178
+ results = [m.content for m in messages if isinstance(m, ToolMessage)]
179
+ assert results == ["probe reading for SF", "probe reading for Paris"]
180
+ assert not [r for r in caplog.records if "tool results" in r.getMessage()]
181
+ assert not [r for r in caplog.records if "tool calls" in r.getMessage()]
182
+
183
+
184
+ # --- a provider that refuses invalid histories -----------------------------------------------
185
+
186
+
187
+ def serve_openai_compatible(openai_compatible: Any, monkeypatch: pytest.MonkeyPatch) -> None:
188
+ """Serve the agent's graph on `ChatOpenAI` against the in-process OpenAI-compatible fake."""
189
+ from langchain.agents import create_agent
190
+ from langchain_core.tools import tool
191
+
192
+ @tool
193
+ def probe(query: str) -> str:
194
+ """Test-only tool: reports what it was asked about."""
195
+ return f"probe reading for {query}"
196
+
197
+ graph = create_agent(
198
+ model=openai_compatible.model(),
199
+ tools=[probe],
200
+ system_prompt=agent_module.SYSTEM_PROMPT,
201
+ middleware=agent_module.middleware(),
202
+ context_schema=agent_module.AgentContext,
203
+ )
204
+ graph.checkpointer = agent_module.graph.checkpointer
205
+ monkeypatch.setattr(agent_module, "graph", graph)
206
+
207
+
208
+ async def events_of(client: httpx.AsyncClient, message: str, thread_id: str) -> list[str]:
209
+ text = await chat(client, message, thread_id)
210
+ return [line[7:] for line in text.splitlines() if line.startswith("event: ")]
211
+
212
+
213
+ @pytest.mark.parametrize("script", ["BADARGS", "TRAILINGCOMMA"])
214
+ async def test_a_tool_call_with_invalid_arguments_leaves_the_thread_usable(
215
+ client: httpx.AsyncClient,
216
+ openai_compatible: Any,
217
+ monkeypatch: pytest.MonkeyPatch,
218
+ script: str,
219
+ ) -> None:
220
+ """Arguments that are not valid JSON: the model is told and replies, every turn after works.
221
+
222
+ Before, the run ended with no reply, and the provider refused every later
223
+ turn (the call had no result).
224
+ """
225
+ serve_openai_compatible(openai_compatible, monkeypatch)
226
+ thread_id = str(uuid.uuid4())
227
+ text = await chat(client, f"probe {script}", thread_id)
228
+ assert "event: tool.call" in text and "event: tool.result" in text
229
+ assert "Found: " in text and '"status": "ok"' in text
230
+ for message in ("thanks", "and again"):
231
+ assert (await events_of(client, message, thread_id))[-1] == "message.end"
232
+ assert openai_compatible.refusals == []
233
+ messages = await history(thread_id)
234
+ assert roles(messages)[:4] == ["user", "ai", "tool!", "ai"]
235
+ assert messages[1].invalid_tool_calls and messages[2].content == INVALID_TOOL_CALL_RESULT
236
+ assert repair_tool_history(messages, lambda call: None) is None
237
+
238
+
239
+ async def test_a_thread_an_invalid_call_broke_before_is_repaired_by_the_next_turn(
240
+ client: httpx.AsyncClient, openai_compatible: Any, monkeypatch: pytest.MonkeyPatch
241
+ ) -> None:
242
+ """The shape older versions left: an invalid call and no result, then more turns."""
243
+ thread_id = str(uuid.uuid4())
244
+ await chat(client, "hello", thread_id)
245
+ broken = AIMessage(
246
+ content="",
247
+ id="ai-bad",
248
+ invalid_tool_calls=[
249
+ {"type": "invalid_tool_call", "id": "call_0", "name": "probe", "args": "query=SF"}
250
+ ],
251
+ )
252
+ await seed(thread_id, [HumanMessage(content="probe?", id="u2"), broken])
253
+ await seed(thread_id, [HumanMessage(content="hello?", id="u3")], as_node="tools")
254
+ serve_openai_compatible(openai_compatible, monkeypatch)
255
+ assert (await events_of(client, "still there?", thread_id))[-1] == "message.end"
256
+ assert openai_compatible.refusals == []
257
+ messages = await history(thread_id)
258
+ assert roles(messages)[2:6] == ["user", "ai", "tool!", "user"]
259
+ assert [messages[2].id, messages[3].id, messages[5].id] == ["u2", "ai-bad", "u3"]
260
+ assert messages[4].tool_call_id == "call_0"
261
+ assert messages[4].content == INVALID_TOOL_CALL_RESULT
262
+ assert all(body.get("stream") for body in openai_compatible.requests)
263
+
264
+
265
+ async def test_one_id_for_two_calls_keeps_both_results_on_the_next_turn(
266
+ client: httpx.AsyncClient, openai_compatible: Any, monkeypatch: pytest.MonkeyPatch
267
+ ) -> None:
268
+ """Some OpenAI-compatible servers give parallel calls one id: no result is dropped."""
269
+ serve_openai_compatible(openai_compatible, monkeypatch)
270
+ thread_id = str(uuid.uuid4())
271
+ await chat(client, "probe DUPIDS", thread_id)
272
+ before = [m.id for m in await history(thread_id)]
273
+ assert (await events_of(client, "thanks", thread_id))[-1] == "message.end"
274
+ assert openai_compatible.refusals == []
275
+ messages = await history(thread_id)
276
+ assert [m.id for m in messages[: len(before)]] == before # nothing rewritten
277
+ results = [m.content for m in messages if isinstance(m, ToolMessage)]
278
+ assert sorted(results) == ["probe reading for Rome", "probe reading for SF"]