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,748 @@
1
+ """A browser on a world's UI: the computer-use half of a live session.
2
+
3
+ A world session answers on SURFACES. ``tools`` is always there (the world's
4
+ own operations over the rendezvous). An api-type world may also answer on
5
+ ``api`` (the service itself), ``ui`` (its dashboard, over the same state) and
6
+ ``browser`` — a headless Chromium the platform runs NEXT TO that UI, which an
7
+ agent drives through Playwright. This module is the Python client for the
8
+ last two:
9
+
10
+ with open_session("payments-like", "refund-dispute", surfaces=["ui", "browser"]) as s:
11
+ s.ready()
12
+ with s.browser() as b:
13
+ b.goto() # the UI's entry page
14
+ b.click("text=Refund")
15
+ png = b.screenshot()
16
+ cu = s.computer_use_tools() # the same actions as model tools
17
+ my_agent(cu.impls, cu.schemas, s.instruction)
18
+ s.grade() # sees what the clicks did
19
+
20
+ Playwright is an OPTIONAL dependency (``pip install playwright`` and
21
+ ``playwright install chromium`` for local mode); a hosted browser needs only
22
+ the ``playwright`` package. Nothing here imports it until a browser is opened,
23
+ so sessions that never touch a UI never pay for it.
24
+
25
+ MODES. ``hosted`` connects to the session's own browser (``surfaces.browser``
26
+ — a Playwright server the pod exposes over an authenticated websocket).
27
+ ``local`` launches Chromium on this machine and points it at ``surfaces.ui``;
28
+ it is what a laptop uses against a hosted twin, and what tests use. ``auto``
29
+ picks hosted when the session has one. Asking for a mode the session cannot
30
+ serve raises :class:`WorldSurfaceUnavailable` — never a silent fallback to a
31
+ different browser than the one the episode will be judged on.
32
+
33
+ OBSERVABILITY. Every action a :class:`WorldBrowser` takes (goto, click, type,
34
+ press, scroll, back) is reported to the platform as one event — what was done,
35
+ whether it worked, where the page ended up, and a JPEG of the screen after —
36
+ through a :class:`BrowserActionRecorder`. Reporting runs on a background
37
+ thread and never fails the action; the platform renders it as the session's
38
+ browser timeline. Typed text into secret-looking fields is redacted before it
39
+ leaves the process.
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ import base64
45
+ import importlib
46
+ import logging
47
+ import queue
48
+ import re
49
+ import threading
50
+ import time
51
+ import urllib.parse
52
+ from dataclasses import dataclass, field
53
+ from datetime import datetime, timezone
54
+ from typing import Any, Callable, Dict, List, Optional, Protocol
55
+
56
+ from .world_tools import (
57
+ WorldSessionError,
58
+ WorldToolkit,
59
+ WorldToolSpec,
60
+ schema_from_signature,
61
+ )
62
+
63
+ DEFAULT_VIEWPORT = {"width": 1280, "height": 800}
64
+ UI_TOKEN_QUERY = "token"
65
+ SETTLE_TIMEOUT_MS = 5_000
66
+ SETTLE_QUIET_MS = 150
67
+ SETTLE_POLL_MS = 25
68
+ SURFACE_KINDS = ("tools", "api", "ui", "browser")
69
+
70
+ EVENT_BATCH_SIZE = 10
71
+ EVENT_FLUSH_INTERVAL_SEC = 1.0
72
+ EVENT_MAX_RETRIES = 3
73
+ EVENT_SCREENSHOT_QUALITY = 60
74
+ EVENT_MAX_STRING = 500
75
+
76
+ _SECRET_KEY = re.compile(
77
+ r"pass(word|wd)?|secret|token|api[-_]?key|authorization|cookie"
78
+ r"|(^|[-_ #.\[\"'=])(otp|pin|cvv|ssn)([-_ \]\"']|$)",
79
+ re.IGNORECASE,
80
+ )
81
+ _SECRET_VALUE = re.compile(r"^((sk|pk|ghp|xox[abp])[-_][-_A-Za-z0-9]{8,}|AKIA[0-9A-Z]{12,}|Bearer [-_.A-Za-z0-9]{8,})")
82
+
83
+ log = logging.getLogger(__name__)
84
+
85
+
86
+ class WorldSurfaceUnavailable(WorldSessionError):
87
+ """The session was not opened with (or the world does not have) this surface."""
88
+
89
+ def __init__(self, surface: str, reason: str) -> None:
90
+ super().__init__(f"World surface '{surface}' is unavailable: {reason}")
91
+ self.surface = surface
92
+
93
+
94
+ @dataclass(frozen=True)
95
+ class ApiSurface:
96
+ url: str
97
+ #: Per-session credential of a World Host api surface, presented as
98
+ #: ``Authorization: Bearer``. ``None`` for a pod or a customer-hosted twin,
99
+ #: which answer with the world's own declared auth.
100
+ token: Optional[str] = None
101
+
102
+ @property
103
+ def headers(self) -> Dict[str, str]:
104
+ """Headers every request to :attr:`url` needs."""
105
+ return {"Authorization": f"Bearer {self.token}"} if self.token else {}
106
+
107
+
108
+ @dataclass(frozen=True)
109
+ class UiSurface:
110
+ url: str
111
+ entry: str = "/"
112
+ #: Same-origin prefix the UI proxies to the world API (``/__world-api``),
113
+ #: or ``None`` when the UI only talks to the API server-side.
114
+ api_path: Optional[str] = None
115
+ #: Per-session credential of a World Host UI. A browser navigation cannot
116
+ #: carry a header, so the first page load presents it once as ``?token=``
117
+ #: and the host answers with an HttpOnly cookie. ``None`` for pod UIs.
118
+ token: Optional[str] = None
119
+
120
+ @property
121
+ def entry_url(self) -> str:
122
+ return self.url_for(self.entry or "/")
123
+
124
+ def url_for(self, path: str) -> str:
125
+ """The URL of ``path`` on this surface, carrying the session token when
126
+ there is one. ``path`` may already have a query string."""
127
+ rel = path if path.startswith("/") else "/" + path
128
+ full = self.url.rstrip("/") + rel
129
+ if not self.token:
130
+ return full
131
+ before_hash, hash_sep, fragment = full.partition("#")
132
+ joiner = "&" if "?" in before_hash else "?"
133
+ query = urllib.parse.urlencode({UI_TOKEN_QUERY: self.token})
134
+ return before_hash + joiner + query + hash_sep + fragment
135
+
136
+ @property
137
+ def api_url(self) -> Optional[str]:
138
+ """The API as reachable through the UI's origin, if it proxies one."""
139
+ if not self.api_path:
140
+ return None
141
+ return self.url.rstrip("/") + self.api_path
142
+
143
+
144
+ def without_ui_token(url: str) -> str:
145
+ """``url`` without its ``token`` query parameter: what may be logged."""
146
+ parts = urllib.parse.urlsplit(url)
147
+ if not parts.query:
148
+ return url
149
+ kept = [(k, v) for k, v in urllib.parse.parse_qsl(parts.query, keep_blank_values=True) if k != UI_TOKEN_QUERY]
150
+ return urllib.parse.urlunsplit(parts._replace(query=urllib.parse.urlencode(kept)))
151
+
152
+
153
+ @dataclass(frozen=True)
154
+ class BrowserSurface:
155
+ """``protocol`` is ``cdp`` for the platform's Chromium relay (any number of
156
+ agents share one browser, each in its own context) or ``playwright`` for a
157
+ ``playwright run-server`` endpoint."""
158
+
159
+ ws_url: str
160
+ protocol: str = "cdp"
161
+ viewport: Dict[str, int] = field(default_factory=lambda: dict(DEFAULT_VIEWPORT))
162
+
163
+
164
+ @dataclass(frozen=True)
165
+ class SessionSurfaces:
166
+ """Parsed ``surfaces`` from the session descriptor."""
167
+
168
+ api: Optional[ApiSurface] = None
169
+ ui: Optional[UiSurface] = None
170
+ browser: Optional[BrowserSurface] = None
171
+
172
+ @classmethod
173
+ def from_descriptor(cls, raw: Optional[Dict[str, Any]]) -> "SessionSurfaces":
174
+ raw = raw or {}
175
+ api = raw.get("api")
176
+ ui = raw.get("ui")
177
+ browser = raw.get("browser")
178
+ return cls(
179
+ api=(ApiSurface(url=api["url"], token=api.get("token") or None) if api else None),
180
+ ui=(
181
+ UiSurface(
182
+ url=ui["url"],
183
+ entry=ui.get("entry") or "/",
184
+ api_path=ui.get("apiPath") or None,
185
+ token=ui.get("token") or None,
186
+ )
187
+ if ui
188
+ else None
189
+ ),
190
+ browser=(
191
+ BrowserSurface(
192
+ ws_url=browser["wsUrl"],
193
+ protocol=browser.get("protocol") or "cdp",
194
+ viewport=dict(browser.get("viewport") or DEFAULT_VIEWPORT),
195
+ )
196
+ if browser
197
+ else None
198
+ ),
199
+ )
200
+
201
+ @property
202
+ def kinds(self) -> List[str]:
203
+ kinds = ["tools"]
204
+ if self.api is not None:
205
+ kinds.append("api")
206
+ if self.ui is not None:
207
+ kinds.append("ui")
208
+ if self.browser is not None:
209
+ kinds.append("browser")
210
+ return kinds
211
+
212
+
213
+ class _Keyboard(Protocol):
214
+ def press(self, key: str, **kwargs: Any) -> Any: ...
215
+
216
+
217
+ class _Mouse(Protocol):
218
+ def wheel(self, delta_x: float, delta_y: float) -> Any: ...
219
+
220
+
221
+ class _Page(Protocol):
222
+ """The slice of a Playwright ``Page`` this module uses."""
223
+
224
+ url: str
225
+ keyboard: _Keyboard
226
+ mouse: _Mouse
227
+
228
+ def goto(self, url: str, **kwargs: Any) -> Any: ...
229
+ def click(self, selector: str, **kwargs: Any) -> Any: ...
230
+ def fill(self, selector: str, value: str, **kwargs: Any) -> Any: ...
231
+ def screenshot(self, **kwargs: Any) -> bytes: ...
232
+ def inner_text(self, selector: str, **kwargs: Any) -> str: ...
233
+ def content(self) -> str: ...
234
+ def go_back(self, **kwargs: Any) -> Any: ...
235
+ def wait_for_load_state(self, state: str = "load", **kwargs: Any) -> Any: ...
236
+ def wait_for_timeout(self, timeout: float) -> Any: ...
237
+ def title(self) -> str: ...
238
+ def on(self, event: str, handler: Callable[..., Any]) -> Any: ...
239
+
240
+
241
+ # ── action events ────────────────────────────────────────────────────────────────
242
+
243
+
244
+ def redact_action_args(action: str, args: Dict[str, Any]) -> Dict[str, Any]:
245
+ """What of an action's arguments may leave the process. Selectors, paths
246
+ and keys are what a reviewer needs; anything keyed like a credential, or
247
+ typed into a field whose selector names one, or shaped like a known key,
248
+ is replaced. Strings are capped, nested values dropped."""
249
+ out: Dict[str, Any] = {}
250
+ secret_field = action == "browser_type" and bool(_SECRET_KEY.search(str(args.get("selector", ""))))
251
+ for key, value in args.items():
252
+ if value is None or isinstance(value, (bool, int, float)):
253
+ out[key] = value
254
+ elif isinstance(value, str):
255
+ if _SECRET_KEY.search(key) or _SECRET_VALUE.match(value) or (secret_field and key == "text"):
256
+ out[key] = "[redacted]"
257
+ else:
258
+ out[key] = value if len(value) <= EVENT_MAX_STRING else value[:EVENT_MAX_STRING] + "…"
259
+ else:
260
+ out[key] = "[omitted]"
261
+ return out
262
+
263
+
264
+ class BrowserActionRecorder:
265
+ """Ships browser action events to the platform, off the action's hot path.
266
+
267
+ ``post`` receives a batch of wire-shaped events (the body of
268
+ ``POST /world-sessions/{id}/browser-events``). Events are numbered here,
269
+ so ordering survives batching and retries — the platform dedups on
270
+ ``(agentId, seq)``. A batch that keeps failing is dropped with a warning:
271
+ telemetry must never stall or fail the agent.
272
+
273
+ Numbering is durable across recorders: :meth:`next_seq` hands out a
274
+ position within this recorder, and the first send adds the base
275
+ ``resume_from`` reports — the platform's next free seq for this agent —
276
+ so an agent id reopened in a later process appends to its timeline
277
+ instead of colliding with (and being deduplicated against) what it
278
+ already recorded. The base is fixed once resolved, which keeps retries
279
+ idempotent.
280
+ """
281
+
282
+ def __init__(
283
+ self,
284
+ post: Callable[[List[Dict[str, Any]]], None],
285
+ agent_id: str,
286
+ *,
287
+ resume_from: Optional[Callable[[], int]] = None,
288
+ batch_size: int = EVENT_BATCH_SIZE,
289
+ flush_interval: float = EVENT_FLUSH_INTERVAL_SEC,
290
+ max_retries: int = EVENT_MAX_RETRIES,
291
+ ) -> None:
292
+ self.agent_id = agent_id
293
+ self._post = post
294
+ self._resume_from = resume_from
295
+ self._base: Optional[int] = None
296
+ self._batch_size = batch_size
297
+ self._flush_interval = flush_interval
298
+ self._max_retries = max_retries
299
+ self._queue: "queue.Queue[Optional[Dict[str, Any]]]" = queue.Queue()
300
+ self._seq = 0
301
+ self._pending = 0
302
+ self._cond = threading.Condition()
303
+ self.sent = 0
304
+ self.dropped = 0
305
+ self._thread = threading.Thread(target=self._run, name="gateway-browser-events", daemon=True)
306
+ self._thread.start()
307
+
308
+ def next_seq(self) -> int:
309
+ """This recorder's next position; the wire seq is ``resume_from() + this``."""
310
+ with self._cond:
311
+ seq = self._seq
312
+ self._seq += 1
313
+ return seq
314
+
315
+ def _resolve_base(self) -> int:
316
+ if self._base is None:
317
+ base = self._resume_from() if self._resume_from is not None else 0
318
+ if not isinstance(base, int) or isinstance(base, bool) or base < 0:
319
+ raise ValueError(f"browser event base seq must be a non-negative integer, got {base!r}")
320
+ self._base = base
321
+ return self._base
322
+
323
+ def record(self, event: Dict[str, Any]) -> None:
324
+ with self._cond:
325
+ self._pending += 1
326
+ self._queue.put(event)
327
+
328
+ def flush(self, timeout: float = 15.0) -> bool:
329
+ """Block until everything recorded so far has been posted (or dropped)."""
330
+ with self._cond:
331
+ return self._cond.wait_for(lambda: self._pending == 0, timeout)
332
+
333
+ def close(self, timeout: float = 15.0) -> None:
334
+ self.flush(timeout)
335
+ self._queue.put(None)
336
+ self._thread.join(timeout)
337
+
338
+ def _run(self) -> None:
339
+ while True:
340
+ item = self._queue.get()
341
+ if item is None:
342
+ return
343
+ batch: List[Dict[str, Any]] = [item]
344
+ deadline = time.monotonic() + self._flush_interval
345
+ stop = False
346
+ while len(batch) < self._batch_size:
347
+ remaining = deadline - time.monotonic()
348
+ if remaining <= 0:
349
+ break
350
+ try:
351
+ item = self._queue.get(timeout=remaining)
352
+ except queue.Empty:
353
+ break
354
+ if item is None:
355
+ stop = True
356
+ break
357
+ batch.append(item)
358
+ self._send(batch)
359
+ if stop:
360
+ return
361
+
362
+ def _send(self, batch: List[Dict[str, Any]]) -> None:
363
+ try:
364
+ for attempt in range(self._max_retries):
365
+ try:
366
+ base = self._resolve_base()
367
+ self._post(batch if base == 0 else [{**e, "seq": base + e["seq"]} for e in batch])
368
+ self.sent += len(batch)
369
+ return
370
+ except Exception as exc: # noqa: BLE001 — telemetry must not raise
371
+ if attempt + 1 == self._max_retries:
372
+ self.dropped += len(batch)
373
+ log.warning("dropping %d browser events after %d attempts: %s", len(batch), attempt + 1, exc)
374
+ return
375
+ time.sleep(0.5 * (2**attempt))
376
+ finally:
377
+ with self._cond:
378
+ self._pending -= len(batch)
379
+ self._cond.notify_all()
380
+
381
+
382
+ def _playwright_module() -> Any:
383
+ try:
384
+ return importlib.import_module("playwright.sync_api")
385
+ except ImportError as error: # pragma: no cover - depends on the install
386
+ raise WorldSessionError(
387
+ "Driving a world UI needs Playwright: pip install playwright "
388
+ "(and `playwright install chromium` for local mode)."
389
+ ) from error
390
+
391
+
392
+ class WorldBrowser:
393
+ """One Playwright page on the session's UI.
394
+
395
+ Thin on purpose: the point is to hand the agent the SAME browser the
396
+ platform records, not to re-invent Playwright. ``page`` is the real
397
+ Playwright page for anything this wrapper does not cover.
398
+ """
399
+
400
+ def __init__(
401
+ self,
402
+ page: _Page,
403
+ ui: Optional[UiSurface],
404
+ *,
405
+ mode: str,
406
+ close: Callable[[], None],
407
+ viewport: Optional[Dict[str, int]] = None,
408
+ recorder: Optional[BrowserActionRecorder] = None,
409
+ ) -> None:
410
+ self.page = page
411
+ self.ui = ui
412
+ self.mode = mode
413
+ self.viewport = dict(viewport or DEFAULT_VIEWPORT)
414
+ self.recorder = recorder
415
+ self._close = close
416
+ self._inflight = 0
417
+ self._last_network = time.monotonic()
418
+ page.on("request", self._on_request)
419
+ page.on("requestfinished", self._on_request_done)
420
+ page.on("requestfailed", self._on_request_done)
421
+
422
+ @property
423
+ def agent_id(self) -> Optional[str]:
424
+ return self.recorder.agent_id if self.recorder else None
425
+
426
+ def _act(self, action: str, args: Dict[str, Any], fn: Callable[[], Any]) -> Any:
427
+ """Run one action and report it: the event carries what was asked,
428
+ whether it worked, and the screen after — for failures too, so a
429
+ reviewer sees what the agent saw when the selector did not match."""
430
+ if self.recorder is None:
431
+ return fn()
432
+ started = time.monotonic()
433
+ ok, error = True, None
434
+ try:
435
+ return fn()
436
+ except Exception as exc:
437
+ ok, error = False, f"{type(exc).__name__}: {exc}"[:4000]
438
+ raise
439
+ finally:
440
+ event: Dict[str, Any] = {
441
+ "agentId": self.recorder.agent_id,
442
+ "seq": self.recorder.next_seq(),
443
+ "ts": datetime.now(timezone.utc).isoformat(),
444
+ "action": action,
445
+ "args": redact_action_args(action, args),
446
+ "ok": ok,
447
+ "error": error,
448
+ "durationMs": int((time.monotonic() - started) * 1000),
449
+ "mode": self.mode,
450
+ "viewport": dict(self.viewport),
451
+ }
452
+ event.update(self._observe_for_event())
453
+ self.recorder.record(event)
454
+
455
+ def _observe_for_event(self) -> Dict[str, Any]:
456
+ out: Dict[str, Any] = {"url": None, "title": None, "screenshot": None}
457
+ try:
458
+ out["url"] = self.url
459
+ out["title"] = self.page.title()
460
+ jpeg = self.page.screenshot(type="jpeg", quality=EVENT_SCREENSHOT_QUALITY)
461
+ out["screenshot"] = base64.b64encode(jpeg).decode("ascii")
462
+ except Exception as exc: # noqa: BLE001 — a closed page still gets its event
463
+ log.debug("browser event observation failed: %s", exc)
464
+ return out
465
+
466
+ # ── navigation ─────────────────────────────────────────────────────────
467
+ def goto(self, path_or_url: Optional[str] = None) -> str:
468
+ """Open a UI path (``/customers``), a full URL, or — with no argument —
469
+ the UI's declared entry page."""
470
+ if path_or_url is None:
471
+ if self.ui is None:
472
+ raise WorldSurfaceUnavailable("ui", "this session has no UI to navigate to")
473
+ target = self.ui.entry_url
474
+ elif path_or_url.startswith(("http://", "https://")):
475
+ target = path_or_url
476
+ else:
477
+ if self.ui is None:
478
+ raise WorldSurfaceUnavailable("ui", "relative paths need a UI surface")
479
+ target = self.ui.url_for(path_or_url)
480
+
481
+ def run() -> str:
482
+ self.page.goto(target, wait_until="load")
483
+ self._settle()
484
+ return self.url
485
+
486
+ return self._act(
487
+ "browser_goto",
488
+ {"path": path_or_url, "url": without_ui_token(target)},
489
+ run,
490
+ )
491
+
492
+ def back(self) -> str:
493
+ def run() -> str:
494
+ self.page.go_back(wait_until="load")
495
+ self._settle()
496
+ return self.url
497
+
498
+ return self._act("browser_back", {}, run)
499
+
500
+ def _on_request(self, _request: Any) -> None:
501
+ self._inflight += 1
502
+ self._last_network = time.monotonic()
503
+
504
+ def _on_request_done(self, _request: Any) -> None:
505
+ self._inflight = max(0, self._inflight - 1)
506
+ self._last_network = time.monotonic()
507
+
508
+ def _settle(self) -> None:
509
+ """Wait for the page to finish reacting to an action.
510
+
511
+ ``load`` alone is not enough for client-rendered UIs (React et al.): a
512
+ click returns before the fetch that re-renders the page, so an agent
513
+ would read stale text. Playwright's ``networkidle`` is per navigation
514
+ and resolves at once after the first one, so this tracks requests
515
+ itself: done when nothing is in flight and the network has been quiet
516
+ for ``SETTLE_QUIET_MS``, bounded so long-polling UIs cannot hang us.
517
+ """
518
+ self.page.wait_for_load_state("load")
519
+ deadline = time.monotonic() + SETTLE_TIMEOUT_MS / 1000
520
+ while time.monotonic() < deadline:
521
+ quiet = (time.monotonic() - self._last_network) * 1000 >= SETTLE_QUIET_MS
522
+ if self._inflight == 0 and quiet:
523
+ return
524
+ self.page.wait_for_timeout(SETTLE_POLL_MS)
525
+
526
+ # ── acting ─────────────────────────────────────────────────────────────
527
+ def click(self, selector: str) -> None:
528
+ def run() -> None:
529
+ self.page.click(selector)
530
+ self._settle()
531
+
532
+ self._act("browser_click", {"selector": selector}, run)
533
+
534
+ def type(self, selector: str, text: str) -> None:
535
+ self._act("browser_type", {"selector": selector, "text": text}, lambda: self.page.fill(selector, text))
536
+
537
+ def press(self, key: str) -> None:
538
+ def run() -> None:
539
+ self.page.keyboard.press(key)
540
+ self._settle()
541
+
542
+ self._act("browser_press", {"key": key}, run)
543
+
544
+ def scroll(self, dy: int) -> None:
545
+ self._act("browser_scroll", {"dy": dy}, lambda: self.page.mouse.wheel(0, dy))
546
+
547
+ # ── observing ──────────────────────────────────────────────────────────
548
+ def screenshot(self, full_page: bool = False) -> bytes:
549
+ return self.page.screenshot(type="png", full_page=full_page)
550
+
551
+ def screenshot_b64(self, full_page: bool = False) -> str:
552
+ return base64.b64encode(self.screenshot(full_page)).decode("ascii")
553
+
554
+ def text(self, selector: str = "body") -> str:
555
+ return self.page.inner_text(selector)
556
+
557
+ def html(self) -> str:
558
+ return self.page.content()
559
+
560
+ @property
561
+ def url(self) -> str:
562
+ """Where the page is, less the session token a World Host landing
563
+ carries: the host trades it for a cookie and redirects, but a page that
564
+ never got that far (host down, wrong origin) would still show it."""
565
+ return without_ui_token(self.page.url)
566
+
567
+ def title(self) -> str:
568
+ return self.page.title()
569
+
570
+ # ── lifecycle ──────────────────────────────────────────────────────────
571
+ def close(self) -> None:
572
+ try:
573
+ self._close()
574
+ finally:
575
+ if self.recorder is not None:
576
+ self.recorder.close()
577
+
578
+ def __enter__(self) -> "WorldBrowser":
579
+ return self
580
+
581
+ def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
582
+ self.close()
583
+
584
+
585
+ def open_browser(
586
+ surfaces: SessionSurfaces,
587
+ *,
588
+ mode: str = "auto",
589
+ headless: bool = True,
590
+ recorder: Optional[BrowserActionRecorder] = None,
591
+ ) -> WorldBrowser:
592
+ """Open a browser on the session's UI in ``hosted``, ``local`` or ``auto`` mode."""
593
+ if mode not in ("auto", "hosted", "local"):
594
+ raise ValueError("mode must be 'auto', 'hosted' or 'local'")
595
+ if surfaces.ui is None and surfaces.browser is None:
596
+ raise WorldSurfaceUnavailable("ui", "open the session with surfaces=['ui'] on a world that declares [ui]")
597
+ resolved = mode
598
+ if mode == "auto":
599
+ resolved = "hosted" if surfaces.browser is not None else "local"
600
+ if resolved == "hosted" and surfaces.browser is None:
601
+ raise WorldSurfaceUnavailable("browser", "open the session with surfaces=['browser'] to get a hosted browser")
602
+
603
+ sync_api = _playwright_module()
604
+ pw = sync_api.sync_playwright().start()
605
+ viewport = surfaces.browser.viewport if surfaces.browser else dict(DEFAULT_VIEWPORT)
606
+ try:
607
+ if resolved == "hosted":
608
+ assert surfaces.browser is not None
609
+ if surfaces.browser.protocol == "playwright":
610
+ browser = pw.chromium.connect(surfaces.browser.ws_url)
611
+ else:
612
+ browser = pw.chromium.connect_over_cdp(surfaces.browser.ws_url)
613
+ else:
614
+ browser = pw.chromium.launch(headless=headless)
615
+ context = browser.new_context(viewport=viewport)
616
+ page = context.new_page()
617
+ except Exception:
618
+ pw.stop()
619
+ raise
620
+
621
+ def close() -> None:
622
+ try:
623
+ context.close()
624
+ browser.close()
625
+ finally:
626
+ pw.stop()
627
+
628
+ return WorldBrowser(page, surfaces.ui, mode=resolved, close=close, viewport=viewport, recorder=recorder)
629
+
630
+
631
+ # ── computer-use tools ───────────────────────────────────────────────────────
632
+
633
+ COMPUTER_USE_SPECS: List[WorldToolSpec] = [
634
+ WorldToolSpec(
635
+ "browser_goto",
636
+ "browser_goto(path=None)",
637
+ "Open a page of the world's UI. `path` is a UI path like /customers, a full URL, or omitted for the entry page. Returns the URL and page text.",
638
+ ),
639
+ WorldToolSpec(
640
+ "browser_click",
641
+ "browser_click(selector)",
642
+ "Click an element by Playwright selector (CSS, `text=Refund`, `role=button[name=Save]`). Returns the page text after the click.",
643
+ ),
644
+ WorldToolSpec(
645
+ "browser_type",
646
+ "browser_type(selector, text)",
647
+ "Replace the value of an input/textarea found by selector with `text`.",
648
+ ),
649
+ WorldToolSpec(
650
+ "browser_press",
651
+ "browser_press(key)",
652
+ "Press a key (Enter, Tab, Escape, ArrowDown, ...). Returns the page text after.",
653
+ ),
654
+ WorldToolSpec(
655
+ "browser_scroll",
656
+ "browser_scroll(dy=600)",
657
+ "Scroll the page vertically by `dy` pixels (negative scrolls up).",
658
+ ),
659
+ WorldToolSpec(
660
+ "browser_screenshot",
661
+ "browser_screenshot(full_page=False)",
662
+ "Take a PNG screenshot; returns {'image_base64': ..., 'url': ...}.",
663
+ ),
664
+ WorldToolSpec(
665
+ "browser_read",
666
+ "browser_read(selector='body')",
667
+ "Return the visible text of an element (default: whole page) plus the URL and title.",
668
+ ),
669
+ WorldToolSpec(
670
+ "browser_back",
671
+ "browser_back()",
672
+ "Go back one page in history.",
673
+ ),
674
+ ]
675
+
676
+
677
+ def computer_use_toolkit(browser: WorldBrowser, max_text: int = 20000) -> WorldToolkit:
678
+ """The browser as model tools, in the same :class:`WorldToolkit` shape
679
+ the world's own tools come in — so an agent loop that already dispatches
680
+ ``toolkit().impls`` merges ``computer_use_toolkit(b).impls`` and gains a
681
+ screen without a second dispatch path."""
682
+
683
+ def observe(selector: str = "body") -> Dict[str, Any]:
684
+ text = browser.text(selector)
685
+ return {
686
+ "url": browser.url,
687
+ "title": browser.title(),
688
+ "text": text[:max_text],
689
+ "truncated": len(text) > max_text,
690
+ }
691
+
692
+ def goto(path: Optional[str] = None) -> Dict[str, Any]:
693
+ browser.goto(path)
694
+ return observe()
695
+
696
+ def click(selector: str) -> Dict[str, Any]:
697
+ browser.click(selector)
698
+ return observe()
699
+
700
+ def type_(selector: str, text: str) -> Dict[str, Any]:
701
+ browser.type(selector, str(text))
702
+ return {"ok": True, "url": browser.url}
703
+
704
+ def press(key: str) -> Dict[str, Any]:
705
+ browser.press(key)
706
+ return observe()
707
+
708
+ def scroll(dy: int = 600) -> Dict[str, Any]:
709
+ browser.scroll(int(dy))
710
+ return {"ok": True, "url": browser.url}
711
+
712
+ def screenshot(full_page: bool = False) -> Dict[str, Any]:
713
+ return {"image_base64": browser.screenshot_b64(bool(full_page)), "url": browser.url}
714
+
715
+ def read(selector: str = "body") -> Dict[str, Any]:
716
+ return observe(selector)
717
+
718
+ def back() -> Dict[str, Any]:
719
+ browser.back()
720
+ return observe()
721
+
722
+ raw_impls: Dict[str, Callable[..., Any]] = {
723
+ "browser_goto": goto,
724
+ "browser_click": click,
725
+ "browser_type": type_,
726
+ "browser_press": press,
727
+ "browser_scroll": scroll,
728
+ "browser_screenshot": screenshot,
729
+ "browser_read": read,
730
+ "browser_back": back,
731
+ }
732
+
733
+ def bind(fn: Callable[..., Any]) -> Callable[..., Any]:
734
+ # Callable either way, like the world's own tools: impl({"selector": s})
735
+ # or impl(selector=s).
736
+ def impl(args: Optional[Dict[str, Any]] = None, **kwargs: Any) -> Any:
737
+ payload = dict(args or {})
738
+ payload.update(kwargs)
739
+ return fn(**payload)
740
+
741
+ impl.__name__ = fn.__name__
742
+ return impl
743
+
744
+ return WorldToolkit(
745
+ schemas=[schema_from_signature(spec) for spec in COMPUTER_USE_SPECS],
746
+ impls={name: bind(fn) for name, fn in raw_impls.items()},
747
+ specs=list(COMPUTER_USE_SPECS),
748
+ )