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,525 @@
1
+ """Session tasks: a task supplied at open instead of read from the bundle.
2
+
3
+ A bundled task is ``scenarios/<name>/`` in the version (instruction, seed
4
+ overlay, ``checks/verify.py``). A session task is the same three things sent
5
+ with the open, for one session, so one published api world serves any number
6
+ of end users each with their own goal, starting rows and pass criteria.
7
+
8
+ This module is the host's half of the contract whose other half is
9
+ ``gateway/packages/shared/src/domain/worldTasks.ts`` (the platform validates
10
+ with Zod, the host validates again here: it trusts nothing it did not check).
11
+ Keep the two in step by hand; the limits and the check grammar are the same.
12
+
13
+ * :func:`parse_task_spec` — the spec, checked and normalised.
14
+ * :func:`write_task_dir` — the spec on disk under the session
15
+ (``task.json``, ``instruction.md``, ``checks/verify.py``), where a grade
16
+ and a restart find it.
17
+ * :func:`evaluate_assertions` — the declarative grader, pure Python over
18
+ the state dump, so a check cannot read outside its session.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import re
25
+ from dataclasses import dataclass
26
+ from pathlib import Path
27
+ from typing import Any, Iterable
28
+
29
+ from .validation import fail
30
+
31
+ # The caps, as the platform's WORLD_TASK_LIMITS.
32
+ NAME_CHARS = 120
33
+ INSTRUCTION_BYTES = 32 * 1024
34
+ METADATA_BYTES = 4 * 1024
35
+ SEED_ROWS = 5_000
36
+ SEED_BYTES = 4 * 1024 * 1024
37
+ CHECKS = 200
38
+ PYTHON_SOURCE_BYTES = 64 * 1024
39
+ PYTHON_TIMEOUT_S = 30.0
40
+ RUBRIC_BYTES = 16 * 1024
41
+ RUBRIC_CRITERIA = 50
42
+ RUBRIC_CRITERION_BYTES = 2 * 1024
43
+ TOOLS = 200
44
+ TOOL_NAME_CHARS = 200
45
+
46
+ SEED_MODES = ("append", "replace")
47
+ ASSERTIONS = ("exists", "absent", "count", "all", "any")
48
+ FIELD_OPS = ("eq", "neq", "in", "gt", "gte", "lt", "lte", "contains")
49
+ _NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*$")
50
+ _CHECK_ID = re.compile(r"^[A-Za-z0-9_.-]+$")
51
+ _CRITERION_ID = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_.-]*$")
52
+ _TOOL_NAME = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_.:-]*$")
53
+
54
+ TASK_DIR = "task"
55
+ TASK_FILE = "task.json"
56
+
57
+
58
+ def _bytes(value: Any) -> int:
59
+ text = value if isinstance(value, str) else json.dumps(value, separators=(",", ":"))
60
+ return len(text.encode("utf-8"))
61
+
62
+
63
+ def _scalar(value: Any) -> bool:
64
+ return value is None or isinstance(value, (str, bool, int, float))
65
+
66
+
67
+ @dataclass(frozen=True)
68
+ class TaskSpec:
69
+ """A validated session task. ``raw`` is the normalised spec as a dict: what
70
+ the platform sent, defaults filled, and what is written to ``task.json``
71
+ and compared when an open is retried."""
72
+
73
+ instruction: str
74
+ name: str | None
75
+ seed: dict | None
76
+ grader: dict | None
77
+ metadata: dict
78
+ raw: dict
79
+ #: The tools the session may call, or None for every tool the world serves.
80
+ tools: tuple[str, ...] | None = None
81
+
82
+ @property
83
+ def grader_kind(self) -> str | None:
84
+ return self.grader["kind"] if self.grader else None
85
+
86
+ @property
87
+ def graded_on_platform(self) -> bool:
88
+ """A rubric task is judged by the platform's inference path, never here."""
89
+ return self.grader_kind == "rubric"
90
+
91
+ def allows_tool(self, tool: str) -> bool:
92
+ return self.tools is None or tool in self.tools
93
+
94
+ def entities(self) -> list[str]:
95
+ """Every entity the task names, seed rows and assertion checks alike, in
96
+ order of first mention. A world that defines none of a name cannot start
97
+ or grade this task: a seed row has no table, and a check over a table
98
+ that does not exist selects no rows, which an ``absent`` or ``count:
99
+ eq 0`` check would pass on a typo."""
100
+ named: list[str] = []
101
+ if self.seed is not None:
102
+ named.extend(self.seed["rows"])
103
+ if self.grader is not None and self.grader["kind"] == "assertions":
104
+ named.extend(check["entity"] for check in self.grader["checks"])
105
+ return list(dict.fromkeys(named))
106
+
107
+
108
+ def unknown_task_entities(spec: TaskSpec, defined: Iterable[str]) -> list[str]:
109
+ """The entities ``spec`` names that ``defined`` (a world contract's) lacks."""
110
+ known = set(defined)
111
+ return [entity for entity in spec.entities() if entity not in known]
112
+
113
+
114
+ def parse_task_spec(value: Any) -> TaskSpec:
115
+ """The spec checked for shape and size; every refusal names its path."""
116
+ if not isinstance(value, dict):
117
+ fail("task", "a task is a json object", "/task")
118
+ unknown = set(value) - {"name", "instruction", "seed", "grader", "tools", "metadata"}
119
+ if unknown:
120
+ fail("task", f"unknown task field(s): {', '.join(sorted(unknown))}", "/task")
121
+ name = value.get("name")
122
+ if name is not None and (not isinstance(name, str) or not _NAME.match(name) or len(name) > NAME_CHARS):
123
+ fail("task", "a task name is letters, digits, _ . - (leading letter or digit)", "/task/name")
124
+ instruction = value.get("instruction")
125
+ if not isinstance(instruction, str) or not instruction:
126
+ fail("task", "a task needs an instruction", "/task/instruction")
127
+ if _bytes(instruction) > INSTRUCTION_BYTES:
128
+ fail("task", f"an instruction is at most {INSTRUCTION_BYTES} bytes", "/task/instruction")
129
+ seed = _parse_seed(value.get("seed"))
130
+ grader = _parse_grader(value.get("grader"))
131
+ tools = _parse_tools(value.get("tools"))
132
+ metadata = value.get("metadata")
133
+ if metadata is None:
134
+ metadata = {}
135
+ if not isinstance(metadata, dict) or any(not isinstance(k, str) or not k for k in metadata):
136
+ fail("task", "metadata is a json object with string keys", "/task/metadata")
137
+ if _bytes(metadata) > METADATA_BYTES:
138
+ fail("task", f"task metadata is at most {METADATA_BYTES} bytes of json", "/task/metadata")
139
+ raw: dict = {"instruction": instruction}
140
+ if name is not None:
141
+ raw["name"] = name
142
+ if seed is not None:
143
+ raw["seed"] = seed
144
+ if grader is not None:
145
+ raw["grader"] = grader
146
+ if tools is not None:
147
+ raw["tools"] = list(tools)
148
+ if metadata:
149
+ raw["metadata"] = metadata
150
+ return TaskSpec(
151
+ instruction=instruction, name=name, seed=seed, grader=grader, metadata=metadata, raw=raw, tools=tools
152
+ )
153
+
154
+
155
+ def _parse_tools(tools: Any) -> tuple[str, ...] | None:
156
+ if tools is None:
157
+ return None
158
+ if not isinstance(tools, list) or not tools:
159
+ fail("task", "tools is a non-empty list of tool names", "/task/tools")
160
+ if len(tools) > TOOLS:
161
+ fail("task", f"a task allows at most {TOOLS} tools", "/task/tools")
162
+ for i, tool in enumerate(tools):
163
+ if not isinstance(tool, str) or not _TOOL_NAME.match(tool) or len(tool) > TOOL_NAME_CHARS:
164
+ fail("task", "a tool name is letters, digits, _ . : -", f"/task/tools/{i}")
165
+ if len(set(tools)) != len(tools):
166
+ fail("task", "a tool is listed once", "/task/tools")
167
+ return tuple(tools)
168
+
169
+
170
+ def unknown_task_tools(spec: TaskSpec, served: Iterable[str]) -> list[str]:
171
+ """The tools ``spec`` allows that the world does not serve. A name the
172
+ world never had would make the allow-list smaller than the author meant
173
+ without anyone noticing, so it is refused at open."""
174
+ if spec.tools is None:
175
+ return []
176
+ known = set(served)
177
+ return [tool for tool in spec.tools if tool not in known]
178
+
179
+
180
+ def _parse_seed(seed: Any) -> dict | None:
181
+ if seed is None:
182
+ return None
183
+ if not isinstance(seed, dict) or set(seed) - {"mode", "rows"}:
184
+ fail("task", "seed is {mode?: append|replace, rows: {entity: [row, ...]}}", "/task/seed")
185
+ mode = seed.get("mode", "append")
186
+ if mode not in SEED_MODES:
187
+ fail("task", f"seed mode must be one of {', '.join(SEED_MODES)}", "/task/seed/mode")
188
+ rows = seed.get("rows")
189
+ if not isinstance(rows, dict):
190
+ fail("task", "seed.rows is {entity: [row, ...]}", "/task/seed/rows")
191
+ total = 0
192
+ for entity, entity_rows in rows.items():
193
+ if not isinstance(entity, str) or not entity or len(entity) > 120:
194
+ fail("task", "an entity name is a string of up to 120 characters", "/task/seed/rows")
195
+ if not isinstance(entity_rows, list) or any(not isinstance(r, dict) for r in entity_rows):
196
+ fail("task", f"seed.rows.{entity} is a list of row objects", f"/task/seed/rows/{entity}")
197
+ total += len(entity_rows)
198
+ if total > SEED_ROWS:
199
+ fail("task", f"a task seed carries at most {SEED_ROWS} rows (got {total})", "/task/seed/rows")
200
+ if _bytes(rows) > SEED_BYTES:
201
+ fail("task", f"a task seed is at most {SEED_BYTES} bytes of json", "/task/seed/rows")
202
+ return {"mode": mode, "rows": rows}
203
+
204
+
205
+ def _parse_grader(grader: Any) -> dict | None:
206
+ if grader is None:
207
+ return None
208
+ if not isinstance(grader, dict):
209
+ fail("task", "grader is a json object with a kind", "/task/grader")
210
+ kind = grader.get("kind")
211
+ if kind == "python":
212
+ if set(grader) - {"kind", "source", "timeoutSeconds"}:
213
+ fail("task", "a python grader is {kind, source, timeoutSeconds?}", "/task/grader")
214
+ source = grader.get("source")
215
+ if not isinstance(source, str) or not source:
216
+ fail("task", "a python grader needs its verify.py source", "/task/grader/source")
217
+ if _bytes(source) > PYTHON_SOURCE_BYTES:
218
+ fail("task", f"a python grader is at most {PYTHON_SOURCE_BYTES} bytes", "/task/grader/source")
219
+ out: dict = {"kind": "python", "source": source}
220
+ timeout = grader.get("timeoutSeconds")
221
+ if timeout is not None:
222
+ if (
223
+ isinstance(timeout, bool)
224
+ or not isinstance(timeout, (int, float))
225
+ or not 0 < timeout <= PYTHON_TIMEOUT_S
226
+ ):
227
+ fail("task", f"timeoutSeconds is a number in (0, {PYTHON_TIMEOUT_S:g}]", "/task/grader/timeoutSeconds")
228
+ out["timeoutSeconds"] = float(timeout)
229
+ return out
230
+ if kind == "assertions":
231
+ if set(grader) - {"kind", "checks"}:
232
+ fail("task", "an assertions grader is {kind, checks}", "/task/grader")
233
+ checks = grader.get("checks")
234
+ if not isinstance(checks, list) or not checks:
235
+ fail("task", "an assertions grader needs at least one check", "/task/grader/checks")
236
+ if len(checks) > CHECKS:
237
+ fail("task", f"an assertions grader has at most {CHECKS} checks", "/task/grader/checks")
238
+ parsed = [_parse_check(check, f"/task/grader/checks/{i}") for i, check in enumerate(checks)]
239
+ # Every check owns one reward key, its id or its position; the keys are
240
+ # distinct as a set, so an explicit `check_2` cannot shadow the third
241
+ # check's positional one. Mirrors WorldTaskAssertionsGraderSchema.
242
+ seen: set[str] = set()
243
+ for i, check in enumerate(parsed):
244
+ key = check_key(check, i)
245
+ if key in seen:
246
+ if "id" in check:
247
+ fail(
248
+ "task",
249
+ f"check id '{key}' is used twice (an id of the form check_<n> is also the positional key of check n)",
250
+ f"/task/grader/checks/{i}/id",
251
+ )
252
+ fail(
253
+ "task",
254
+ f"check {i} has no id and its positional key '{key}' is taken by another check's id",
255
+ f"/task/grader/checks/{i}/id",
256
+ )
257
+ seen.add(key)
258
+ return {"kind": "assertions", "checks": parsed}
259
+ if kind == "rubric":
260
+ return _parse_rubric(grader)
261
+ fail("task", "grader.kind must be assertions, python or rubric", "/task/grader/kind")
262
+ raise AssertionError # fail() never returns
263
+
264
+
265
+ def _parse_rubric(grader: dict) -> dict:
266
+ """An LLM judge's brief. Checked here for shape only: the host stores it
267
+ with the task and never runs it; the platform grades it."""
268
+ if set(grader) - {"kind", "rubric", "criteria", "model"}:
269
+ fail("task", "a rubric grader is {kind, rubric, criteria?, model?}", "/task/grader")
270
+ rubric = grader.get("rubric")
271
+ if not isinstance(rubric, str) or not rubric:
272
+ fail("task", "a rubric grader needs its rubric text", "/task/grader/rubric")
273
+ if _bytes(rubric) > RUBRIC_BYTES:
274
+ fail("task", f"a rubric is at most {RUBRIC_BYTES} bytes", "/task/grader/rubric")
275
+ out: dict = {"kind": "rubric", "rubric": rubric}
276
+ criteria = grader.get("criteria")
277
+ if criteria is not None:
278
+ if not isinstance(criteria, list) or not criteria:
279
+ fail("task", "criteria is a non-empty list", "/task/grader/criteria")
280
+ if len(criteria) > RUBRIC_CRITERIA:
281
+ fail("task", f"a rubric has at most {RUBRIC_CRITERIA} criteria", "/task/grader/criteria")
282
+ parsed = []
283
+ seen: set[str] = set()
284
+ for i, criterion in enumerate(criteria):
285
+ at = f"/task/grader/criteria/{i}"
286
+ if not isinstance(criterion, dict) or set(criterion) - {"id", "description", "weight"}:
287
+ fail("task", "a criterion is {id, description, weight?}", at)
288
+ criterion_id = criterion.get("id")
289
+ if not isinstance(criterion_id, str) or not _CRITERION_ID.match(criterion_id) or len(criterion_id) > 120:
290
+ fail("task", "a criterion id is letters, digits, _ . -", f"{at}/id")
291
+ if criterion_id in seen:
292
+ fail("task", f"criterion id '{criterion_id}' is used twice", f"{at}/id")
293
+ seen.add(criterion_id)
294
+ description = criterion.get("description")
295
+ if not isinstance(description, str) or not description:
296
+ fail("task", "a criterion needs a description", f"{at}/description")
297
+ if _bytes(description) > RUBRIC_CRITERION_BYTES:
298
+ fail("task", f"a criterion is at most {RUBRIC_CRITERION_BYTES} bytes", f"{at}/description")
299
+ item: dict = {"id": criterion_id, "description": description}
300
+ weight = criterion.get("weight")
301
+ if weight is not None:
302
+ if isinstance(weight, bool) or not isinstance(weight, (int, float)) or not weight > 0:
303
+ fail("task", "weight is a positive number", f"{at}/weight")
304
+ item["weight"] = float(weight)
305
+ parsed.append(item)
306
+ out["criteria"] = parsed
307
+ model = grader.get("model")
308
+ if model is not None:
309
+ if not isinstance(model, dict) or set(model) != {"provider", "model"}:
310
+ fail("task", "model is {provider, model}", "/task/grader/model")
311
+ provider, name = model["provider"], model["model"]
312
+ if not isinstance(provider, str) or not 0 < len(provider) <= 120:
313
+ fail("task", "model.provider is a string of up to 120 characters", "/task/grader/model/provider")
314
+ if not isinstance(name, str) or not 0 < len(name) <= 200:
315
+ fail("task", "model.model is a string of up to 200 characters", "/task/grader/model/model")
316
+ out["model"] = {"provider": provider, "model": name}
317
+ return out
318
+
319
+
320
+ def check_key(check: dict, index: int) -> str:
321
+ """The reward key of a check: its id, else its position as ``check_<i>``."""
322
+ return check.get("id") or f"check_{index}"
323
+
324
+
325
+ def _parse_check(check: Any, at: str) -> dict:
326
+ if not isinstance(check, dict):
327
+ fail("task", "a check is a json object", at)
328
+ unknown = set(check) - {"id", "weight", "entity", "where", "assert", "count", "field", "op", "value"}
329
+ if unknown:
330
+ fail("task", f"unknown check field(s): {', '.join(sorted(unknown))}", at)
331
+ out: dict = {}
332
+ check_id = check.get("id")
333
+ if check_id is not None:
334
+ if not isinstance(check_id, str) or not _CHECK_ID.match(check_id) or len(check_id) > 120:
335
+ fail("task", "a check id is letters, digits, _ . -", f"{at}/id")
336
+ out["id"] = check_id
337
+ weight = check.get("weight", 1)
338
+ if isinstance(weight, bool) or not isinstance(weight, (int, float)) or not weight > 0 or weight == float("inf"):
339
+ fail("task", "weight is a positive number", f"{at}/weight")
340
+ out["weight"] = float(weight)
341
+ entity = check.get("entity")
342
+ if not isinstance(entity, str) or not entity or len(entity) > 120:
343
+ fail("task", "a check names an entity", f"{at}/entity")
344
+ out["entity"] = entity
345
+ where = check.get("where")
346
+ if where is not None:
347
+ if not isinstance(where, dict) or any(
348
+ not isinstance(k, str) or not k or not _scalar(v) for k, v in where.items()
349
+ ):
350
+ fail("task", "where is {field: scalar, ...}", f"{at}/where")
351
+ out["where"] = where
352
+ assertion = check.get("assert", "exists")
353
+ if assertion not in ASSERTIONS:
354
+ fail("task", f"assert must be one of {', '.join(ASSERTIONS)}", f"{at}/assert")
355
+ out["assert"] = assertion
356
+ count = check.get("count")
357
+ if assertion == "count":
358
+ if not isinstance(count, dict) or set(count) - {"eq", "gte", "lte"} or not count:
359
+ fail("task", "assert: count needs a count bound {eq|gte|lte: n}", f"{at}/count")
360
+ for key, bound in count.items():
361
+ if isinstance(bound, bool) or not isinstance(bound, int) or bound < 0:
362
+ fail("task", f"count.{key} is a non-negative integer", f"{at}/count/{key}")
363
+ out["count"] = count
364
+ elif count is not None:
365
+ fail("task", "count only goes with assert: count", f"{at}/count")
366
+ field = check.get("field")
367
+ op = check.get("op", "eq")
368
+ value = check.get("value")
369
+ if assertion in ("all", "any"):
370
+ if not isinstance(field, str) or not field:
371
+ fail("task", f"assert: {assertion} needs a field to test", f"{at}/field")
372
+ if op not in FIELD_OPS:
373
+ fail("task", f"op must be one of {', '.join(FIELD_OPS)}", f"{at}/op")
374
+ if "value" not in check:
375
+ fail("task", f"assert: {assertion} needs a value to compare with", f"{at}/value")
376
+ if op == "in":
377
+ if not isinstance(value, list) or any(not _scalar(v) for v in value):
378
+ fail("task", "op: in compares against a list of scalars", f"{at}/value")
379
+ elif not _scalar(value):
380
+ fail("task", f"op: {op} compares against one scalar", f"{at}/value")
381
+ out["field"], out["op"], out["value"] = field, op, value
382
+ elif field is not None or "value" in check or "op" in check:
383
+ fail("task", "field/op/value only go with assert: all or any", f"{at}/field")
384
+ return out
385
+
386
+
387
+ # --------------------------------------------------------------------------- #
388
+ # On disk
389
+ # --------------------------------------------------------------------------- #
390
+
391
+
392
+ def write_task_dir(spec: TaskSpec, session_dir: Path) -> Path:
393
+ """The task under the session: ``task.json`` (the whole spec),
394
+ ``instruction.md``, and for a python grader ``checks/verify.py`` where
395
+ the bundled layout has it, so the one verifier path runs both."""
396
+ root = Path(session_dir) / TASK_DIR
397
+ root.mkdir(parents=True, exist_ok=True)
398
+ (root / TASK_FILE).write_text(json.dumps(spec.raw, indent=2, sort_keys=True) + "\n", encoding="utf-8")
399
+ (root / "instruction.md").write_text(spec.instruction, encoding="utf-8")
400
+ if spec.grader_kind == "python":
401
+ checks = root / "checks"
402
+ checks.mkdir(exist_ok=True)
403
+ (checks / "verify.py").write_text(spec.grader["source"], encoding="utf-8")
404
+ return root
405
+
406
+
407
+ def read_task_dir(session_dir: Path) -> TaskSpec | None:
408
+ """The task a session directory holds, or None for a bundled-task session."""
409
+ path = Path(session_dir) / TASK_DIR / TASK_FILE
410
+ if not path.is_file():
411
+ return None
412
+ return parse_task_spec(json.loads(path.read_text(encoding="utf-8")))
413
+
414
+
415
+ # --------------------------------------------------------------------------- #
416
+ # The assertions grader
417
+ # --------------------------------------------------------------------------- #
418
+
419
+
420
+ def _lookup(row: Any, path: str) -> tuple[bool, Any]:
421
+ """``(found, value)`` of a dotted path into a row."""
422
+ current = row
423
+ for part in path.split("."):
424
+ if not isinstance(current, dict) or part not in current:
425
+ return False, None
426
+ current = current[part]
427
+ return True, current
428
+
429
+
430
+ def _same(actual: Any, expected: Any) -> bool:
431
+ """JSON equality: ``true`` is not ``1``, as it is not in the schema."""
432
+ if isinstance(actual, bool) or isinstance(expected, bool):
433
+ return isinstance(actual, bool) and isinstance(expected, bool) and actual is expected
434
+ return actual == expected
435
+
436
+
437
+ def _matches_where(row: dict, where: dict | None) -> bool:
438
+ for path, expected in (where or {}).items():
439
+ found, actual = _lookup(row, path)
440
+ if not found or not _same(actual, expected):
441
+ return False
442
+ return True
443
+
444
+
445
+ def _ordered(actual: Any, expected: Any) -> bool:
446
+ """Whether the two compare with ``<``: both numbers (not bools) or both strings."""
447
+ numbers = (int, float)
448
+ if isinstance(actual, bool) or isinstance(expected, bool):
449
+ return False
450
+ if isinstance(actual, numbers) and isinstance(expected, numbers):
451
+ return True
452
+ return isinstance(actual, str) and isinstance(expected, str)
453
+
454
+
455
+ def _predicate(row: dict, field: str, op: str, expected: Any) -> bool:
456
+ found, actual = _lookup(row, field)
457
+ if op == "eq":
458
+ return found and _same(actual, expected)
459
+ if op == "neq":
460
+ return not found or not _same(actual, expected)
461
+ if op == "in":
462
+ return found and any(_same(actual, option) for option in expected)
463
+ if op == "contains":
464
+ if not found:
465
+ return False
466
+ if isinstance(actual, str):
467
+ return isinstance(expected, str) and expected in actual
468
+ if isinstance(actual, list):
469
+ return any(_same(item, expected) for item in actual)
470
+ return False
471
+ if not found or not _ordered(actual, expected):
472
+ return False
473
+ if op == "gt":
474
+ return actual > expected
475
+ if op == "gte":
476
+ return actual >= expected
477
+ if op == "lt":
478
+ return actual < expected
479
+ return actual <= expected # lte
480
+
481
+
482
+ def _rows_of(state: dict, entity: str) -> list[dict]:
483
+ """The rows of one entity from a state dump: ``{entity: {key: row}}`` (the
484
+ verifier's shape) or ``{entity: [row, ...]}``."""
485
+ held = state.get(entity)
486
+ if isinstance(held, dict):
487
+ return [row for row in held.values() if isinstance(row, dict)]
488
+ if isinstance(held, list):
489
+ return [row for row in held if isinstance(row, dict)]
490
+ return []
491
+
492
+
493
+ def evaluate_check(check: dict, state: dict) -> bool:
494
+ selected = [row for row in _rows_of(state, check["entity"]) if _matches_where(row, check.get("where"))]
495
+ assertion = check["assert"]
496
+ if assertion == "exists":
497
+ return bool(selected)
498
+ if assertion == "absent":
499
+ return not selected
500
+ if assertion == "count":
501
+ bound = check["count"]
502
+ n = len(selected)
503
+ return all(
504
+ (key != "eq" or n == value) and (key != "gte" or n >= value) and (key != "lte" or n <= value)
505
+ for key, value in bound.items()
506
+ )
507
+ tests = (_predicate(row, check["field"], check["op"], check["value"]) for row in selected)
508
+ if assertion == "all":
509
+ return bool(selected) and all(tests)
510
+ return any(tests) # any
511
+
512
+
513
+ def evaluate_assertions(checks: list[dict], state: dict) -> dict:
514
+ """The grade of an assertions grader over a state dump: the weighted share
515
+ of checks that passed as ``reward``, each check's 0/1 under its id (or its
516
+ position) in ``rewards``. The runner's shape, as ``verify.py`` prints it."""
517
+ rewards: dict[str, float] = {}
518
+ total = 0.0
519
+ passed = 0.0
520
+ for i, check in enumerate(checks):
521
+ ok = evaluate_check(check, state)
522
+ rewards[check_key(check, i)] = 1.0 if ok else 0.0
523
+ total += check["weight"]
524
+ passed += check["weight"] if ok else 0.0
525
+ return {"reward": (passed / total) if total else None, "rewards": rewards}