gatewaysdk 0.3.2__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 (376) hide show
  1. gatewaysdk/__init__.py +540 -0
  2. gatewaysdk/adapter/__init__.py +15 -0
  3. gatewaysdk/adapter/base.py +94 -0
  4. gatewaysdk/adapter/messages.py +270 -0
  5. gatewaysdk/adapter/triplet.py +1026 -0
  6. gatewaysdk/algorithm/__init__.py +39 -0
  7. gatewaysdk/algorithm/apo/__init__.py +5 -0
  8. gatewaysdk/algorithm/apo/apo.py +898 -0
  9. gatewaysdk/algorithm/apo/prompts/apply_edit_variant01.poml +22 -0
  10. gatewaysdk/algorithm/apo/prompts/apply_edit_variant02.poml +18 -0
  11. gatewaysdk/algorithm/apo/prompts/text_gradient_variant01.poml +18 -0
  12. gatewaysdk/algorithm/apo/prompts/text_gradient_variant02.poml +16 -0
  13. gatewaysdk/algorithm/apo/prompts/text_gradient_variant03.poml +107 -0
  14. gatewaysdk/algorithm/base.py +262 -0
  15. gatewaysdk/algorithm/decorator.py +264 -0
  16. gatewaysdk/algorithm/evals/__init__.py +7 -0
  17. gatewaysdk/algorithm/evals/evals.py +217 -0
  18. gatewaysdk/algorithm/fast.py +250 -0
  19. gatewaysdk/algorithm/gepa/__init__.py +61 -0
  20. gatewaysdk/algorithm/gepa/adapter.py +495 -0
  21. gatewaysdk/algorithm/gepa/gepa.py +570 -0
  22. gatewaysdk/algorithm/gepa/lib/__init__.py +18 -0
  23. gatewaysdk/algorithm/gepa/lib/adapters/README.md +12 -0
  24. gatewaysdk/algorithm/gepa/lib/adapters/__init__.py +0 -0
  25. gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/README.md +341 -0
  26. gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/__init__.py +1 -0
  27. gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/anymaths_adapter.py +174 -0
  28. gatewaysdk/algorithm/gepa/lib/adapters/anymaths_adapter/requirements.txt +1 -0
  29. gatewaysdk/algorithm/gepa/lib/adapters/default_adapter/README.md +0 -0
  30. gatewaysdk/algorithm/gepa/lib/adapters/default_adapter/__init__.py +0 -0
  31. gatewaysdk/algorithm/gepa/lib/adapters/default_adapter/default_adapter.py +209 -0
  32. gatewaysdk/algorithm/gepa/lib/adapters/dspy_adapter/README.md +7 -0
  33. gatewaysdk/algorithm/gepa/lib/adapters/dspy_adapter/__init__.py +0 -0
  34. gatewaysdk/algorithm/gepa/lib/adapters/dspy_adapter/dspy_adapter.py +307 -0
  35. gatewaysdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/README.md +99 -0
  36. gatewaysdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/dspy_program_proposal_signature.py +137 -0
  37. gatewaysdk/algorithm/gepa/lib/adapters/dspy_full_program_adapter/full_program_adapter.py +268 -0
  38. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/GEPA_RAG.md +621 -0
  39. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/__init__.py +56 -0
  40. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/evaluation_metrics.py +226 -0
  41. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/generic_rag_adapter.py +496 -0
  42. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/rag_pipeline.py +238 -0
  43. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_store_interface.py +212 -0
  44. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/__init__.py +2 -0
  45. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/chroma_store.py +196 -0
  46. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/lancedb_store.py +422 -0
  47. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/milvus_store.py +409 -0
  48. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/qdrant_store.py +368 -0
  49. gatewaysdk/algorithm/gepa/lib/adapters/generic_rag_adapter/vector_stores/weaviate_store.py +418 -0
  50. gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/README.md +552 -0
  51. gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/__init__.py +37 -0
  52. gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/mcp_adapter.py +699 -0
  53. gatewaysdk/algorithm/gepa/lib/adapters/mcp_adapter/mcp_client.py +364 -0
  54. gatewaysdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/README.md +9 -0
  55. gatewaysdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/__init__.py +0 -0
  56. gatewaysdk/algorithm/gepa/lib/adapters/terminal_bench_adapter/terminal_bench_adapter.py +217 -0
  57. gatewaysdk/algorithm/gepa/lib/api.py +382 -0
  58. gatewaysdk/algorithm/gepa/lib/core/__init__.py +0 -0
  59. gatewaysdk/algorithm/gepa/lib/core/adapter.py +180 -0
  60. gatewaysdk/algorithm/gepa/lib/core/data_loader.py +74 -0
  61. gatewaysdk/algorithm/gepa/lib/core/engine.py +379 -0
  62. gatewaysdk/algorithm/gepa/lib/core/result.py +233 -0
  63. gatewaysdk/algorithm/gepa/lib/core/state.py +636 -0
  64. gatewaysdk/algorithm/gepa/lib/examples/__init__.py +0 -0
  65. gatewaysdk/algorithm/gepa/lib/examples/aime.py +24 -0
  66. gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/eval_default.py +111 -0
  67. gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/prompt-templates/instruction_prompt.txt +9 -0
  68. gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/prompt-templates/optimal_prompt.txt +24 -0
  69. gatewaysdk/algorithm/gepa/lib/examples/anymaths-bench/train_anymaths.py +177 -0
  70. gatewaysdk/algorithm/gepa/lib/examples/dspy_full_program_evolution/arc_agi.ipynb +25705 -0
  71. gatewaysdk/algorithm/gepa/lib/examples/dspy_full_program_evolution/example.ipynb +348 -0
  72. gatewaysdk/algorithm/gepa/lib/examples/mcp_adapter/__init__.py +4 -0
  73. gatewaysdk/algorithm/gepa/lib/examples/mcp_adapter/mcp_optimization_example.py +456 -0
  74. gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/RAG_GUIDE.md +613 -0
  75. gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/__init__.py +9 -0
  76. gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/rag_optimization.py +820 -0
  77. gatewaysdk/algorithm/gepa/lib/examples/rag_adapter/requirements-rag.txt +29 -0
  78. gatewaysdk/algorithm/gepa/lib/examples/terminal-bench/prompt-templates/instruction_prompt.txt +16 -0
  79. gatewaysdk/algorithm/gepa/lib/examples/terminal-bench/prompt-templates/terminus.txt +9 -0
  80. gatewaysdk/algorithm/gepa/lib/examples/terminal-bench/train_terminus.py +161 -0
  81. gatewaysdk/algorithm/gepa/lib/gepa_utils.py +117 -0
  82. gatewaysdk/algorithm/gepa/lib/logging/__init__.py +0 -0
  83. gatewaysdk/algorithm/gepa/lib/logging/experiment_tracker.py +187 -0
  84. gatewaysdk/algorithm/gepa/lib/logging/logger.py +75 -0
  85. gatewaysdk/algorithm/gepa/lib/logging/utils.py +103 -0
  86. gatewaysdk/algorithm/gepa/lib/proposer/__init__.py +0 -0
  87. gatewaysdk/algorithm/gepa/lib/proposer/base.py +31 -0
  88. gatewaysdk/algorithm/gepa/lib/proposer/merge.py +357 -0
  89. gatewaysdk/algorithm/gepa/lib/proposer/reflective_mutation/__init__.py +0 -0
  90. gatewaysdk/algorithm/gepa/lib/proposer/reflective_mutation/base.py +49 -0
  91. gatewaysdk/algorithm/gepa/lib/proposer/reflective_mutation/reflective_mutation.py +176 -0
  92. gatewaysdk/algorithm/gepa/lib/py.typed +0 -0
  93. gatewaysdk/algorithm/gepa/lib/strategies/__init__.py +0 -0
  94. gatewaysdk/algorithm/gepa/lib/strategies/batch_sampler.py +77 -0
  95. gatewaysdk/algorithm/gepa/lib/strategies/candidate_selector.py +50 -0
  96. gatewaysdk/algorithm/gepa/lib/strategies/component_selector.py +36 -0
  97. gatewaysdk/algorithm/gepa/lib/strategies/eval_policy.py +64 -0
  98. gatewaysdk/algorithm/gepa/lib/strategies/instruction_proposal.py +126 -0
  99. gatewaysdk/algorithm/gepa/lib/utils/__init__.py +10 -0
  100. gatewaysdk/algorithm/gepa/lib/utils/stop_condition.py +196 -0
  101. gatewaysdk/algorithm/gepa/tracing.py +105 -0
  102. gatewaysdk/algorithm/utils.py +177 -0
  103. gatewaysdk/algorithm/verl/__init__.py +5 -0
  104. gatewaysdk/algorithm/verl/interface.py +202 -0
  105. gatewaysdk/automations.py +111 -0
  106. gatewaysdk/benchmark_hub/README.md +88 -0
  107. gatewaysdk/benchmark_hub/__init__.py +119 -0
  108. gatewaysdk/benchmark_hub/_archive.py +189 -0
  109. gatewaysdk/benchmark_hub/_tar.py +6 -0
  110. gatewaysdk/benchmark_hub/client.py +992 -0
  111. gatewaysdk/benchmark_hub/dispatch_shard.py +34 -0
  112. gatewaysdk/benchmark_hub/eval_config.py +20 -0
  113. gatewaysdk/benchmark_hub/evals.py +351 -0
  114. gatewaysdk/benchmark_hub/harbor_adapter.py +673 -0
  115. gatewaysdk/benchmark_hub/path_utils.py +81 -0
  116. gatewaysdk/benchmark_hub/save_utils.py +101 -0
  117. gatewaysdk/benchmark_hub/verifiers_adapter.py +327 -0
  118. gatewaysdk/benchmark_hub/versioning.py +72 -0
  119. gatewaysdk/build.py +515 -0
  120. gatewaysdk/cli/__init__.py +58 -0
  121. gatewaysdk/cli/agent_runner.py +132 -0
  122. gatewaysdk/cli/http_client.py +115 -0
  123. gatewaysdk/cli/platform.py +4086 -0
  124. gatewaysdk/cli/prometheus.py +115 -0
  125. gatewaysdk/cli/release_gate.py +215 -0
  126. gatewaysdk/cli/store.py +131 -0
  127. gatewaysdk/cli/vllm.py +29 -0
  128. gatewaysdk/client.py +406 -0
  129. gatewaysdk/config.py +348 -0
  130. gatewaysdk/connectors/__init__.py +25 -0
  131. gatewaysdk/connectors/client.py +203 -0
  132. gatewaysdk/connectors/skill.py +25 -0
  133. gatewaysdk/connectors/template.py +106 -0
  134. gatewaysdk/context.py +606 -0
  135. gatewaysdk/emitter/__init__.py +43 -0
  136. gatewaysdk/emitter/annotation.py +370 -0
  137. gatewaysdk/emitter/exception.py +54 -0
  138. gatewaysdk/emitter/message.py +61 -0
  139. gatewaysdk/emitter/object.py +117 -0
  140. gatewaysdk/emitter/reward.py +320 -0
  141. gatewaysdk/env_var.py +156 -0
  142. gatewaysdk/environment/__init__.py +108 -0
  143. gatewaysdk/environment/_bundle.py +281 -0
  144. gatewaysdk/environment/_harbor.py +276 -0
  145. gatewaysdk/environment/_materialize.py +97 -0
  146. gatewaysdk/environment/_tar.py +33 -0
  147. gatewaysdk/environment/_world.py +346 -0
  148. gatewaysdk/environment/core.py +527 -0
  149. gatewaysdk/environment/errors.py +58 -0
  150. gatewaysdk/environment/runtime.py +150 -0
  151. gatewaysdk/environment/schema/__init__.py +35 -0
  152. gatewaysdk/environment/schema/__main__.py +225 -0
  153. gatewaysdk/environment/schema/_slack_fidelity.py +116 -0
  154. gatewaysdk/environment/schema/_slack_scenario.py +183 -0
  155. gatewaysdk/environment/schema/api.py +1628 -0
  156. gatewaysdk/environment/schema/batch.py +755 -0
  157. gatewaysdk/environment/schema/compiler.py +615 -0
  158. gatewaysdk/environment/schema/conform.py +428 -0
  159. gatewaysdk/environment/schema/connector.py +777 -0
  160. gatewaysdk/environment/schema/connectors/apple-business-manager/handlers.py +17 -0
  161. gatewaysdk/environment/schema/connectors/apple-business-manager/parity.json +28 -0
  162. gatewaysdk/environment/schema/connectors/apple-business-manager/provenance.json +233 -0
  163. gatewaysdk/environment/schema/connectors/apple-business-manager/scope.toml +112 -0
  164. gatewaysdk/environment/schema/connectors/apple-business-manager/world.json +1380 -0
  165. gatewaysdk/environment/schema/connectors/base.json +28 -0
  166. gatewaysdk/environment/schema/connectors/custom/handlers.py +18 -0
  167. gatewaysdk/environment/schema/connectors/custom/world.json +123 -0
  168. gatewaysdk/environment/schema/connectors/github/handlers.py +81 -0
  169. gatewaysdk/environment/schema/connectors/github/parity.json +37 -0
  170. gatewaysdk/environment/schema/connectors/github/provenance.json +134 -0
  171. gatewaysdk/environment/schema/connectors/github/scope.toml +158 -0
  172. gatewaysdk/environment/schema/connectors/github/world.json +5869 -0
  173. gatewaysdk/environment/schema/connectors/google-calendar/handlers.py +23 -0
  174. gatewaysdk/environment/schema/connectors/google-calendar/world.json +209 -0
  175. gatewaysdk/environment/schema/connectors/google-drive/handlers.py +18 -0
  176. gatewaysdk/environment/schema/connectors/google-drive/world.json +286 -0
  177. gatewaysdk/environment/schema/connectors/jamf/handlers.py +34 -0
  178. gatewaysdk/environment/schema/connectors/jamf/parity.json +49 -0
  179. gatewaysdk/environment/schema/connectors/jamf/provenance.json +181 -0
  180. gatewaysdk/environment/schema/connectors/jamf/scope.toml +128 -0
  181. gatewaysdk/environment/schema/connectors/jamf/world.json +2218 -0
  182. gatewaysdk/environment/schema/connectors/jira/handlers.py +28 -0
  183. gatewaysdk/environment/schema/connectors/jira/world.json +389 -0
  184. gatewaysdk/environment/schema/connectors/kandji/handlers.py +38 -0
  185. gatewaysdk/environment/schema/connectors/kandji/parity.json +29 -0
  186. gatewaysdk/environment/schema/connectors/kandji/provenance.json +152 -0
  187. gatewaysdk/environment/schema/connectors/kandji/scope.toml +120 -0
  188. gatewaysdk/environment/schema/connectors/kandji/world.json +765 -0
  189. gatewaysdk/environment/schema/connectors/linear/handlers.py +20 -0
  190. gatewaysdk/environment/schema/connectors/linear/world.json +363 -0
  191. gatewaysdk/environment/schema/connectors/microsoft-teams/fidelity.json +45 -0
  192. gatewaysdk/environment/schema/connectors/microsoft-teams/handlers.py +1 -0
  193. gatewaysdk/environment/schema/connectors/microsoft-teams/provenance.json +851 -0
  194. gatewaysdk/environment/schema/connectors/microsoft-teams/scope.md +231 -0
  195. gatewaysdk/environment/schema/connectors/microsoft-teams/teams-types.json +1710 -0
  196. gatewaysdk/environment/schema/connectors/microsoft-teams/world.json +858 -0
  197. gatewaysdk/environment/schema/connectors/netsuite/handlers.py +32 -0
  198. gatewaysdk/environment/schema/connectors/netsuite/parity.json +50 -0
  199. gatewaysdk/environment/schema/connectors/netsuite/provenance.json +137 -0
  200. gatewaysdk/environment/schema/connectors/netsuite/scope.toml +78 -0
  201. gatewaysdk/environment/schema/connectors/netsuite/world.json +16243 -0
  202. gatewaysdk/environment/schema/connectors/salesforce/handlers.py +806 -0
  203. gatewaysdk/environment/schema/connectors/salesforce/parity.json +46 -0
  204. gatewaysdk/environment/schema/connectors/salesforce/provenance.json +143 -0
  205. gatewaysdk/environment/schema/connectors/salesforce/scope.toml +174 -0
  206. gatewaysdk/environment/schema/connectors/salesforce/world.json +4695 -0
  207. gatewaysdk/environment/schema/connectors/slack/MODEL.md +236 -0
  208. gatewaysdk/environment/schema/connectors/slack/capabilities.json +1039 -0
  209. gatewaysdk/environment/schema/connectors/slack/handlers.py +762 -0
  210. gatewaysdk/environment/schema/connectors/slack/parity.json +65 -0
  211. gatewaysdk/environment/schema/connectors/slack/provenance.json +294 -0
  212. gatewaysdk/environment/schema/connectors/slack/scope.toml +44 -0
  213. gatewaysdk/environment/schema/connectors/slack/world.json +4993 -0
  214. gatewaysdk/environment/schema/connectors/workday/handlers.py +75 -0
  215. gatewaysdk/environment/schema/connectors/workday/parity.json +36 -0
  216. gatewaysdk/environment/schema/connectors/workday/provenance.json +179 -0
  217. gatewaysdk/environment/schema/connectors/workday/scope.toml +180 -0
  218. gatewaysdk/environment/schema/connectors/workday/world.json +806 -0
  219. gatewaysdk/environment/schema/data.py +593 -0
  220. gatewaysdk/environment/schema/extract.py +263 -0
  221. gatewaysdk/environment/schema/host.py +2437 -0
  222. gatewaysdk/environment/schema/host_surfaces.py +275 -0
  223. gatewaysdk/environment/schema/platform.py +1272 -0
  224. gatewaysdk/environment/schema/scaffold.py +250 -0
  225. gatewaysdk/environment/schema/skill.py +118 -0
  226. gatewaysdk/environment/schema/skills/agents/connector-template-author.md +202 -0
  227. gatewaysdk/environment/schema/skills/connector-schema-authoring/CONNECTORS.md +331 -0
  228. gatewaysdk/environment/schema/skills/connector-schema-authoring/CONVENTIONS.md +100 -0
  229. gatewaysdk/environment/schema/skills/connector-schema-authoring/SKILL.md +313 -0
  230. gatewaysdk/environment/schema/skills/world-data-ingestion/ROWS.md +223 -0
  231. gatewaysdk/environment/schema/skills/world-data-ingestion/SKILL.md +293 -0
  232. gatewaysdk/environment/schema/skills/worlds-getting-started/SKILL.md +390 -0
  233. gatewaysdk/environment/schema/slack.py +757 -0
  234. gatewaysdk/environment/schema/snapshot.py +479 -0
  235. gatewaysdk/environment/schema/store.py +904 -0
  236. gatewaysdk/environment/schema/tasks.py +525 -0
  237. gatewaysdk/environment/schema/tools_world.py +322 -0
  238. gatewaysdk/environment/schema/validation.py +577 -0
  239. gatewaysdk/environment/schema/worker.py +78 -0
  240. gatewaysdk/environment/schema/workers.py +500 -0
  241. gatewaysdk/environment/schema/world_tests.py +336 -0
  242. gatewaysdk/environments/__init__.py +24 -0
  243. gatewaysdk/environments/client.py +486 -0
  244. gatewaysdk/environments/types.py +318 -0
  245. gatewaysdk/execution/__init__.py +15 -0
  246. gatewaysdk/execution/base.py +64 -0
  247. gatewaysdk/execution/client_server.py +443 -0
  248. gatewaysdk/execution/events.py +69 -0
  249. gatewaysdk/execution/inter_process.py +16 -0
  250. gatewaysdk/execution/shared_memory.py +282 -0
  251. gatewaysdk/experiments/__init__.py +90 -0
  252. gatewaysdk/experiments/assignment.py +177 -0
  253. gatewaysdk/experiments/client.py +877 -0
  254. gatewaysdk/experiments/exposure.py +222 -0
  255. gatewaysdk/experiments/types.py +81 -0
  256. gatewaysdk/importers/__init__.py +49 -0
  257. gatewaysdk/importers/_normalize.py +91 -0
  258. gatewaysdk/importers/builder.py +173 -0
  259. gatewaysdk/importers/client.py +95 -0
  260. gatewaysdk/importers/langsmith.py +196 -0
  261. gatewaysdk/importers/recipes.py +249 -0
  262. gatewaysdk/instrumentation/__init__.py +300 -0
  263. gatewaysdk/instrumentation/agentops.py +314 -0
  264. gatewaysdk/instrumentation/agentops_langchain.py +45 -0
  265. gatewaysdk/instrumentation/base.py +119 -0
  266. gatewaysdk/instrumentation/litellm.py +83 -0
  267. gatewaysdk/instrumentation/registry.py +273 -0
  268. gatewaysdk/instrumentation/vllm.py +81 -0
  269. gatewaysdk/instrumentation/weave.py +500 -0
  270. gatewaysdk/integrations/__init__.py +15 -0
  271. gatewaysdk/integrations/gateway/__init__.py +11 -0
  272. gatewaysdk/integrations/gateway/client.py +171 -0
  273. gatewaysdk/integrations/tool_access.py +549 -0
  274. gatewaysdk/litagent/__init__.py +11 -0
  275. gatewaysdk/litagent/decorator.py +536 -0
  276. gatewaysdk/litagent/litagent.py +252 -0
  277. gatewaysdk/llm_proxy.py +1742 -0
  278. gatewaysdk/logging.py +370 -0
  279. gatewaysdk/memory.py +278 -0
  280. gatewaysdk/personas.py +186 -0
  281. gatewaysdk/platform/__init__.py +17 -0
  282. gatewaysdk/platform/builder.py +221 -0
  283. gatewaysdk/platform/compatibility.py +67 -0
  284. gatewaysdk/platform/manifest.py +185 -0
  285. gatewaysdk/platform/orchestrator.py +901 -0
  286. gatewaysdk/platform/registry.py +122 -0
  287. gatewaysdk/platform/worker.py +864 -0
  288. gatewaysdk/replay/__init__.py +1032 -0
  289. gatewaysdk/replay/pytest.py +56 -0
  290. gatewaysdk/reward.py +7 -0
  291. gatewaysdk/run.py +1781 -0
  292. gatewaysdk/runner/__init__.py +11 -0
  293. gatewaysdk/runner/agent.py +878 -0
  294. gatewaysdk/runner/base.py +182 -0
  295. gatewaysdk/runner/legacy.py +309 -0
  296. gatewaysdk/security.py +700 -0
  297. gatewaysdk/semconv.py +170 -0
  298. gatewaysdk/server.py +399 -0
  299. gatewaysdk/sessions.py +282 -0
  300. gatewaysdk/store/__init__.py +45 -0
  301. gatewaysdk/store/base.py +908 -0
  302. gatewaysdk/store/client_server.py +2093 -0
  303. gatewaysdk/store/collection/__init__.py +30 -0
  304. gatewaysdk/store/collection/base.py +587 -0
  305. gatewaysdk/store/collection/memory.py +970 -0
  306. gatewaysdk/store/collection/mongo.py +1412 -0
  307. gatewaysdk/store/collection_based.py +1823 -0
  308. gatewaysdk/store/gateway.py +983 -0
  309. gatewaysdk/store/gateway_listener.py +465 -0
  310. gatewaysdk/store/listener.py +58 -0
  311. gatewaysdk/store/memory.py +396 -0
  312. gatewaysdk/store/mongo.py +165 -0
  313. gatewaysdk/store/redis_stream.py +517 -0
  314. gatewaysdk/store/sqlite.py +3 -0
  315. gatewaysdk/store/threading.py +370 -0
  316. gatewaysdk/store/utils.py +142 -0
  317. gatewaysdk/tracer/__init__.py +14 -0
  318. gatewaysdk/tracer/base.py +286 -0
  319. gatewaysdk/tracer/dummy.py +106 -0
  320. gatewaysdk/tracer/otel.py +559 -0
  321. gatewaysdk/tracing/__init__.py +110 -0
  322. gatewaysdk/tracing/api.py +808 -0
  323. gatewaysdk/tracing/attributes.py +9 -0
  324. gatewaysdk/tracing/context.py +272 -0
  325. gatewaysdk/tracing/exporters/__init__.py +10 -0
  326. gatewaysdk/tracing/exporters/gateway.py +228 -0
  327. gatewaysdk/tracing/identity.py +288 -0
  328. gatewaysdk/tracing/init.py +620 -0
  329. gatewaysdk/tracing/instrumentors/__init__.py +15 -0
  330. gatewaysdk/tracing/instrumentors/claude_agent_sdk.py +766 -0
  331. gatewaysdk/tracing/instrumentors/instrumentation_principles.md +294 -0
  332. gatewaysdk/tracing/instrumentors/registry.py +352 -0
  333. gatewaysdk/tracing/mapping.py +729 -0
  334. gatewaysdk/tracing/processors.py +393 -0
  335. gatewaysdk/tracing/push.py +617 -0
  336. gatewaysdk/tracing/push_models.py +247 -0
  337. gatewaysdk/tracing/semconv.py +294 -0
  338. gatewaysdk/tracing/span_builder.py +356 -0
  339. gatewaysdk/trainer/__init__.py +6 -0
  340. gatewaysdk/trainer/init_utils.py +263 -0
  341. gatewaysdk/trainer/legacy.py +359 -0
  342. gatewaysdk/trainer/registry.py +12 -0
  343. gatewaysdk/trainer/trainer.py +638 -0
  344. gatewaysdk/types/__init__.py +63 -0
  345. gatewaysdk/types/core.py +556 -0
  346. gatewaysdk/types/resources.py +204 -0
  347. gatewaysdk/types/tracer.py +515 -0
  348. gatewaysdk/types/tracing.py +162 -0
  349. gatewaysdk/users.py +251 -0
  350. gatewaysdk/utils/__init__.py +1 -0
  351. gatewaysdk/utils/id.py +18 -0
  352. gatewaysdk/utils/metrics.py +1025 -0
  353. gatewaysdk/utils/otel.py +550 -0
  354. gatewaysdk/utils/otlp.py +556 -0
  355. gatewaysdk/utils/redact.py +22 -0
  356. gatewaysdk/utils/server_launcher.py +1045 -0
  357. gatewaysdk/utils/system_snapshot.py +90 -0
  358. gatewaysdk/verl/__init__.py +8 -0
  359. gatewaysdk/verl/__main__.py +6 -0
  360. gatewaysdk/verl/async_server.py +46 -0
  361. gatewaysdk/verl/config.yaml +27 -0
  362. gatewaysdk/verl/daemon.py +1154 -0
  363. gatewaysdk/verl/dataset.py +44 -0
  364. gatewaysdk/verl/entrypoint.py +248 -0
  365. gatewaysdk/verl/trainer.py +549 -0
  366. gatewaysdk/world_browser.py +748 -0
  367. gatewaysdk/world_data.py +453 -0
  368. gatewaysdk/world_sessions.py +978 -0
  369. gatewaysdk/world_tasks.py +285 -0
  370. gatewaysdk/world_tools.py +115 -0
  371. gatewaysdk/worlds.py +639 -0
  372. gatewaysdk-0.3.2.dist-info/METADATA +236 -0
  373. gatewaysdk-0.3.2.dist-info/RECORD +376 -0
  374. gatewaysdk-0.3.2.dist-info/WHEEL +4 -0
  375. gatewaysdk-0.3.2.dist-info/entry_points.txt +5 -0
  376. gatewaysdk-0.3.2.dist-info/licenses/LICENSE +19 -0
