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,901 @@
1
+ # Copyright (c) Gateway. All rights reserved.
2
+
3
+ """Orchestrator ABC and DockerOrchestrator implementation.
4
+
5
+ The ``Orchestrator`` base class defines the contract for compute lifecycle
6
+ management. Each backend (Docker, Nomad, KubeRay) implements this contract.
7
+ ``ComputeHandle`` is an opaque token that the orchestrator returns from
8
+ ``start()`` and accepts back in every subsequent call.
9
+
10
+ Usage (via AlgorithmContext)::
11
+
12
+ orch = DockerOrchestrator()
13
+ handle = orch.start(
14
+ run_id="run-abc",
15
+ image="my-agent:v1",
16
+ store_url="http://host.docker.internal:9876",
17
+ count=3,
18
+ lifecycle=ContainerLifecycle.PERSISTENT,
19
+ )
20
+ orch.wait_ready(handle, timeout=120.0)
21
+ # ... algorithm enqueues rollouts, N agents process them ...
22
+ orch.stop(handle)
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import logging
28
+ import time as _time
29
+ from abc import ABC, abstractmethod
30
+ from dataclasses import dataclass, field
31
+ from enum import Enum
32
+ from typing import Any, Dict, List, Optional
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ # ═══════════════════════════════════════════════════════════════════════
38
+ # Enums & data classes
39
+ # ═══════════════════════════════════════════════════════════════════════
40
+
41
+
42
+ class ComputeStatus(str, Enum):
43
+ """Lifecycle state of orchestrated compute."""
44
+
45
+ PENDING = "pending"
46
+ RUNNING = "running"
47
+ SUCCEEDED = "succeeded"
48
+ FAILED = "failed"
49
+ UNKNOWN = "unknown"
50
+
51
+
52
+ class ContainerLifecycle(str, Enum):
53
+ """How agent containers are recycled during a run.
54
+
55
+ PERSISTENT: containers stay alive for the entire run duration.
56
+ Best for stateless agents -- avoids startup overhead.
57
+ EPHEMERAL: containers are replaced after each rollout batch.
58
+ Guarantees clean state for stateful agents (e.g. CRM sim).
59
+ """
60
+
61
+ PERSISTENT = "persistent"
62
+ EPHEMERAL = "ephemeral"
63
+
64
+
65
+ @dataclass
66
+ class ComputeHandle:
67
+ """Opaque handle returned by start(), passed back to stop()/status().
68
+
69
+ Each orchestrator subclass stores whatever it needs here.
70
+ Docker stores container IDs; Nomad stores a job ID; KubeRay stores a
71
+ RayJob name; etc.
72
+ """
73
+
74
+ run_id: str
75
+ backend: str # "docker", "nomad", "kuberay", ...
76
+ identifiers: Dict[str, str] = field(default_factory=dict)
77
+ # e.g. {"agent-0": "abc123", "agent-1": "def456"} (Docker)
78
+ # e.g. {"job_id": "gateway-run-42"} (Nomad)
79
+ # e.g. {"rayjob": "gateway-verl-run-42"} (KubeRay)
80
+ metadata: Dict[str, Any] = field(default_factory=dict)
81
+
82
+
83
+ # ═══════════════════════════════════════════════════════════════════════
84
+ # Orchestrator ABC
85
+ # ═══════════════════════════════════════════════════════════════════════
86
+
87
+
88
+ class Orchestrator(ABC):
89
+ """Base class for compute orchestrators.
90
+
91
+ An orchestrator manages the lifecycle of compute resources for a
92
+ platform run. Subclasses handle the specifics of Docker, Nomad,
93
+ KubeRay, etc.
94
+
95
+ Lifecycle (called by AlgorithmContext)::
96
+
97
+ handle = orch.start(...) # __aenter__
98
+ orch.wait_ready(handle) # block until healthy
99
+ # ... algorithm runs ...
100
+ orch.status(handle) # health checks during run
101
+ orch.stop(handle) # __aexit__
102
+ """
103
+
104
+ @property
105
+ @abstractmethod
106
+ def available(self) -> bool:
107
+ """Can this orchestrator function in the current environment?"""
108
+ ...
109
+
110
+ @property
111
+ @abstractmethod
112
+ def backend_name(self) -> str:
113
+ """Short identifier: 'docker', 'nomad', 'kuberay', etc."""
114
+ ...
115
+
116
+ @abstractmethod
117
+ def start(
118
+ self,
119
+ *,
120
+ run_id: str,
121
+ image: str,
122
+ store_url: str,
123
+ count: int = 1,
124
+ lifecycle: ContainerLifecycle = ContainerLifecycle.PERSISTENT,
125
+ env: Optional[Dict[str, str]] = None,
126
+ labels: Optional[Dict[str, str]] = None,
127
+ runner_config: Optional[Dict[str, Any]] = None,
128
+ resource_limits: Optional[Dict[str, Any]] = None,
129
+ # --- reactive scaling (optional) ---
130
+ store: Optional[Any] = None,
131
+ n_runners_per_unit: int = 1,
132
+ max_units: int = 20,
133
+ # --- extended fields (used by KubeRay, ignored by Docker/Nomad) ---
134
+ trainer_resources: Optional[Dict[str, Any]] = None,
135
+ services: Optional[Dict[str, Any]] = None,
136
+ algorithm_config: Optional[Dict[str, Any]] = None,
137
+ ) -> ComputeHandle:
138
+ """Provision compute for a run.
139
+
140
+ For Docker: starts ``count`` agent containers
141
+ For Nomad: submits a job spec with task group ``count``
142
+ For KubeRay: creates a RayJob CR (head + workers)
143
+
144
+ If ``store`` is provided, the orchestrator registers a listener
145
+ for reactive scaling (start containers when rollouts are enqueued)
146
+ and ephemeral recycling (replace containers after rollout completion
147
+ when lifecycle is EPHEMERAL).
148
+
149
+ Args:
150
+ run_id: GatewayRun ID.
151
+ image: Container image to run.
152
+ store_url: HTTP URL of the worker's LightningStoreServer.
153
+ count: Number of agent instances to start immediately.
154
+ Use 0 for fully reactive (containers start on demand).
155
+ lifecycle: Whether containers persist or are replaced per batch.
156
+ env: Additional environment variables.
157
+ labels: Metadata labels for the containers.
158
+ runner_config: Runner lifecycle config from agent manifest.
159
+ resource_limits: Container resource limits from agent manifest.
160
+ store: LightningStore instance. If provided, enables reactive
161
+ scaling via store listener hooks.
162
+ n_runners_per_unit: Rollouts a single compute unit handles
163
+ concurrently (used for capacity calculation).
164
+ max_units: Hard cap on number of compute units.
165
+ trainer_resources: GPU/memory for trainer (KubeRay only).
166
+ services: Sidecar services (KubeRay only).
167
+ algorithm_config: Algorithm-specific config (KubeRay only).
168
+
169
+ Returns:
170
+ ComputeHandle with identifiers for the provisioned compute.
171
+
172
+ Raises:
173
+ RuntimeError: If compute cannot be provisioned.
174
+ """
175
+ ...
176
+
177
+ @abstractmethod
178
+ def stop(self, handle: ComputeHandle, timeout: int = 30) -> None:
179
+ """Tear down all compute for a run.
180
+
181
+ Must be idempotent -- safe to call multiple times or after
182
+ compute has already exited.
183
+ """
184
+ ...
185
+
186
+ @abstractmethod
187
+ def status(self, handle: ComputeHandle) -> ComputeStatus:
188
+ """Check aggregate health of the compute for a run.
189
+
190
+ Returns RUNNING if all members are healthy, FAILED if any have
191
+ crashed, PENDING if still starting up.
192
+ """
193
+ ...
194
+
195
+ @abstractmethod
196
+ def wait_ready(self, handle: ComputeHandle, timeout: float = 120.0) -> None:
197
+ """Block until all provisioned compute is healthy.
198
+
199
+ Docker: poll container.status until all report 'running'.
200
+ Nomad: poll deployment health.
201
+ KubeRay: poll RayJob status.
202
+
203
+ Raises:
204
+ RuntimeError: If timeout exceeded before all members are ready.
205
+ """
206
+ ...
207
+
208
+ @abstractmethod
209
+ def scale(self, handle: ComputeHandle, count: int) -> None:
210
+ """Adjust the number of running instances.
211
+
212
+ Docker: start/stop delta containers, update handle.identifiers.
213
+ Nomad: update job count, scheduler reconciles.
214
+
215
+ Args:
216
+ handle: ComputeHandle to scale.
217
+ count: New desired count.
218
+ """
219
+ ...
220
+
221
+ @abstractmethod
222
+ def replace_member(
223
+ self,
224
+ handle: ComputeHandle,
225
+ member_key: str,
226
+ ) -> Optional[str]:
227
+ """Replace one member of the compute group (ephemeral mode).
228
+
229
+ Docker: stop container, start fresh one, update handle.
230
+ Nomad: ``nomad alloc restart``, or no-op (scheduler handles).
231
+
232
+ Args:
233
+ handle: ComputeHandle containing the member.
234
+ member_key: Key in handle.identifiers (e.g. "agent-0").
235
+
236
+ Returns:
237
+ New container/alloc ID, or None if replacement failed.
238
+ """
239
+ ...
240
+
241
+ def members(self, handle: ComputeHandle) -> List[Dict[str, Any]]:
242
+ """List individual members of the compute group.
243
+
244
+ Optional -- returns empty list by default.
245
+ """
246
+ return []
247
+
248
+ def get_logs(self, handle: ComputeHandle, tail: int = 100) -> str:
249
+ """Fetch recent logs. Optional -- returns empty string by default."""
250
+ return ""
251
+
252
+
253
+ # ═══════════════════════════════════════════════════════════════════════
254
+ # DockerOrchestrator
255
+ # ═══════════════════════════════════════════════════════════════════════
256
+
257
+
258
+ class DockerOrchestrator(Orchestrator):
259
+ """Manages agent containers via Docker SDK.
260
+
261
+ For local development, uses the Docker socket to start/stop containers.
262
+ Gracefully degrades if Docker is not available (e.g., in CI without Docker).
263
+ """
264
+
265
+ def __init__(self, docker_host: Optional[str] = None):
266
+ """
267
+ Args:
268
+ docker_host: Docker daemon URL. Defaults to unix:///var/run/docker.sock.
269
+ """
270
+ self._client = None
271
+ self._stop_timeouts: Dict[str, int] = {}
272
+ try:
273
+ import docker
274
+
275
+ self._client = docker.from_env() if not docker_host else docker.DockerClient(base_url=docker_host)
276
+ # Quick connectivity check
277
+ self._client.ping()
278
+ logger.info("DockerOrchestrator connected to Docker daemon")
279
+ except ImportError:
280
+ logger.warning("Docker SDK not installed. Install with: pip install docker")
281
+ except Exception as e:
282
+ logger.warning(f"Could not connect to Docker daemon: {e}")
283
+ self._client = None
284
+
285
+ # ── Orchestrator properties ─────────────────────────────────────
286
+
287
+ @property
288
+ def available(self) -> bool:
289
+ return self._client is not None
290
+
291
+ @property
292
+ def backend_name(self) -> str:
293
+ return "docker"
294
+
295
+ # ── Private helpers ─────────────────────────────────────────────
296
+
297
+ def _build_container_env(
298
+ self,
299
+ *,
300
+ run_id: str,
301
+ store_url: str,
302
+ env: Optional[Dict[str, str]],
303
+ runner_config: Optional[Dict[str, Any]],
304
+ ) -> Dict[str, str]:
305
+ """Build the environment dict for an agent container."""
306
+ import os as _os
307
+
308
+ container_env: Dict[str, str] = {
309
+ "GATEWAY_RUN_ID": run_id,
310
+ "GATEWAY_STORE_URL": store_url,
311
+ }
312
+
313
+ # Forward Gateway tracing config from worker env unconditionally
314
+ for _key in (
315
+ "GATEWAY_HOST",
316
+ "GATEWAY_PUBLIC_KEY",
317
+ "GATEWAY_SECRET_KEY",
318
+ ):
319
+ _val = _os.environ.get(_key)
320
+ if _val:
321
+ if _key == "GATEWAY_HOST" and ("localhost" in _val or "127.0.0.1" in _val):
322
+ import platform as _plat
323
+
324
+ _access = "host.docker.internal" if _plat.system() == "Darwin" else _val
325
+ _val = _val.replace("localhost", _access).replace("127.0.0.1", _access)
326
+ container_env[_key] = _val
327
+
328
+ # Fallback: forward LLM API keys from worker env ONLY if not already
329
+ # provided via ProjectSecret (caller-provided env takes precedence).
330
+ for _key in (
331
+ "ANTHROPIC_API_KEY",
332
+ "OPENAI_API_KEY",
333
+ "AZURE_API_KEY",
334
+ "GOOGLE_API_KEY",
335
+ "COHERE_API_KEY",
336
+ ):
337
+ if _key not in container_env:
338
+ _val = _os.environ.get(_key)
339
+ if _val:
340
+ container_env[_key] = _val
341
+
342
+ # Merge caller-provided env (takes precedence)
343
+ if env:
344
+ container_env.update(env)
345
+
346
+ # Inject runner config as env vars (read by LitAgentRunner)
347
+ if runner_config:
348
+ if runner_config.get("max_rollouts") is not None:
349
+ container_env["GATEWAY_MAX_ROLLOUTS"] = str(runner_config["max_rollouts"])
350
+ if runner_config.get("poll_interval") is not None:
351
+ container_env["GATEWAY_POLL_INTERVAL"] = str(runner_config["poll_interval"])
352
+ if runner_config.get("n_runners") is not None:
353
+ container_env["GATEWAY_N_RUNNERS"] = str(runner_config["n_runners"])
354
+
355
+ return container_env
356
+
357
+ def _detect_network(self) -> str:
358
+ """Auto-detect the Docker network to attach agent containers to."""
359
+ if not self._client:
360
+ return "bridge"
361
+ try:
362
+ networks = [n.name for n in self._client.networks.list()]
363
+ for candidate in (
364
+ "langfuse-network",
365
+ "gateway_default",
366
+ "gateway_default",
367
+ ):
368
+ if candidate in networks:
369
+ logger.info(f"Auto-detected Docker network: {candidate}")
370
+ return candidate
371
+ except Exception:
372
+ pass
373
+ return "bridge"
374
+
375
+ def _build_docker_kwargs(
376
+ self,
377
+ *,
378
+ container_env: Dict[str, str],
379
+ container_labels: Dict[str, str],
380
+ network: str,
381
+ resource_limits: Optional[Dict[str, Any]],
382
+ name: Optional[str] = None,
383
+ ) -> Dict[str, Any]:
384
+ """Build the kwargs dict for ``docker.containers.run()``."""
385
+ docker_kwargs: Dict[str, Any] = {
386
+ "detach": True,
387
+ "network": network,
388
+ "environment": container_env,
389
+ "labels": container_labels,
390
+ "auto_remove": False,
391
+ }
392
+
393
+ if name:
394
+ docker_kwargs["name"] = name
395
+
396
+ if resource_limits:
397
+ if resource_limits.get("memory"):
398
+ docker_kwargs["mem_limit"] = resource_limits["memory"]
399
+ if resource_limits.get("cpu"):
400
+ docker_kwargs["nano_cpus"] = int(float(resource_limits["cpu"]) * 1e9)
401
+ gpu = resource_limits.get("gpu")
402
+ if gpu and gpu is not False:
403
+ try:
404
+ import docker as _docker
405
+
406
+ gpu_count = -1 if gpu is True else int(gpu)
407
+ docker_kwargs["device_requests"] = [
408
+ _docker.types.DeviceRequest(count=gpu_count, capabilities=[["gpu"]])
409
+ ]
410
+ except (ImportError, Exception) as e:
411
+ logger.warning(f"GPU device request failed: {e}")
412
+
413
+ return docker_kwargs
414
+
415
+ def _start_one_container(
416
+ self,
417
+ *,
418
+ image: str,
419
+ run_id: str,
420
+ index: int,
421
+ container_env: Dict[str, str],
422
+ network: str,
423
+ labels: Optional[Dict[str, str]],
424
+ resource_limits: Optional[Dict[str, Any]],
425
+ ) -> Optional[str]:
426
+ """Start a single agent container. Returns container ID or None."""
427
+ if not self._client:
428
+ return None
429
+
430
+ name = f"gateway-{run_id[:8]}-agent-{index}"
431
+ container_labels = {
432
+ "gateway.run_id": run_id,
433
+ "gateway.component": "agent",
434
+ "gateway.agent_index": str(index),
435
+ **(labels or {}),
436
+ }
437
+
438
+ docker_kwargs = self._build_docker_kwargs(
439
+ container_env=container_env,
440
+ container_labels=container_labels,
441
+ network=network,
442
+ resource_limits=resource_limits,
443
+ name=name,
444
+ )
445
+
446
+ try:
447
+ container = self._client.containers.run(image, **docker_kwargs)
448
+ logger.info(f"Started container {name} ({container.short_id}) " f"from {image} for run {run_id}")
449
+ return container.id
450
+ except Exception as e:
451
+ logger.error(f"Failed to start container {name}: {e}")
452
+ return None
453
+
454
+ # ── Orchestrator interface ──────────────────────────────────────
455
+
456
+ def start(
457
+ self,
458
+ *,
459
+ run_id: str,
460
+ image: str,
461
+ store_url: str,
462
+ count: int = 0,
463
+ lifecycle: ContainerLifecycle = ContainerLifecycle.PERSISTENT,
464
+ env: Optional[Dict[str, str]] = None,
465
+ labels: Optional[Dict[str, str]] = None,
466
+ runner_config: Optional[Dict[str, Any]] = None,
467
+ resource_limits: Optional[Dict[str, Any]] = None,
468
+ store: Optional[Any] = None,
469
+ n_runners_per_unit: int = 1,
470
+ max_units: int = 20,
471
+ trainer_resources: Optional[Dict[str, Any]] = None,
472
+ services: Optional[Dict[str, Any]] = None,
473
+ algorithm_config: Optional[Dict[str, Any]] = None,
474
+ ) -> ComputeHandle:
475
+ if not self._client:
476
+ raise RuntimeError("Docker not available, cannot start containers")
477
+
478
+ container_env = self._build_container_env(
479
+ run_id=run_id,
480
+ store_url=store_url,
481
+ env=env,
482
+ runner_config=runner_config,
483
+ )
484
+ network = self._detect_network()
485
+
486
+ # Store stop timeout for use in stop()
487
+ stop_timeout = (resource_limits or {}).get("stop_timeout")
488
+ if stop_timeout:
489
+ self._stop_timeouts[run_id] = int(stop_timeout)
490
+
491
+ identifiers: Dict[str, str] = {}
492
+ for i in range(count):
493
+ cid = self._start_one_container(
494
+ image=image,
495
+ run_id=run_id,
496
+ index=i,
497
+ container_env=container_env,
498
+ network=network,
499
+ labels=labels,
500
+ resource_limits=resource_limits,
501
+ )
502
+ if cid is None:
503
+ # Clean up any containers that were already started
504
+ for key, started_id in identifiers.items():
505
+ self._stop_container(started_id)
506
+ raise RuntimeError(f"Failed to start container agent-{i} " f"({i}/{count} started successfully)")
507
+ identifiers[f"agent-{i}"] = cid
508
+
509
+ if count > 0:
510
+ logger.info(f"Started {count} agent container(s) for run {run_id} " f"(lifecycle={lifecycle.value})")
511
+
512
+ handle = ComputeHandle(
513
+ run_id=run_id,
514
+ backend=self.backend_name,
515
+ identifiers=identifiers,
516
+ metadata={
517
+ "lifecycle": lifecycle.value,
518
+ "desired_count": count,
519
+ "image": image,
520
+ "store_url": store_url,
521
+ "network": network,
522
+ "env": env or {},
523
+ "runner_config": runner_config or {},
524
+ "resource_limits": resource_limits or {},
525
+ "labels": labels or {},
526
+ },
527
+ )
528
+
529
+ # If a store is provided, set up reactive scaling automatically.
530
+ if store is not None:
531
+ self._setup_reactive_listener(
532
+ handle,
533
+ store,
534
+ n_runners_per_unit,
535
+ max_units,
536
+ )
537
+
538
+ return handle
539
+
540
+ def stop(self, handle: ComputeHandle, timeout: int = 30) -> None:
541
+ if not self._client:
542
+ return
543
+
544
+ # Remove reactive listener first so no more events fire.
545
+ self._teardown_reactive_listener(handle)
546
+
547
+ stopped = 0
548
+ for key, cid in list(handle.identifiers.items()):
549
+ self._stop_container(cid)
550
+ stopped += 1
551
+
552
+ if stopped:
553
+ logger.info(f"Killed and removed {stopped} container(s) for " f"run {handle.run_id}")
554
+
555
+ def status(self, handle: ComputeHandle) -> ComputeStatus:
556
+ if not self._client:
557
+ return ComputeStatus.UNKNOWN
558
+
559
+ statuses = []
560
+ for key, cid in handle.identifiers.items():
561
+ s = self._get_container_status(cid)
562
+ statuses.append(s)
563
+
564
+ if not statuses:
565
+ return ComputeStatus.UNKNOWN
566
+ if all(s == "running" for s in statuses):
567
+ return ComputeStatus.RUNNING
568
+ if any(s in ("exited", "dead") for s in statuses):
569
+ return ComputeStatus.FAILED
570
+ if all(s in ("created", "restarting") for s in statuses):
571
+ return ComputeStatus.PENDING
572
+ # Mixed state (some running, some starting) -- still consider running
573
+ if any(s == "running" for s in statuses):
574
+ return ComputeStatus.RUNNING
575
+ return ComputeStatus.UNKNOWN
576
+
577
+ def wait_ready(
578
+ self,
579
+ handle: ComputeHandle,
580
+ timeout: float = 120.0,
581
+ ) -> None:
582
+ if not self._client:
583
+ raise RuntimeError("Docker not available")
584
+
585
+ deadline = _time.monotonic() + timeout
586
+ poll_interval = 2.0
587
+ target_count = len(handle.identifiers)
588
+
589
+ while _time.monotonic() < deadline:
590
+ ready = 0
591
+ for key, cid in handle.identifiers.items():
592
+ s = self._get_container_status(cid)
593
+ if s == "running":
594
+ ready += 1
595
+ elif s in ("exited", "dead"):
596
+ raise RuntimeError(f"Container {key} ({cid[:12]}) exited before " f"becoming ready")
597
+
598
+ if ready >= target_count:
599
+ logger.info(f"All {target_count} container(s) ready for " f"run {handle.run_id}")
600
+ return
601
+
602
+ logger.debug(
603
+ f"Waiting for containers: {ready}/{target_count} ready "
604
+ f"({timeout - (_time.monotonic() - (deadline - timeout)):.0f}s remaining)"
605
+ )
606
+ _time.sleep(poll_interval)
607
+
608
+ raise RuntimeError(
609
+ f"Timeout ({timeout}s) waiting for {target_count} container(s) " f"to become ready for run {handle.run_id}"
610
+ )
611
+
612
+ def scale(self, handle: ComputeHandle, count: int) -> None:
613
+ if not self._client:
614
+ raise RuntimeError("Docker not available")
615
+
616
+ current = len(handle.identifiers)
617
+ if count == current:
618
+ return
619
+
620
+ meta = handle.metadata
621
+
622
+ if count > current:
623
+ # Scale up: start additional containers
624
+ container_env = self._build_container_env(
625
+ run_id=handle.run_id,
626
+ store_url=meta["store_url"],
627
+ env=meta.get("env"),
628
+ runner_config=meta.get("runner_config"),
629
+ )
630
+ network = meta.get("network", self._detect_network())
631
+
632
+ for i in range(current, count):
633
+ cid = self._start_one_container(
634
+ image=meta["image"],
635
+ run_id=handle.run_id,
636
+ index=i,
637
+ container_env=container_env,
638
+ network=network,
639
+ labels=meta.get("labels"),
640
+ resource_limits=meta.get("resource_limits"),
641
+ )
642
+ if cid is None:
643
+ logger.error(f"Failed to scale up: could not start agent-{i}")
644
+ break
645
+ handle.identifiers[f"agent-{i}"] = cid
646
+
647
+ logger.info(f"Scaled up from {current} to {len(handle.identifiers)} " f"for run {handle.run_id}")
648
+ else:
649
+ # Scale down: stop highest-indexed containers
650
+ keys_sorted = sorted(
651
+ handle.identifiers.keys(),
652
+ key=lambda k: int(k.split("-")[-1]),
653
+ reverse=True,
654
+ )
655
+ to_remove = keys_sorted[: current - count]
656
+ for key in to_remove:
657
+ cid = handle.identifiers.pop(key)
658
+ self._stop_container(cid)
659
+
660
+ logger.info(f"Scaled down from {current} to {len(handle.identifiers)} " f"for run {handle.run_id}")
661
+
662
+ handle.metadata["desired_count"] = count
663
+
664
+ def replace_member(
665
+ self,
666
+ handle: ComputeHandle,
667
+ member_key: str,
668
+ ) -> Optional[str]:
669
+ if not self._client:
670
+ return None
671
+
672
+ old_cid = handle.identifiers.get(member_key)
673
+ if old_cid:
674
+ self._stop_container(old_cid)
675
+
676
+ meta = handle.metadata
677
+ index = int(member_key.split("-")[-1])
678
+
679
+ container_env = self._build_container_env(
680
+ run_id=handle.run_id,
681
+ store_url=meta["store_url"],
682
+ env=meta.get("env"),
683
+ runner_config=meta.get("runner_config"),
684
+ )
685
+ network = meta.get("network", self._detect_network())
686
+
687
+ new_cid = self._start_one_container(
688
+ image=meta["image"],
689
+ run_id=handle.run_id,
690
+ index=index,
691
+ container_env=container_env,
692
+ network=network,
693
+ labels=meta.get("labels"),
694
+ resource_limits=meta.get("resource_limits"),
695
+ )
696
+
697
+ if new_cid:
698
+ handle.identifiers[member_key] = new_cid
699
+ logger.info(
700
+ f"Replaced {member_key} ({old_cid[:12] if old_cid else '?'}) "
701
+ f"with {new_cid[:12]} for run {handle.run_id}"
702
+ )
703
+ else:
704
+ logger.error(f"Failed to replace {member_key} for run {handle.run_id}")
705
+
706
+ return new_cid
707
+
708
+ def members(self, handle: ComputeHandle) -> List[Dict[str, Any]]:
709
+ if not self._client:
710
+ return []
711
+
712
+ result = []
713
+ for key, cid in sorted(handle.identifiers.items()):
714
+ index = int(key.split("-")[-1])
715
+ status = self._get_container_status(cid) or "unknown"
716
+ result.append(
717
+ {
718
+ "key": key,
719
+ "id": cid,
720
+ "short_id": cid[:12],
721
+ "index": index,
722
+ "status": status,
723
+ "name": f"gateway-{handle.run_id[:8]}-agent-{index}",
724
+ }
725
+ )
726
+ return result
727
+
728
+ def get_logs(self, handle: ComputeHandle, tail: int = 100) -> str:
729
+ if not self._client:
730
+ return ""
731
+
732
+ parts: List[str] = []
733
+ for key, cid in sorted(handle.identifiers.items()):
734
+ try:
735
+ container = self._client.containers.get(cid)
736
+ logs = container.logs(tail=tail, timestamps=False)
737
+ if isinstance(logs, bytes):
738
+ logs = logs.decode("utf-8", errors="replace")
739
+ parts.append(f"=== {key} ({cid[:12]}) ===\n{logs}")
740
+ except Exception as e:
741
+ parts.append(f"=== {key} ({cid[:12]}) === [error: {e}]")
742
+ return "\n".join(parts)
743
+
744
+ # ── Low-level container helpers ─────────────────────────────────
745
+
746
+ def _stop_container(self, container_id: str) -> None:
747
+ """Kill and remove a container immediately."""
748
+ if not self._client:
749
+ return
750
+ try:
751
+ container = self._client.containers.get(container_id)
752
+ try:
753
+ container.kill()
754
+ except Exception:
755
+ pass # Already stopped / exited
756
+ container.remove(force=True)
757
+ logger.debug(f"Killed and removed container {container_id[:12]}")
758
+ except Exception as e:
759
+ logger.warning(f"Could not kill/remove container {container_id[:12]}: {e}")
760
+
761
+ def _get_container_status(self, container_id: str) -> Optional[str]:
762
+ """Get container status (running, exited, etc.)."""
763
+ if not self._client:
764
+ return None
765
+ try:
766
+ container = self._client.containers.get(container_id)
767
+ return container.status
768
+ except Exception as e:
769
+ logger.debug(f"Could not get container status: {e}")
770
+ return None
771
+
772
+ # ── Legacy helpers (used by stop_all_for_run fallback) ──────────
773
+
774
+ def list_run_containers(self, run_id: str) -> List[Dict[str, Any]]:
775
+ """List all containers associated with a run (by label)."""
776
+ if not self._client:
777
+ return []
778
+ try:
779
+ containers = self._client.containers.list(
780
+ all=True,
781
+ filters={"label": f"gateway.run_id={run_id}"},
782
+ )
783
+ return [
784
+ {
785
+ "id": c.id,
786
+ "short_id": c.short_id,
787
+ "name": c.name,
788
+ "status": c.status,
789
+ "image": str(c.image),
790
+ }
791
+ for c in containers
792
+ ]
793
+ except Exception as e:
794
+ logger.debug(f"Could not list containers for run {run_id}: {e}")
795
+ return []
796
+
797
+ def stop_all_for_run(self, run_id: str) -> int:
798
+ """Kill all containers associated with a run (label-based fallback)."""
799
+ containers = self.list_run_containers(run_id)
800
+ stopped = 0
801
+ for c in containers:
802
+ self._stop_container(c["id"])
803
+ stopped += 1
804
+ return stopped
805
+
806
+ # ── Reactive scaling / ephemeral recycling ───────────────────
807
+
808
+ def _setup_reactive_listener(
809
+ self,
810
+ handle: ComputeHandle,
811
+ store: Any,
812
+ n_runners_per_unit: int,
813
+ max_units: int,
814
+ ) -> None:
815
+ """Register a store listener for on-demand scaling and ephemeral replacement.
816
+
817
+ Called automatically from ``start()`` when a ``store`` is provided.
818
+ """
819
+ import math
820
+
821
+ lifecycle_str = handle.metadata.get("lifecycle", "persistent")
822
+ is_ephemeral = lifecycle_str == ContainerLifecycle.EPHEMERAL.value
823
+ orch = self
824
+
825
+ class _ReactiveListener:
826
+ """Store listener that starts/replaces containers on demand."""
827
+
828
+ @property
829
+ def capabilities(self) -> Dict[str, bool]:
830
+ return {}
831
+
832
+ def otlp_traces_endpoint(self) -> Optional[str]:
833
+ return None
834
+
835
+ def get_otlp_headers(self) -> Dict[str, str]:
836
+ return {}
837
+
838
+ async def on_rollout_created(self, rollout: Any) -> None:
839
+ current_units = len(handle.identifiers)
840
+ # Scale to one more container than we currently have,
841
+ # but only if all existing slots are occupied.
842
+ # Don't call wait_ready() here -- it blocks the event
843
+ # loop and causes cascading scale-ups. The container
844
+ # will become ready asynchronously; the runner inside
845
+ # polls the store and picks up work once healthy.
846
+ if current_units < max_units:
847
+ needed = current_units + 1
848
+ try:
849
+ orch.scale(handle, needed)
850
+ logger.info(
851
+ f"Reactive: started container agent-{current_units} "
852
+ f"for run {handle.run_id} "
853
+ f"({needed} total)"
854
+ )
855
+ except Exception as e:
856
+ logger.warning(f"Reactive scale-up failed for " f"run {handle.run_id}: {e}")
857
+
858
+ async def on_rollout_updated(self, rollout: Any) -> None:
859
+ if not is_ephemeral:
860
+ return
861
+ status = getattr(rollout, "status", None)
862
+ if status not in ("succeeded", "failed"):
863
+ return
864
+
865
+ # Replace the first container that is no longer running
866
+ # (exited after processing its rollout).
867
+ for key, cid in list(handle.identifiers.items()):
868
+ cstatus = orch._get_container_status(cid)
869
+ if cstatus in ("exited", "dead", None):
870
+ try:
871
+ new_cid = orch.replace_member(handle, key)
872
+ if new_cid:
873
+ logger.info(
874
+ f"Ephemeral recycle: replaced {key} "
875
+ f"({cid[:12]}) with {new_cid[:12]} "
876
+ f"for run {handle.run_id}"
877
+ )
878
+ except Exception as e:
879
+ logger.warning(f"Ephemeral recycle failed for {key}: {e}")
880
+ break
881
+
882
+ listener = _ReactiveListener()
883
+ if not hasattr(self, "_reactive_listeners"):
884
+ self._reactive_listeners: Dict[str, Any] = {}
885
+ self._reactive_listeners[handle.run_id] = (listener, store)
886
+
887
+ store.add_listener(listener)
888
+ logger.info(
889
+ f"Reactive listener registered for run {handle.run_id} "
890
+ f"(ephemeral={is_ephemeral}, n_runners={n_runners_per_unit}, "
891
+ f"max_units={max_units})"
892
+ )
893
+
894
+ def _teardown_reactive_listener(self, handle: ComputeHandle) -> None:
895
+ """Remove the reactive listener from the store."""
896
+ listeners = getattr(self, "_reactive_listeners", {})
897
+ entry = listeners.pop(handle.run_id, None)
898
+ if entry:
899
+ listener, store = entry
900
+ store.remove_listener(listener)
901
+ logger.debug(f"Reactive listener removed for run {handle.run_id}")