@@ -0,0 +1,904 @@
1
+ """Schema-aware SQLite state and atomic, provider-neutral mutation batches."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sqlite3
7
+ import threading
8
+ import uuid
9
+ from contextlib import contextmanager
10
+ from pathlib import Path
11
+ from typing import Any, Iterator
12
+
13
+ from .compiler import Contract, payload_value, quote
14
+ from .validation import (
15
+ ContractError,
16
+ canonical,
17
+ digest,
18
+ encoded,
19
+ fail,
20
+ members,
21
+ nonempty,
22
+ parse_json,
23
+ validate,
24
+ )
25
+
26
+ PRIVATE_DDL = """
27
+ CREATE TABLE _gateway_contract (contract TEXT NOT NULL);
28
+ CREATE TABLE _gateway_batches (id TEXT PRIMARY KEY, digest TEXT NOT NULL, metadata TEXT NOT NULL);
29
+ CREATE TABLE _gateway_lineage (batch TEXT NOT NULL REFERENCES _gateway_batches(id),
30
+ ordinal INTEGER NOT NULL, entity TEXT NOT NULL, key TEXT NOT NULL, source TEXT NOT NULL,
31
+ PRIMARY KEY(batch, ordinal));
32
+ CREATE TABLE benchmark_runtime_context (context_key TEXT PRIMARY KEY, scenario_id TEXT NOT NULL);
33
+ CREATE TABLE benchmark_runtime_parameters (key TEXT PRIMARY KEY, value TEXT NOT NULL);
34
+ CREATE TABLE benchmark_conversation_events (scenario_id TEXT NOT NULL, user_turn_index INTEGER NOT NULL,
35
+ event_type TEXT NOT NULL, actor TEXT NOT NULL, name TEXT, payload_json TEXT NOT NULL);
36
+
37
+ """
38
+ # The host owns these tables. Schema authors cannot declare them. Tools may
39
+ # read parameters; context/events are available only to trusted verifier reads.
40
+ RUNTIME_TABLES = {
41
+ "benchmark_runtime_context",
42
+ "benchmark_runtime_parameters",
43
+ "benchmark_conversation_events",
44
+ }
45
+ READ_ACTIONS = {
46
+ sqlite3.SQLITE_SELECT,
47
+ sqlite3.SQLITE_READ,
48
+ sqlite3.SQLITE_FUNCTION,
49
+ sqlite3.SQLITE_RECURSIVE,
50
+ }
51
+ WRITE_ACTIONS = {sqlite3.SQLITE_INSERT, sqlite3.SQLITE_UPDATE, sqlite3.SQLITE_DELETE}
52
+ # Objects an ingest session may create for its own staging; they never persist.
53
+ TEMP_ACTIONS = {
54
+ sqlite3.SQLITE_CREATE_TEMP_TABLE,
55
+ sqlite3.SQLITE_CREATE_TEMP_INDEX,
56
+ sqlite3.SQLITE_CREATE_TEMP_VIEW,
57
+ sqlite3.SQLITE_DROP_TEMP_TABLE,
58
+ sqlite3.SQLITE_DROP_TEMP_INDEX,
59
+ sqlite3.SQLITE_DROP_TEMP_VIEW,
60
+ }
61
+ # Bookkeeping and host tables no SQL session may read; parameters stay readable.
62
+ PRIVATE_TABLES = {
63
+ "_gateway_contract",
64
+ "_gateway_batches",
65
+ "_gateway_lineage",
66
+ "benchmark_runtime_context",
67
+ "benchmark_conversation_events",
68
+ }
69
+ UNSAFE_FUNCTIONS = {"load_extension", "writefile", "readfile"}
70
+
71
+
72
+ def _source(source: Any, location: str) -> None:
73
+ members(source, {"record", "evidence"}, {"record"}, location)
74
+ nonempty(source["record"], f"{location}/record")
75
+ evidence = source.get("evidence", [])
76
+ if not isinstance(evidence, list):
77
+ fail(
78
+ "provenance",
79
+ "evidence must be an array of stable reference/digest pairs",
80
+ location,
81
+ )
82
+ for index, entry in enumerate(evidence):
83
+ members(
84
+ entry,
85
+ {"reference", "digest"},
86
+ {"reference", "digest"},
87
+ f"{location}/evidence/{index}",
88
+ )
89
+ nonempty(entry["reference"], location)
90
+ # Digest syntax is explicit; providers can translate a verified base64
91
+ # SHA-256 to hex without retaining staging credentials or locations.
92
+ value = entry["digest"]
93
+ if not isinstance(value, str) or len(value) != 64 or any(c not in "0123456789abcdef" for c in value):
94
+ fail("provenance", "Evidence digest must be lowercase SHA-256 hex", location)
95
+
96
+
97
+ class ReadConnection:
98
+ """Compatibility conn= surface: execute SELECT only; no raw writes/commit."""
99
+
100
+ def __init__(self, store: "SQLiteStore"):
101
+ self._store = store
102
+
103
+ def execute(self, sql: str, params: Any = ()) -> "ReadCursor":
104
+ return ReadCursor(self._store.query(sql, tuple(params)))
105
+
106
+ def cursor(self):
107
+ fail(
108
+ "raw_write",
109
+ "Use conn.execute for reads and gateway_env.store for mutations",
110
+ )
111
+
112
+ def executescript(self, *args):
113
+ fail("raw_write", "Raw SQL scripts are unavailable in schema-backed tools")
114
+
115
+ def commit(self):
116
+ fail("raw_write", "The world owns the tool transaction")
117
+
118
+ rollback = commit
119
+ close = commit
120
+
121
+
122
+ class IngestConnection:
123
+ """Raw SQL inside one schema-gated ingest session.
124
+
125
+ Domain tables and TEMP objects are writable; the store owns the transaction
126
+ and commits only when every persisted row satisfies the contract.
127
+ """
128
+
129
+ def __init__(self, store: "SQLiteStore"):
130
+ self._store = store
131
+ self._open = True
132
+ self.result: dict | None = None
133
+
134
+ def execute(self, sql: str, params: Any = ()) -> sqlite3.Cursor:
135
+ return self._run(lambda conn: conn.execute(sql, params))
136
+
137
+ def executemany(self, sql: str, rows: Any) -> sqlite3.Cursor:
138
+ return self._run(lambda conn: conn.executemany(sql, rows))
139
+
140
+ def script(self, sql: str) -> None:
141
+ """Run a multi-statement script one statement at a time.
142
+
143
+ ``sqlite3.executescript`` would commit the session's transaction first,
144
+ so it is deliberately not exposed.
145
+ """
146
+ from .._world import split_sql_statements
147
+
148
+ for statement in split_sql_statements(sql):
149
+ self.execute(statement)
150
+
151
+ def _run(self, operation):
152
+ if not self._open:
153
+ fail("transaction", "The ingest session has ended")
154
+ store = self._store
155
+ store._key_error = None
156
+ try:
157
+ return operation(store._conn)
158
+ except sqlite3.Error as error:
159
+ context = {"sqlite": str(error)}
160
+ if store._key_error is not None:
161
+ context["key"] = store._key_error
162
+ raise ContractError(
163
+ "ingest_sql",
164
+ "Statement rejected; only domain tables and TEMP objects are writable",
165
+ **context,
166
+ ) from error
167
+
168
+ def commit(self):
169
+ fail("raw_write", "The store commits the ingest session when it ends")
170
+
171
+ rollback = commit
172
+ close = commit
173
+
174
+
175
+ class ReadCursor:
176
+ def __init__(self, rows: list[sqlite3.Row]):
177
+ self._rows = iter(rows)
178
+ self.rowcount = -1
179
+
180
+ def fetchone(self):
181
+ return next(self._rows, None)
182
+
183
+ def fetchall(self):
184
+ return list(self._rows)
185
+
186
+ def __iter__(self):
187
+ return self._rows
188
+
189
+
190
+ class SQLiteStore:
191
+ """Use create/open as context managers; every public mutation is atomic.
192
+
193
+ Payloads are JSON values, keys are dicts of exactly the primary-key fields.
194
+ Updates shallow-merge provided properties and validate the complete result.
195
+ """
196
+
197
+ def __init__(self, path: Path, contract: Contract, *, create: bool = False, audit: bool = True):
198
+ self.path = Path(path)
199
+ self.contract = contract
200
+ self._lock = threading.RLock()
201
+ self._depth = 0
202
+ self._internal = 0
203
+ self._touched: set[tuple[str, str]] = set()
204
+ self._read_only = False
205
+ self._private_reads = False
206
+ self._ingesting = False
207
+ self._key_error: dict | None = None
208
+ self._closed = False
209
+ if create and self.path.exists():
210
+ fail(
211
+ "state_exists",
212
+ "Refusing to replace existing state; reset/rebuild explicitly",
213
+ )
214
+ if not create and not self.path.is_file():
215
+ fail("state_missing", "Cannot open missing state")
216
+ self.path.parent.mkdir(parents=True, exist_ok=True)
217
+ self._conn = sqlite3.connect(
218
+ self.path,
219
+ isolation_level=None,
220
+ check_same_thread=False,
221
+ cached_statements=0,
222
+ )
223
+ self._conn.row_factory = sqlite3.Row
224
+ try:
225
+ self._conn.execute("PRAGMA foreign_keys = ON")
226
+ if self._conn.execute("PRAGMA foreign_keys").fetchone()[0] != 1:
227
+ fail("sqlite", "Foreign-key enforcement is required")
228
+ if create:
229
+ self._conn.executescript(contract.artifacts["db/schema.sql"].decode() + PRIVATE_DDL)
230
+ self._conn.execute(
231
+ "INSERT INTO _gateway_contract VALUES (?)",
232
+ (canonical(contract.lock),),
233
+ )
234
+ self._check_contract()
235
+ self._conn.create_function("gateway_key", 2, self._sql_key, deterministic=True)
236
+ self._conn.set_authorizer(self._authorize)
237
+ if audit:
238
+ self.audit()
239
+ except BaseException:
240
+ self._conn.close()
241
+ if create:
242
+ self.path.unlink(missing_ok=True)
243
+ raise
244
+
245
+ @classmethod
246
+ def create(cls, path: str | Path, contract: Contract) -> "SQLiteStore":
247
+ return cls(Path(path), contract, create=True)
248
+
249
+ @classmethod
250
+ def open(cls, path: str | Path, contract: Contract, *, audit: bool = True) -> "SQLiteStore":
251
+ """Open existing state. ``audit=False`` skips the full row audit: for a snapshot
252
+ the store itself wrote and whose bytes a sidecar digest has just verified,
253
+ every row was audited when it was written, so opening is then a matter of
254
+ milliseconds however many rows the file holds. The contract check always runs."""
255
+ return cls(Path(path), contract, audit=audit)
256
+
257
+ def _check_contract(self) -> None:
258
+ try:
259
+ rows = self._conn.execute("SELECT contract FROM _gateway_contract").fetchall()
260
+ if len(rows) != 1 or rows[0][0] != canonical(self.contract.lock):
261
+ fail(
262
+ "state_contract",
263
+ "State belongs to a different schema/package lock; no automatic migration",
264
+ )
265
+ # Compare actual DDL with fresh generated DDL, not merely a stored hash.
266
+ expected = sqlite3.connect(":memory:")
267
+ try:
268
+ expected.executescript(self.contract.artifacts["db/schema.sql"].decode() + PRIVATE_DDL)
269
+ expected_sql = dict(expected.execute("SELECT name, sql FROM sqlite_master WHERE sql IS NOT NULL"))
270
+ finally:
271
+ expected.close()
272
+ actual_sql = dict(self._conn.execute("SELECT name, sql FROM sqlite_master WHERE sql IS NOT NULL"))
273
+ if (
274
+ any(actual_sql.get(k) != v for k, v in expected_sql.items())
275
+ or set(actual_sql) - set(expected_sql) - RUNTIME_TABLES
276
+ ):
277
+ fail(
278
+ "state_contract",
279
+ "State DDL differs from the compiled storage contract",
280
+ )
281
+ except sqlite3.Error as error:
282
+ raise ContractError("state_contract", "State has no compatible schema contract") from error
283
+
284
+ def _authorize(self, action, first, second, database, trigger):
285
+ if self._read_only:
286
+ if action not in READ_ACTIONS or (
287
+ action == sqlite3.SQLITE_READ
288
+ and first not in self.contract.entities
289
+ and first != "benchmark_runtime_parameters"
290
+ and not (self._private_reads and first in RUNTIME_TABLES)
291
+ ):
292
+ return sqlite3.SQLITE_DENY
293
+ if action == sqlite3.SQLITE_FUNCTION and (second or "").lower() in UNSAFE_FUNCTIONS:
294
+ return sqlite3.SQLITE_DENY
295
+ return sqlite3.SQLITE_OK
296
+ if self._internal:
297
+ return sqlite3.SQLITE_OK
298
+ if self._ingesting:
299
+ return self._authorize_ingest(action, first, second, database)
300
+ if action in READ_ACTIONS:
301
+ return sqlite3.SQLITE_OK
302
+ if action in WRITE_ACTIONS and first in RUNTIME_TABLES:
303
+ return sqlite3.SQLITE_OK
304
+ return sqlite3.SQLITE_DENY
305
+
306
+ def _authorize_ingest(self, action, first, second, database):
307
+ """Domain tables and TEMP staging are writable; everything else is not.
308
+
309
+ Transaction control, PRAGMA, ATTACH, persistent DDL and the private
310
+ bookkeeping tables stay with the store, so a session cannot commit
311
+ early, disable enforcement, or forge provenance.
312
+ """
313
+ if action in READ_ACTIONS:
314
+ if action == sqlite3.SQLITE_READ and first in PRIVATE_TABLES:
315
+ return sqlite3.SQLITE_DENY
316
+ if action == sqlite3.SQLITE_FUNCTION and (second or "").lower() in UNSAFE_FUNCTIONS:
317
+ return sqlite3.SQLITE_DENY
318
+ return sqlite3.SQLITE_OK
319
+ if action in WRITE_ACTIONS:
320
+ if first in self.contract.entities or database == "temp":
321
+ return sqlite3.SQLITE_OK
322
+ return sqlite3.SQLITE_DENY
323
+ if action in TEMP_ACTIONS or action == sqlite3.SQLITE_SAVEPOINT:
324
+ return sqlite3.SQLITE_OK
325
+ return sqlite3.SQLITE_DENY
326
+
327
+ def _sql_key(self, entity: Any, payload: Any) -> str | None:
328
+ """``gateway_key(entity, payload)``: the encoded ``_key`` for a payload.
329
+
330
+ Returns NULL on an invalid entity or payload so the NOT NULL constraint
331
+ rejects the row; the reason is attached to the resulting error.
332
+ """
333
+ try:
334
+ value = payload
335
+ if isinstance(payload, (str, bytes)):
336
+ value = parse_json(payload)
337
+ if not isinstance(value, dict):
338
+ fail("key", "gateway_key needs a JSON object payload", entity=entity)
339
+ return self._key(entity, value)
340
+ except (ContractError, ValueError, TypeError) as error:
341
+ detail = getattr(error, "detail", None)
342
+ self._key_error = detail or {"code": "key", "message": str(error)}
343
+ return None
344
+
345
+ @contextmanager
346
+ def _trusted(self):
347
+ self._internal += 1
348
+ try:
349
+ yield
350
+ finally:
351
+ self._internal -= 1
352
+
353
+ @contextmanager
354
+ def transaction(self) -> Iterator["SQLiteStore"]:
355
+ with self._lock:
356
+ if self._ingesting:
357
+ fail(
358
+ "transaction",
359
+ "Use SQL inside an ingest session; store mutations belong outside",
360
+ )
361
+ outer = self._depth == 0
362
+ savepoint = f"gateway_batch_{self._depth}"
363
+ with self._trusted():
364
+ self._conn.execute("BEGIN IMMEDIATE" if outer else f"SAVEPOINT {savepoint}")
365
+ if outer:
366
+ self._touched = set()
367
+ self._depth += 1
368
+ try:
369
+ yield self
370
+ if outer:
371
+ self._audit_touched()
372
+ with self._trusted():
373
+ self._conn.execute("COMMIT" if outer else f"RELEASE {savepoint}")
374
+ except BaseException:
375
+ with self._trusted():
376
+ self._conn.execute("ROLLBACK" if outer else f"ROLLBACK TO {savepoint}")
377
+ if not outer:
378
+ self._conn.execute(f"RELEASE {savepoint}")
379
+ raise
380
+ finally:
381
+ self._depth -= 1
382
+
383
+ def _entity(self, name: str) -> dict:
384
+ if not isinstance(name, str) or name not in self.contract.entities:
385
+ fail("entity", "Unknown entity type", entity=name)
386
+ return self.contract.entities[name]
387
+
388
+ def _key(self, name: str, value: dict, *, exact: bool = False) -> str:
389
+ entity = self._entity(name)
390
+ fields = entity["primary_key"]
391
+ if not isinstance(value, dict) or not set(fields) <= set(value) or (exact and set(value) != set(fields)):
392
+ fail("key", "Supply exactly the declared primary-key fields", entity=name)
393
+ for field in fields:
394
+ validate(
395
+ entity["schema"]["properties"][field],
396
+ value[field],
397
+ location=f"{entity['location']}/schema/properties/{field}",
398
+ )
399
+ # JSON Schema integers include 1.0; identity normalizes mathematically
400
+ # integral keys only, without changing the actual payload value.
401
+ parts = [int(value[f]) if type(value[f]) is float else value[f] for f in fields]
402
+ return canonical(parts)
403
+
404
+ def get(self, entity: str, key: dict) -> dict | None:
405
+ with self._lock:
406
+ encoded_key = self._key(entity, key, exact=True)
407
+ row = self._conn.execute(f"SELECT payload FROM {quote(entity)} WHERE _key = ?", (encoded_key,)).fetchone()
408
+ return json.loads(row[0]) if row else None
409
+
410
+ def list(self, entity: str) -> list[dict]:
411
+ with self._lock:
412
+ self._entity(entity)
413
+ return [
414
+ json.loads(row[0]) for row in self._conn.execute(f"SELECT payload FROM {quote(entity)} ORDER BY _key")
415
+ ]
416
+
417
+ def query(self, sql: str, params: tuple = (), *, private: bool = False) -> list[sqlite3.Row]:
418
+ """Read-only SQL over domain tables, enforced by SQLite's authorizer."""
419
+ with self._lock:
420
+ self._read_only = True
421
+ self._private_reads = private
422
+ try:
423
+ return self._conn.execute(sql, params).fetchall()
424
+ except sqlite3.Error as error:
425
+ raise ContractError("read_only", "Query failed or attempted a forbidden operation") from error
426
+ finally:
427
+ self._read_only = False
428
+ self._private_reads = False
429
+
430
+ def _mutate(self, mutation: dict) -> str:
431
+ name = mutation["entity"]
432
+ entity = self._entity(name)
433
+ operation = mutation["op"]
434
+ if operation == "insert":
435
+ value = mutation["value"]
436
+ validate(entity["schema"], value, location=f"{entity['location']}/schema")
437
+ key = self._key(name, value)
438
+ else:
439
+ key = self._key(name, mutation["key"], exact=True)
440
+ current = self.get(name, mutation["key"])
441
+ if current is None:
442
+ fail(
443
+ "not_found",
444
+ "No entity exists at this key",
445
+ entity=name,
446
+ key=mutation["key"],
447
+ )
448
+ if operation == "update":
449
+ if not isinstance(mutation["value"], dict):
450
+ fail("type", "An update must be an object")
451
+ value = {**current, **mutation["value"]}
452
+ validate(entity["schema"], value, location=f"{entity['location']}/schema")
453
+ if self._key(name, value) != key:
454
+ fail(
455
+ "key_immutable",
456
+ "Updates cannot change primary keys",
457
+ entity=name,
458
+ )
459
+ self._touched.add((name, key))
460
+ try:
461
+ with self._trusted():
462
+ if operation == "insert":
463
+ self._conn.execute(
464
+ f"INSERT INTO {quote(name)} (_key, payload) VALUES (?, ?)",
465
+ (key, canonical(value)),
466
+ )
467
+ elif operation == "update":
468
+ self._conn.execute(
469
+ f"UPDATE {quote(name)} SET payload = ? WHERE _key = ?",
470
+ (canonical(value), key),
471
+ )
472
+ else:
473
+ self._conn.execute(f"DELETE FROM {quote(name)} WHERE _key = ?", (key,))
474
+ except sqlite3.IntegrityError as error:
475
+ raise ContractError(
476
+ "unique",
477
+ "Primary or unique key conflicts with existing state",
478
+ entity=name,
479
+ key=json.loads(key),
480
+ schema_path=f"{entity['location']}/unique",
481
+ ) from error
482
+ return key
483
+
484
+ def apply_batch(self, batch: dict) -> dict:
485
+ """Atomic rejection; exact retries no-op, conflicting ID reuse rejects."""
486
+ members(
487
+ batch,
488
+ {"id", "target", "mapping", "coverage", "mutations"},
489
+ {"id", "target", "mapping", "coverage", "mutations"},
490
+ "/batch",
491
+ )
492
+ nonempty(batch["id"], "/batch/id")
493
+ if batch["target"] != self.contract.target:
494
+ fail(
495
+ "batch_target",
496
+ "Batch targets a different domain identity/version/hash",
497
+ "/batch/target",
498
+ )
499
+ members(batch["mapping"], {"id", "version"}, {"id", "version"}, "/batch/mapping")
500
+ for key, value in batch["mapping"].items():
501
+ nonempty(value, f"/batch/mapping/{key}")
502
+ if batch["coverage"] not in ("partial", "complete"):
503
+ fail(
504
+ "coverage",
505
+ "coverage must be partial or complete (neither implies deletion)",
506
+ "/batch/coverage",
507
+ )
508
+ if not isinstance(batch["mutations"], list):
509
+ fail("batch", "mutations must be an array", "/batch/mutations")
510
+ batch_hash = digest(encoded(batch))
511
+ with self.transaction():
512
+ prior = self._conn.execute("SELECT digest FROM _gateway_batches WHERE id = ?", (batch["id"],)).fetchone()
513
+ if prior:
514
+ if prior[0] != batch_hash:
515
+ fail(
516
+ "idempotency_conflict",
517
+ "Batch ID was already applied with different content",
518
+ "/batch/id",
519
+ batch=batch["id"],
520
+ )
521
+ return {"status": "already_applied", "batch": batch["id"], "applied": 0}
522
+ with self._trusted():
523
+ self._conn.execute(
524
+ "INSERT INTO _gateway_batches VALUES (?, ?, ?)",
525
+ (
526
+ batch["id"],
527
+ batch_hash,
528
+ canonical({k: v for k, v in batch.items() if k != "mutations"}),
529
+ ),
530
+ )
531
+ for index, mutation in enumerate(batch["mutations"]):
532
+ location = f"/batch/mutations/{index}"
533
+ try:
534
+ members(
535
+ mutation,
536
+ {"op", "entity", "key", "value", "source"},
537
+ {"op", "entity", "source"},
538
+ location,
539
+ )
540
+ op = mutation["op"]
541
+ if not isinstance(op, str):
542
+ fail(
543
+ "operation",
544
+ "Operation must be insert, update or delete",
545
+ location,
546
+ )
547
+ required = {
548
+ "insert": {"value"},
549
+ "update": {"key", "value"},
550
+ "delete": {"key"},
551
+ }.get(op)
552
+ if required is None:
553
+ fail(
554
+ "operation",
555
+ "Only insert, update and delete are supported",
556
+ location,
557
+ )
558
+ members(
559
+ mutation,
560
+ {"op", "entity", "source"} | required,
561
+ {"op", "entity", "source"} | required,
562
+ location,
563
+ )
564
+ _source(mutation["source"], f"{location}/source")
565
+ key = self._mutate(mutation)
566
+ with self._trusted():
567
+ self._conn.execute(
568
+ "INSERT INTO _gateway_lineage VALUES (?, ?, ?, ?, ?)",
569
+ (
570
+ batch["id"],
571
+ index,
572
+ mutation["entity"],
573
+ key,
574
+ canonical(mutation["source"]),
575
+ ),
576
+ )
577
+ except ContractError as error:
578
+ # Only attach source after it has passed metadata validation.
579
+ context = {"batch": batch["id"], "index": index}
580
+ if isinstance(mutation, dict):
581
+ context["entity"] = mutation.get("entity")
582
+ if "key" in mutation:
583
+ context["key"] = mutation["key"]
584
+ try:
585
+ _source(mutation.get("source"), location)
586
+ context["source"] = mutation["source"]
587
+ except ContractError:
588
+ pass
589
+ raise error.contextualize(**context)
590
+ # Nested batches inside a tool defer references to the tool boundary.
591
+ return {
592
+ "status": "applied",
593
+ "batch": batch["id"],
594
+ "applied": len(batch["mutations"]),
595
+ }
596
+
597
+ def _single(self, op: str, entity: str, *, source: dict | None = None, **values):
598
+ self.apply_batch(
599
+ {
600
+ "id": uuid.uuid4().hex,
601
+ "target": self.contract.target,
602
+ "mapping": {"id": "gateway-store", "version": "1"},
603
+ "coverage": "partial",
604
+ "mutations": [
605
+ {
606
+ "op": op,
607
+ "entity": entity,
608
+ **values,
609
+ "source": (source if source is not None else {"record": f"store.{op}"}),
610
+ }
611
+ ],
612
+ }
613
+ )
614
+
615
+ def insert(self, entity: str, value: dict, *, source: dict | None = None) -> dict:
616
+ self._single("insert", entity, value=value, source=source)
617
+ return json.loads(canonical(value))
618
+
619
+ def update(self, entity: str, key: dict, patch: dict, *, source: dict | None = None) -> dict:
620
+ with self._lock:
621
+ self._single("update", entity, key=key, value=patch, source=source)
622
+ return self.get(entity, key)
623
+
624
+ def delete(self, entity: str, key: dict, *, source: dict | None = None) -> None:
625
+ self._single("delete", entity, key=key, source=source)
626
+
627
+ @contextmanager
628
+ def ingest(
629
+ self,
630
+ *,
631
+ source: dict,
632
+ mapping: dict,
633
+ batch_id: str | None = None,
634
+ limit: int = 50,
635
+ ) -> Iterator[IngestConnection]:
636
+ """Schema-gated raw SQL: write freely, commit only if every row conforms.
637
+
638
+ The session yields a connection that may INSERT/UPDATE/DELETE domain
639
+ tables and create TEMP staging objects. ``gateway_key(entity, payload)``
640
+ computes ``_key`` in SQL. On exit the store validates all persisted
641
+ state; any violation (up to ``limit`` are reported together) rolls the
642
+ whole session back. Committed changes are recorded as one batch with a
643
+ lineage row per touched key, so raw ingestion keeps provenance.
644
+ """
645
+ _source(source, "/ingest/source")
646
+ members(mapping, {"id", "version"}, {"id", "version"}, "/ingest/mapping")
647
+ for key, value in mapping.items():
648
+ nonempty(value, f"/ingest/mapping/{key}")
649
+ with self._lock:
650
+ if self._depth or self._ingesting:
651
+ fail(
652
+ "transaction",
653
+ "Ingest needs no active tool, batch or ingest transaction",
654
+ )
655
+ batch = batch_id if batch_id is not None else uuid.uuid4().hex
656
+ nonempty(batch, "/ingest/id")
657
+ if self._conn.execute("SELECT 1 FROM _gateway_batches WHERE id = ?", (batch,)).fetchone():
658
+ fail(
659
+ "idempotency_conflict",
660
+ "Ingest ID was already applied; raw sessions are not replayable",
661
+ "/ingest/id",
662
+ batch=batch,
663
+ )
664
+ before = self._state()
665
+ connection = IngestConnection(self)
666
+ with self._trusted():
667
+ self._conn.execute("BEGIN IMMEDIATE")
668
+ self._depth = 1
669
+ self._ingesting = True
670
+ try:
671
+ yield connection
672
+ violations = self._violations(limit)
673
+ if violations:
674
+ fail(
675
+ "ingest_rejected",
676
+ "Persisted rows violate the contract; nothing was committed",
677
+ batch=batch,
678
+ violations=[error.detail for error in violations],
679
+ truncated=len(violations) >= limit,
680
+ )
681
+ changes = self._changes(before, self._state())
682
+ metadata = {
683
+ "id": batch,
684
+ "kind": "ingest",
685
+ "target": self.contract.target,
686
+ "mapping": mapping,
687
+ "coverage": "partial",
688
+ }
689
+ with self._trusted():
690
+ if changes:
691
+ self._conn.execute(
692
+ "INSERT INTO _gateway_batches VALUES (?, ?, ?)",
693
+ (batch, digest(encoded(changes)), canonical(metadata)),
694
+ )
695
+ self._conn.executemany(
696
+ "INSERT INTO _gateway_lineage VALUES (?, ?, ?, ?, ?)",
697
+ [
698
+ (batch, index, entity, key, canonical(source))
699
+ for index, (entity, key, _) in enumerate(changes)
700
+ ],
701
+ )
702
+ self._conn.execute("COMMIT")
703
+ except BaseException:
704
+ with self._trusted():
705
+ self._conn.execute("ROLLBACK")
706
+ raise
707
+ finally:
708
+ self._ingesting = False
709
+ self._depth = 0
710
+ connection._open = False
711
+ self._drop_temp_objects()
712
+ counts = {op: 0 for op in ("insert", "update", "delete")}
713
+ for _, _, op in changes:
714
+ counts[op] += 1
715
+ connection.result = {
716
+ "status": "applied" if changes else "unchanged",
717
+ "batch": batch,
718
+ "inserted": counts["insert"],
719
+ "updated": counts["update"],
720
+ "deleted": counts["delete"],
721
+ }
722
+
723
+ def _drop_temp_objects(self) -> None:
724
+ """TEMP staging belongs to the session, not the connection."""
725
+ with self._trusted():
726
+ objects = self._conn.execute(
727
+ "SELECT type, name FROM sqlite_temp_master " "WHERE type IN ('view', 'table') ORDER BY type DESC"
728
+ ).fetchall()
729
+ for kind, name in objects:
730
+ self._conn.execute(f"DROP {kind} IF EXISTS temp.{quote(name)}")
731
+
732
+ def _state(self) -> dict[str, dict[str, str]]:
733
+ """Persisted ``_key -> payload`` per entity, for change attribution."""
734
+ with self._trusted():
735
+ return {
736
+ name: dict(self._conn.execute(f"SELECT _key, payload FROM {quote(name)}"))
737
+ for name in self.contract.entities
738
+ }
739
+
740
+ @staticmethod
741
+ def _changes(before: dict, after: dict) -> list[list[str]]:
742
+ changes = []
743
+ for name in sorted(before):
744
+ old, new = before[name], after[name]
745
+ for key in sorted(set(old) | set(new)):
746
+ if old.get(key) == new.get(key):
747
+ continue
748
+ if key not in old:
749
+ op = "insert"
750
+ elif key not in new:
751
+ op = "delete"
752
+ else:
753
+ op = "update"
754
+ changes.append([name, key, op])
755
+ return changes
756
+
757
+ def lineage(self, entity: str, key: dict) -> list[dict]:
758
+ with self._lock:
759
+ encoded_key = self._key(entity, key, exact=True)
760
+ rows = self._conn.execute(
761
+ "SELECT b.metadata, l.ordinal, l.source "
762
+ "FROM _gateway_lineage l "
763
+ "JOIN _gateway_batches b ON b.id=l.batch "
764
+ "WHERE l.entity=? AND l.key=? "
765
+ "ORDER BY b.rowid, l.ordinal",
766
+ (entity, encoded_key),
767
+ )
768
+ return [
769
+ {
770
+ "batch": json.loads(row[0]),
771
+ "index": row[1],
772
+ "source": json.loads(row[2]),
773
+ }
774
+ for row in rows
775
+ ]
776
+
777
+ def _row_violation(self, name: str, entity: dict, row) -> ContractError | None:
778
+ """The contract violation for one persisted row, with lineage context."""
779
+ try:
780
+ value = parse_json(row[1], f"{entity['location']}/schema")
781
+ except ContractError as error:
782
+ return error.contextualize(entity=name)
783
+ except (ValueError, TypeError):
784
+ return ContractError("state_payload", "Stored entity payload is not JSON", entity=name)
785
+ try:
786
+ validate(entity["schema"], value, location=f"{entity['location']}/schema")
787
+ if self._key(name, value) != row[0]:
788
+ fail("key", "Stored key differs from payload identity", entity=name)
789
+ for rel in entity["relationships"]:
790
+ keys = [payload_value(value, field) for field in rel["fields"]]
791
+ if all(k is None for k in keys):
792
+ continue
793
+ if any(k is None for k in keys):
794
+ fail(
795
+ "foreign_key",
796
+ "Composite references must be wholly present or wholly absent/null",
797
+ f"{entity['location']}/relationships",
798
+ )
799
+ target = self.contract.entities[rel["target"]]
800
+ target_key = dict(zip(target["primary_key"], keys))
801
+ if self.get(rel["target"], target_key) is None:
802
+ fail(
803
+ "foreign_key",
804
+ "Relationship target does not exist; incomplete evidence requires an explicit mapping decision",
805
+ f"{entity['location']}/relationships",
806
+ )
807
+ except ContractError as error:
808
+ try:
809
+ diagnostic_key = json.loads(row[0])
810
+ except (ValueError, TypeError):
811
+ diagnostic_key = None
812
+ context = {"entity": name, "key": diagnostic_key}
813
+ with self._trusted():
814
+ lineage = self._conn.execute(
815
+ "SELECT batch, ordinal, source FROM _gateway_lineage "
816
+ "WHERE entity=? AND key=? ORDER BY rowid DESC LIMIT 1",
817
+ (name, row[0]),
818
+ ).fetchone()
819
+ if lineage:
820
+ context.update(batch=lineage[0], index=lineage[1], source=json.loads(lineage[2]))
821
+ return error.contextualize(**context)
822
+ return None
823
+
824
+ def _violations(self, limit: int | None = None) -> list[ContractError]:
825
+ """Contract violations in persisted state, at most ``limit`` of them."""
826
+ found: list[ContractError] = []
827
+ with self._trusted():
828
+ rows = [
829
+ (name, entity, row)
830
+ for name, entity in self.contract.entities.items()
831
+ for row in self._conn.execute(f"SELECT _key, payload FROM {quote(name)} ORDER BY _key").fetchall()
832
+ ]
833
+ for name, entity, row in rows:
834
+ error = self._row_violation(name, entity, row)
835
+ if error is not None:
836
+ found.append(error)
837
+ if limit is not None and len(found) >= limit:
838
+ return found
839
+ with self._trusted():
840
+ if self._conn.execute("PRAGMA foreign_key_check").fetchone():
841
+ found.append(ContractError("foreign_key", "Persisted SQLite relationship check failed"))
842
+ return found
843
+
844
+ def audit(self) -> None:
845
+ """Validate real persisted payloads, key identity and strict references."""
846
+ with self._lock:
847
+ violations = self._violations(limit=1)
848
+ if violations:
849
+ raise violations[0]
850
+
851
+ def _audit_touched(self) -> None:
852
+ """The commit-time audit of one transaction: the rows it wrote, re-read from
853
+ disk and checked like a full audit, plus SQLite's own relationship check over
854
+ the whole state (a delete that orphans an untouched row shows up there).
855
+ Rows the transaction never touched cannot have changed, so a transaction's
856
+ cost follows its size, not the size of the world."""
857
+ with self._trusted():
858
+ for name, key in sorted(self._touched):
859
+ entity = self.contract.entities[name]
860
+ row = self._conn.execute(f"SELECT _key, payload FROM {quote(name)} WHERE _key = ?", (key,)).fetchone()
861
+ if row is None:
862
+ continue
863
+ error = self._row_violation(name, entity, row)
864
+ if error is not None:
865
+ raise error
866
+ if self._conn.execute("PRAGMA foreign_key_check").fetchone():
867
+ raise ContractError("foreign_key", "Persisted SQLite relationship check failed")
868
+
869
+ def counts(self) -> dict[str, int]:
870
+ """Rows per entity, counted by SQLite: no payload is decoded."""
871
+ with self._lock, self._trusted():
872
+ return {
873
+ name: self._conn.execute(f"SELECT COUNT(*) FROM {quote(name)}").fetchone()[0]
874
+ for name in self.contract.entities
875
+ }
876
+
877
+ def snapshot(self, destination: str | Path) -> Path:
878
+ with self._lock:
879
+ if self._depth:
880
+ fail("transaction", "Cannot snapshot uncommitted state")
881
+ self.audit()
882
+ dest = Path(destination)
883
+ if dest.exists():
884
+ fail("state_exists", "Snapshot destination already exists")
885
+ dest.parent.mkdir(parents=True, exist_ok=True)
886
+ target = sqlite3.connect(dest)
887
+ try:
888
+ self._conn.backup(target)
889
+ finally:
890
+ target.close()
891
+ return dest
892
+
893
+ def close(self) -> None:
894
+ with self._lock:
895
+ if self._depth:
896
+ fail("transaction", "Cannot close an active tool/batch transaction")
897
+ self._conn.close()
898
+ self._closed = True
899
+
900
+ def __enter__(self):
901
+ return self
902
+
903
+ def __exit__(self, *args):
904
+ self.close()