From 6dd10e0339f5404f53877ab229499276a7f63dd1 Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Tue, 27 Oct 2026 18:17:12 -0700 Subject: [PATCH 1/9] feat(training): train with TRL on a bundle served as an OpenEnv env agent_env.training.trl, behind a new `trl` extra (openenv and datasets): AgentEnv(url) reads a build_app server's tasks into a dataset, one row per task, and builds TRL's environment_factory. Each environment holds one session on the server and plays one episode at a time over OpenEnv's WebSocket. The turn's tools become typed, documented methods, reset runs in the background, end_turn hands over the next prompt, and get_reward owns the reward, None when the episode ended in an error. GRPOTrainer and AsyncGRPO take both unchanged. AgentEnv.local(bundle) serves the bundle in the training process. The Task API now names each task's toolset (the played agent's envs, role, and whether it gets bash). Tasks with different toolsets take TRL's multi-environment mode. The dependency tests now allow an extra that includes another, as trl includes openenv. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 3 +- README.md | 12 + THIRD_PARTY_NOTICES.md | 2533 ++++++++++++++++- pyproject.toml | 7 +- src/agent_env/openenv/app.py | 9 +- src/agent_env/openenv/environment.py | 12 +- src/agent_env/training/__init__.py | 2 + src/agent_env/training/trl.py | 566 ++++ tst/integration/training/__init__.py | 0 tst/integration/training/conftest.py | 5 + tst/integration/training/trl_test.py | 73 + tst/unit/openenv/environment_test.py | 3 +- .../test_optional_extras_stay_optional.py | 16 +- .../test_runtime_dependencies_are_imported.py | 47 +- tst/unit/training/__init__.py | 0 tst/unit/training/conftest.py | 6 + tst/unit/training/trl_test.py | 252 ++ uv.lock | 307 +- 18 files changed, 3819 insertions(+), 34 deletions(-) create mode 100644 src/agent_env/training/__init__.py create mode 100644 src/agent_env/training/trl.py create mode 100644 tst/integration/training/__init__.py create mode 100644 tst/integration/training/conftest.py create mode 100644 tst/integration/training/trl_test.py create mode 100644 tst/unit/training/__init__.py create mode 100644 tst/unit/training/conftest.py create mode 100644 tst/unit/training/trl_test.py diff --git a/AGENTS.md b/AGENTS.md index 19b082bef..9db05a3f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -112,7 +112,8 @@ and deserialized through a registry. | `config/` | The `Config` singleton (`get_config`, `configure`, `reset_config`) in `config/runtime.py`, file discovery in `config/loader.py`, and `load_impl`, which resolves `module:Class` pointers. `agent_env.store` re-exports the config names for compatibility. | | `providers/` | `providers/sandbox_providers/` holds the sandbox providers `local`, `modal`, `modal_vm`, `e2b`, `sail_vm` (the `sail` extra), `smol_vm` (the `smol` extra), `tensorlake` (the `tensorlake` extra), `tenki` (the `tenki` extra), `vercel` (the `vercel` extra), `boxd` (the `boxd` extra), `daytona` (the `daytona` extra); `[sandbox] default` and `agent_default` accept a comma-separated fallback chain, whose deploys skip a provider that can't run them. A sandbox provider declares what it runs (`ON_THIS_MACHINE`, and for VMs on this machine the address their requests to it arrive at, `REACHES_HOST_AT`; `CONTAINER_ACCEPTS`, `PRIVATE_NETWORK`; `SUPPORTED_SANDBOX_MODES`, derived from whether it implements `create_sandbox` and `create_vm`; and `SANDBOX_ACCEPTS`, the images an agent's sandbox runs, which defaults to those a VM loads when it implements `create_vm` and to `CONTAINER_ACCEPTS` otherwise, so only a provider that implements `create_vm` yet gives an agent a container, like the local one, declares it), each sandbox carries the `platform` of the images it runs (linux/amd64 unless its provider sets another when it creates it; None, any, unchecked, for a local sandbox), and code outside `providers/sandbox_providers/` reads those, never which provider it has. `providers/env_providers/` holds the environment providers: `EnvironmentProvider` (an env's containers and state store), `EnvironmentGatewayProvider` (`gateway`, an env's default), which renders a docker-compose for the gateway and its MCP servers inside the sandbox, and `EnvironmentServerProvider` (`server`), one MCP server in its own container with no gateway in front and no state store; `providers/env_state/` holds env-state providers (`local_postgres` built in). | | `a2a_agent/` | The `A2AAgent` entity (`a2a_agent`), its stores and the validator steps. The protocol package provides the agent-side framework. | -| `openenv/` | `build_app(bundle)`: the [OpenEnv](https://github.com/openenv/OpenEnv) server for a bundle's tasks, each session playing one episode through the Episode API, with the envs' MCP tools, an added `end_turn` tool and the task's verifier score as the reward. It prepares bundles as `agent-env run` does (`prepare_bundle(played=True)`, without the agent the client plays) and keeps namespace routing on for the app's life. `openenv` comes with the `openenv` extra, a floor with no cap, and this package is the only one that imports it. | +| `openenv/` | `build_app(bundle)`: the [OpenEnv](https://github.com/openenv/OpenEnv) server for a bundle's tasks, each session playing one episode through the Episode API, with the envs' MCP tools, an added `end_turn` tool and the task's verifier score as the reward. It prepares bundles as `agent-env run` does (`prepare_bundle(played=True)`, without the agent the client plays) and keeps namespace routing on for the app's life. `openenv` comes with the `openenv` extra, a floor with no cap; this package and `training/trl.py` are the only ones that import it. Each task's description in the Task API names its `toolset` (the played agent's envs, role, and whether it gets `bash`). | +| `training/` | One module per outside trainer, each behind its own extra. `trl.py` (`trl` extra): `AgentEnv(url)` over a server `openenv/` makes, with `dataset` (a row per task) and `factory` (TRL's `environment_factory`: a generated class per toolset whose methods are the tools, a background `reset` and an owned `get_reward`); `AgentEnv.local(bundle)` serves the bundle in process. | | `runner/` | The `[runner]` seam: `Runner.submit()` returns a `RunHandle` (`run_id`, `instance_id`) before the run starts; `LocalRunner` is built in. `runner/store.py` is the run index every runner writes and the explorer reads. | | `bundle/` | A bundle is a folder of entities (`envs/`, `agents/`, `artifacts/`, `skills/`, `tasks/.json`, `evals/.toml`) that runs without a registry: `agent-env run` plans it, writes it into the `@local` namespace (reusing each version whose inputs haven't changed) and runs its evals or tasks; `agent-env bundle init` writes a stored task or eval as one, with every env, agent and artifact it uses in its own folder (each image as its build context, pruned to what its Dockerfile reads and pinned to the Python and npm versions of the stored image) so it runs with no store. | | `eval/` | `Eval` (`eval`), a versioned list of task references, and its store. | diff --git a/README.md b/README.md index 93e469d7b..815432763 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,18 @@ A package of your own can add env types, providers, task steps and stores to age Every task in a bundle is an [OpenEnv](https://github.com/openenv/OpenEnv) env, with the `openenv` extra installed (`pip install 'agentenv-framework[openenv]'`). `agent_env.openenv.build_app(bundle)` returns the OpenEnv server for a bundle, a folder or an installed bundle's name, or for a mapping of split names to bundles. Any OpenEnv client resets it to a task, which deploys the task's envs and opens its agent's turn, acts through the envs' tools and an added `end_turn` tool, and gets the task's verifier score as its reward. +TRL's `GRPOTrainer` and AsyncGRPO train on such a server with the `trl` extra (`pip install 'agentenv-framework[trl]'`). `AgentEnv(url)` from `agent_env.training.trl` gives `dataset`, one row per task, and `factory`, the `environment_factory` whose environments play the tasks' episodes and own their reward, so the trainer needs no `reward_funcs`. `AgentEnv.local(bundle, max_concurrent_envs=...)` serves the bundle in the training process instead. The server must allow a session for every rollout the trainer generates at once (`MAX_CONCURRENT_ENVS`). OpenEnv 0.10 holds the trainer to mcp 1, and so to vLLM 0.29 or older. + +```python +from trl import GRPOConfig, GRPOTrainer +from agent_env.training.trl import AgentEnv + +env = AgentEnv("http://localhost:8000") +trainer = GRPOTrainer(model="Qwen/Qwen3-4B", args=GRPOConfig(max_tool_calling_iterations=12), + train_dataset=env.dataset, environment_factory=env.factory) +trainer.train() +``` + ## Time zones Servers and the env clock work in UTC. To run an agent in a user's time zone, the task sets it on the agent's `deploy_agent` step as `env_vars: {"TZ": "America/New_York"}`, which reaches the agent's sandbox, and names the zone in the agent's prompt. While the env clock is armed, the gateway's `get_time` tool takes that IANA name as `time_zone` and adds the `local_time` in that zone, and a trigger executor's working `hours` can take a `time_zone` instead of a fixed `utc_offset`, so they keep their local hours across daylight-saving changes. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 1fc40c98e..83a625be6 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -14,7 +14,7 @@ marks, or contributor names to endorse or promote Scale AI, or related products. ## Python dependencies -The runtime dependencies of `agentenv-framework` and its `aws`, `gcp`, `sail`, `tensorlake`, `tenki`, `boxd`, `daytona` and `openenv` extras, at the versions resolved in `uv.lock`. Development-only dependencies are not listed. +The runtime dependencies of `agentenv-framework` and its `aws`, `gcp`, `sail`, `tensorlake`, `tenki`, `boxd`, `daytona`, `openenv` and `trl` extras, at the versions resolved in `uv.lock`. Development-only dependencies are not listed. ### a2a-sdk 0.3.26 @@ -257,6 +257,12 @@ The runtime dependencies of `agentenv-framework` and its `aws`, `gcp`, `sail`, ` - Source: - [License text 1](#license-text-1) +### datasets 5.1.0 + +- License: Apache-2.0 +- Source: +- [License text 82](#license-text-82) + ### daytona 0.224.1 - License: Apache-2.0 @@ -313,6 +319,13 @@ The runtime dependencies of `agentenv-framework` and its `aws`, `gcp`, `sail`, ` - Source: - [License text 79](#license-text-79) +### dill 0.4.1 + +- License: BSD-3-Clause +- Copyright: Copyright (c) 2004-2016 California Institute of Technology.; Copyright (c) 2016-2026 The Uncertainty Quantification Foundation. +- Source: +- [License text 117](#license-text-117) + ### distro 1.9.0 - License: Apache-2.0 @@ -760,6 +773,13 @@ The runtime dependencies of `agentenv-framework` and its `aws`, `gcp`, `sail`, ` - Source: - [License text 28](#license-text-28) +### multiprocess 0.70.19 + +- License: BSD-3-Clause +- Copyright: Copyright (c) 2008-2016 California Institute of Technology.; Copyright (c) 2016-2026 The Uncertainty Quantification Foundation.; Copyright (c) 2006-2008, R Oudkerk +- Source: +- [License text 118](#license-text-118), [License text 119](#license-text-119) + ### numpy 2.5.4 - License: BSD-3-Clause AND 0BSD AND MIT AND Zlib AND CC0-1.0 @@ -940,6 +960,13 @@ The runtime dependencies of `agentenv-framework` and its `aws`, `gcp`, `sail`, ` - Source: - [License text 1](#license-text-1) +### pyarrow 26.0.0 + +- License: Apache-2.0 +- Copyright: Copyright 2016-2026 The Apache Software Foundation; Copyright (c) 2013 The Chromium Authors. All rights reserved.; Copyright: 2013 Daniel Lemire; Copyright 2015 The TensorFlow Authors. All Rights Reserved.; Copyright (c) 2005-2017, NumPy Developers.; Copyright 2014 Google Inc. +- Source: +- [License text 120](#license-text-120), [License text 121](#license-text-121) + ### pyasn1 0.6.4 - License: BSD-2-Clause @@ -1386,6 +1413,13 @@ The runtime dependencies of `agentenv-framework` and its `aws`, `gcp`, `sail`, ` - Source: - [License text 86](#license-text-86) +### xxhash 4.0.2 + +- License: BSD-2-Clause +- Copyright: Copyright (c) 2014-2026, Yue Du +- Source: +- [License text 122](#license-text-122) + ### yarl 1.25.1 - License: Apache-2.0 @@ -15281,3 +15315,2500 @@ OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ``` + +### License text 117 + +```text +Copyright (c) 2004-2016 California Institute of Technology. +Copyright (c) 2016-2026 The Uncertainty Quantification Foundation. +All rights reserved. + +This software is available subject to the conditions and terms laid +out below. By downloading and using this software you are agreeing +to the following conditions. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + - Neither the names of the copyright holders nor the names of any of + the contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### License text 118 + +```text +Copyright (c) 2008-2016 California Institute of Technology. +Copyright (c) 2016-2026 The Uncertainty Quantification Foundation. +All rights reserved. + +This software forks the python package "multiprocessing". Licence and +copyright information for multiprocessing can be found in "COPYING". + +This software is available subject to the conditions and terms laid +out below. By downloading and using this software you are agreeing +to the following conditions. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + - Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + - Neither the names of the copyright holders nor the names of any of + the contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +### License text 119 + +```text +Copyright (c) 2006-2008, R Oudkerk + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. +3. Neither the name of author nor the names of any contributors may be + used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. +``` + +### License text 120 + +```text + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +-------------------------------------------------------------------------------- + +src/arrow/util (some portions): Apache 2.0, and 3-clause BSD + +Some portions of this module are derived from code in the Chromium project, +copyright (c) Google inc and (c) The Chromium Authors and licensed under the +Apache 2.0 License or the under the 3-clause BSD license: + + Copyright (c) 2013 The Chromium Authors. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following disclaimer + in the documentation and/or other materials provided with the + distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +This project includes code from Daniel Lemire's FrameOfReference project. + +https://github.com/lemire/FrameOfReference/blob/6ccaf9e97160f9a3b299e23a8ef739e711ef0c71/src/bpacking.cpp +https://github.com/lemire/FrameOfReference/blob/146948b6058a976bc7767262ad3a2ce201486b93/scripts/turbopacking64.py + +Copyright: 2013 Daniel Lemire +Home page: http://lemire.me/en/ +Project page: https://github.com/lemire/FrameOfReference +License: Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 + +-------------------------------------------------------------------------------- + +This project includes code from the TensorFlow project + +Copyright 2015 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------------- + +This project includes code from the NumPy project. + +https://github.com/numpy/numpy/blob/e1f191c46f2eebd6cb892a4bfe14d9dd43a06c4e/numpy/core/src/multiarray/multiarraymodule.c#L2910 + +https://github.com/numpy/numpy/blob/68fd82271b9ea5a9e50d4e761061dfcca851382a/numpy/core/src/multiarray/datetime.c + +Copyright (c) 2005-2017, NumPy Developers. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the NumPy Developers nor the names of any + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +This project includes code from the Boost project + +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + +This project includes code from the FlatBuffers project + +Copyright 2014 Google Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------------- + +This project includes code from the tslib project + +Copyright 2015 Microsoft Corporation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------------- + +This project includes code from the jemalloc project + +https://github.com/jemalloc/jemalloc + +Copyright (C) 2002-2017 Jason Evans . +All rights reserved. +Copyright (C) 2007-2012 Mozilla Foundation. All rights reserved. +Copyright (C) 2009-2017 Facebook, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: +1. Redistributions of source code must retain the above copyright notice(s), + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice(s), + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) ``AS IS'' AND ANY EXPRESS +OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL THE COPYRIGHT HOLDER(S) BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-------------------------------------------------------------------------------- + +This project includes code from the Go project, BSD 3-clause license + PATENTS +weak patent termination clause +(https://github.com/golang/go/blob/master/PATENTS). + +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +This project includes code from the hs2client + +https://github.com/cloudera/hs2client + +Copyright 2016 Cloudera Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------------- + +The script r/configure has the following license (MIT) + +Copyright (c) 2017, Jeroen Ooms and Jim Hester + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- + +cpp/src/arrow/util/logging.cc, cpp/src/arrow/util/logging.h and +cpp/src/arrow/util/logging-test.cc are adapted from +Ray Project (https://github.com/ray-project/ray) (Apache 2.0). + +Copyright (c) 2016 Ray Project (https://github.com/ray-project/ray) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------------- +The files cpp/src/arrow/vendored/datetime/date.h, cpp/src/arrow/vendored/datetime/tz.h, +cpp/src/arrow/vendored/datetime/tz_private.h, cpp/src/arrow/vendored/datetime/ios.h, +cpp/src/arrow/vendored/datetime/ios.mm, +cpp/src/arrow/vendored/datetime/tz.cpp are adapted from +Howard Hinnant's date library (https://github.com/HowardHinnant/date) +It is licensed under MIT license. + +The MIT License (MIT) +Copyright (c) 2015, 2016, 2017 Howard Hinnant +Copyright (c) 2016 Adrian Colomitchi +Copyright (c) 2017 Florian Dang +Copyright (c) 2017 Paul Thompson +Copyright (c) 2018 Tomasz KamiƄski + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- + +The file cpp/src/arrow/util/utf8.h includes code adapted from the page + https://bjoern.hoehrmann.de/utf-8/decoder/dfa/ +with the following license (MIT) + +Copyright (c) 2008-2009 Bjoern Hoehrmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- + +The files in cpp/src/arrow/vendored/xxhash/ have the following license +(BSD 2-Clause License) + +xxHash Library +Copyright (c) 2012-2014, Yann Collet +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +You can contact the author at : +- xxHash homepage: http://www.xxhash.com +- xxHash source repository : https://github.com/Cyan4973/xxHash + +-------------------------------------------------------------------------------- + +The files in cpp/src/arrow/vendored/double-conversion/ have the following license +(BSD 3-Clause License) + +Copyright 2006-2011, the V8 project authors. All rights reserved. +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +The files under dev/tasks/conda-recipes have the following license + +BSD 3-clause license +Copyright (c) 2015-2018, conda-forge +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +The files in cpp/src/arrow/vendored/utfcpp/ have the following license + +Copyright 2006-2018 Nemanja Trifunovic + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + +This project includes code from Apache Kudu. + + * cpp/cmake_modules/CompilerInfo.cmake is based on Kudu's cmake_modules/CompilerInfo.cmake + +Copyright: 2016 The Apache Software Foundation. +Home page: https://kudu.apache.org/ +License: http://www.apache.org/licenses/LICENSE-2.0 + +-------------------------------------------------------------------------------- + +This project includes code from Apache Impala (incubating), formerly +Impala. The Impala code and rights were donated to the ASF as part of the +Incubator process after the initial code imports into Apache Parquet. + +Copyright: 2012 Cloudera, Inc. +Copyright: 2016 The Apache Software Foundation. +Home page: http://impala.apache.org/ +License: http://www.apache.org/licenses/LICENSE-2.0 + +-------------------------------------------------------------------------------- + +This project includes code from Apache Aurora. + +* dev/release/{release,changelog,release-candidate} are based on the scripts from + Apache Aurora + +Copyright: 2016 The Apache Software Foundation. +Home page: https://aurora.apache.org/ +License: http://www.apache.org/licenses/LICENSE-2.0 + +-------------------------------------------------------------------------------- + +This project includes code from Snappy. + +* cpp/cmake_modules/{SnappyCMakeLists.txt,SnappyConfig.h} are based on code + from Google's Snappy project. + +Copyright: 2009 Google Inc. All rights reserved. +Homepage: https://github.com/google/snappy +License: 3-clause BSD + +-------------------------------------------------------------------------------- + +This project includes code from the manylinux project. + +* python/manylinux1/scripts/{build_python.sh,python-tag-abi-tag.py, + requirements.txt} are based on code from the manylinux project. + +Copyright: 2016 manylinux +Homepage: https://github.com/pypa/manylinux +License: The MIT License (MIT) + +-------------------------------------------------------------------------------- + +This project includes code from the cymove project: + +* python/pyarrow/includes/common.pxd includes code from the cymove project + +The MIT License (MIT) +Copyright (c) 2019 Omer Ozarslan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + +The projects includes code from the Ursabot project under the dev/archery +directory. + +License: BSD 2-Clause + +Copyright 2019 RStudio, Inc. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +This project include code from mingw-w64. + +* cpp/src/arrow/util/cpu-info.cc has a polyfill for mingw-w64 < 5 + +Copyright (c) 2009 - 2013 by the mingw-w64 project +Homepage: https://mingw-w64.org +License: Zope Public License (ZPL) Version 2.1. + +--------------------------------------------------------------------------------- + +This project include code from Google's Asylo project. + +* cpp/src/arrow/result.h is based on status_or.h + +Copyright (c) Copyright 2017 Asylo authors +Homepage: https://asylo.dev/ +License: Apache 2.0 + +-------------------------------------------------------------------------------- + +This project includes code from Google's protobuf project + +* cpp/src/arrow/result.h ARROW_ASSIGN_OR_RAISE is based off ASSIGN_OR_RETURN +* cpp/src/arrow/util/bit_stream_utils.h contains code from wire_format_lite.h + +Copyright 2008 Google Inc. All rights reserved. +Homepage: https://developers.google.com/protocol-buffers/ +License: + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Code generated by the Protocol Buffer compiler is owned by the owner +of the input file used when generating it. This code is not +standalone and requires a support library to be linked with it. This +support library is itself covered by the above license. + +-------------------------------------------------------------------------------- + +3rdparty dependency LLVM is statically linked in certain binary distributions. +Additionally some sections of source code have been derived from sources in LLVM +and have been clearly labeled as such. LLVM has the following license: + +============================================================================== +The LLVM Project is under the Apache License v2.0 with LLVM Exceptions: +============================================================================== + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + +---- LLVM Exceptions to the Apache 2.0 License ---- + +As an exception, if, as a result of your compiling your source code, portions +of this Software are embedded into an Object form of such source code, you +may redistribute such embedded portions in such Object form without complying +with the conditions of Sections 4(a), 4(b) and 4(d) of the License. + +In addition, if you combine or link compiled forms of this Software with +software that is licensed under the GPLv2 ("Combined Software") and if a +court of competent jurisdiction determines that the patent provision (Section +3), the indemnity provision (Section 9) or other Section of the License +conflicts with the conditions of the GPLv2, you may retroactively and +prospectively choose to deem waived or otherwise exclude such Section(s) of +the License, but only in their entirety and only with respect to the Combined +Software. + +============================================================================== +Software from third parties included in the LLVM Project: +============================================================================== +The LLVM Project contains third party software which is under different license +terms. All such code will be identified clearly using at least one of two +mechanisms: +1) It will be in a separate directory tree with its own `LICENSE.txt` or + `LICENSE` file at the top containing the specific license and restrictions + which apply to that software, or +2) It will contain specific license and restriction terms at the top of every + file. + +-------------------------------------------------------------------------------- + +3rdparty dependency gRPC is statically linked in certain binary +distributions, like the python wheels. gRPC has the following license: + +Copyright 2014 gRPC authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------------- + +3rdparty dependency Apache Thrift is statically linked in certain binary +distributions, like the python wheels. Apache Thrift has the following license: + +Apache Thrift +Copyright (C) 2006 - 2019, The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------------- + +3rdparty dependency Apache ORC is statically linked in certain binary +distributions, like the python wheels. Apache ORC has the following license: + +Apache ORC +Copyright 2013-2019 The Apache Software Foundation + +This product includes software developed by The Apache Software +Foundation (http://www.apache.org/). + +This product includes software developed by Hewlett-Packard: +(c) Copyright [2014-2015] Hewlett-Packard Development Company, L.P + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------------- + +3rdparty dependency zstd is statically linked in certain binary +distributions, like the python wheels. ZSTD has the following license: + +BSD License + +For Zstandard software + +Copyright (c) 2016-present, Facebook, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + * Neither the name Facebook nor the names of its contributors may be used to + endorse or promote products derived from this software without specific + prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +3rdparty dependency lz4 is statically linked in certain binary +distributions, like the python wheels. lz4 has the following license: + +LZ4 Library +Copyright (c) 2011-2016, Yann Collet +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +3rdparty dependency Brotli is statically linked in certain binary +distributions, like the python wheels. Brotli has the following license: + +Copyright (c) 2009, 2010, 2013-2016 by the Brotli Authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +-------------------------------------------------------------------------------- + +3rdparty dependency rapidjson is statically linked in certain binary +distributions, like the python wheels. rapidjson and its dependencies have the +following licenses: + +Tencent is pleased to support the open source community by making RapidJSON +available. + +Copyright (C) 2015 THL A29 Limited, a Tencent company, and Milo Yip. +All rights reserved. + +If you have downloaded a copy of the RapidJSON binary from Tencent, please note +that the RapidJSON binary is licensed under the MIT License. +If you have downloaded a copy of the RapidJSON source code from Tencent, please +note that RapidJSON source code is licensed under the MIT License, except for +the third-party components listed below which are subject to different license +terms. Your integration of RapidJSON into your own projects may require +compliance with the MIT License, as well as the other licenses applicable to +the third-party components included within RapidJSON. To avoid the problematic +JSON license in your own projects, it's sufficient to exclude the +bin/jsonchecker/ directory, as it's the only code under the JSON license. +A copy of the MIT License is included in this file. + +Other dependencies and licenses: + + Open Source Software Licensed Under the BSD License: + -------------------------------------------------------------------- + + The msinttypes r29 + Copyright (c) 2006-2013 Alexander Chemeris + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR + ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH + DAMAGE. + + Terms of the MIT License: + -------------------------------------------------------------------- + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included + in all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + +3rdparty dependency snappy is statically linked in certain binary +distributions, like the python wheels. snappy has the following license: + +Copyright 2011, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of Google Inc. nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +=== + +Some of the benchmark data in testdata/ is licensed differently: + + - fireworks.jpeg is Copyright 2013 Steinar H. Gunderson, and + is licensed under the Creative Commons Attribution 3.0 license + (CC-BY-3.0). See https://creativecommons.org/licenses/by/3.0/ + for more information. + + - kppkn.gtb is taken from the Gaviota chess tablebase set, and + is licensed under the MIT License. See + https://sites.google.com/site/gaviotachessengine/Home/endgame-tablebases-1 + for more information. + + - paper-100k.pdf is an excerpt (bytes 92160 to 194560) from the paper + “Combinatorial Modeling of Chromatin Features Quantitatively Predicts DNA + Replication Timing in _Drosophila_” by Federico Comoglio and Renato Paro, + which is licensed under the CC-BY license. See + http://www.ploscompbiol.org/static/license for more ifnormation. + + - alice29.txt, asyoulik.txt, plrabn12.txt and lcet10.txt are from Project + Gutenberg. The first three have expired copyrights and are in the public + domain; the latter does not have expired copyright, but is still in the + public domain according to the license information + (http://www.gutenberg.org/ebooks/53). + +-------------------------------------------------------------------------------- + +3rdparty dependency gflags is statically linked in certain binary +distributions, like the python wheels. gflags has the following license: + +Copyright (c) 2006, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +3rdparty dependency glog is statically linked in certain binary +distributions, like the python wheels. glog has the following license: + +Copyright (c) 2008, Google Inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +A function gettimeofday in utilities.cc is based on + +http://www.google.com/codesearch/p?hl=en#dR3YEbitojA/COPYING&q=GetSystemTimeAsFileTime%20license:bsd + +The license of this code is: + +Copyright (c) 2003-2008, Jouni Malinen and contributors +All Rights Reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name(s) of the above-listed copyright holder(s) nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +3rdparty dependency re2 is statically linked in certain binary +distributions, like the python wheels. re2 has the following license: + +Copyright (c) 2009 The RE2 Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its contributors + may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +3rdparty dependency c-ares is statically linked in certain binary +distributions, like the python wheels. c-ares has the following license: + +# c-ares license + +Copyright (c) 2007 - 2018, Daniel Stenberg with many contributors, see AUTHORS +file. + +Copyright 1998 by the Massachusetts Institute of Technology. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, provided that +the above copyright notice appear in all copies and that both that copyright +notice and this permission notice appear in supporting documentation, and that +the name of M.I.T. not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior permission. +M.I.T. makes no representations about the suitability of this software for any +purpose. It is provided "as is" without express or implied warranty. + +-------------------------------------------------------------------------------- + +3rdparty dependency zlib is redistributed as a dynamically linked shared +library in certain binary distributions, like the python wheels. In the future +this will likely change to static linkage. zlib has the following license: + +zlib.h -- interface of the 'zlib' general purpose compression library + version 1.2.11, January 15th, 2017 + + Copyright (C) 1995-2017 Jean-loup Gailly and Mark Adler + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + +-------------------------------------------------------------------------------- + +3rdparty dependency openssl is redistributed as a dynamically linked shared +library in certain binary distributions, like the python wheels. openssl +preceding version 3 has the following license: + + LICENSE ISSUES + ============== + + The OpenSSL toolkit stays under a double license, i.e. both the conditions of + the OpenSSL License and the original SSLeay license apply to the toolkit. + See below for the actual license texts. + + OpenSSL License + --------------- + +/* ==================================================================== + * Copyright (c) 1998-2019 The OpenSSL Project. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. All advertising materials mentioning features or use of this + * software must display the following acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit. (http://www.openssl.org/)" + * + * 4. The names "OpenSSL Toolkit" and "OpenSSL Project" must not be used to + * endorse or promote products derived from this software without + * prior written permission. For written permission, please contact + * openssl-core@openssl.org. + * + * 5. Products derived from this software may not be called "OpenSSL" + * nor may "OpenSSL" appear in their names without prior written + * permission of the OpenSSL Project. + * + * 6. Redistributions of any form whatsoever must retain the following + * acknowledgment: + * "This product includes software developed by the OpenSSL Project + * for use in the OpenSSL Toolkit (http://www.openssl.org/)" + * + * THIS SOFTWARE IS PROVIDED BY THE OpenSSL PROJECT ``AS IS'' AND ANY + * EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE OpenSSL PROJECT OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED + * OF THE POSSIBILITY OF SUCH DAMAGE. + * ==================================================================== + * + * This product includes cryptographic software written by Eric Young + * (eay@cryptsoft.com). This product includes software written by Tim + * Hudson (tjh@cryptsoft.com). + * + */ + + Original SSLeay License + ----------------------- + +/* Copyright (C) 1995-1998 Eric Young (eay@cryptsoft.com) + * All rights reserved. + * + * This package is an SSL implementation written + * by Eric Young (eay@cryptsoft.com). + * The implementation was written so as to conform with Netscapes SSL. + * + * This library is free for commercial and non-commercial use as long as + * the following conditions are aheared to. The following conditions + * apply to all code found in this distribution, be it the RC4, RSA, + * lhash, DES, etc., code; not just the SSL code. The SSL documentation + * included with this distribution is covered by the same copyright terms + * except that the holder is Tim Hudson (tjh@cryptsoft.com). + * + * Copyright remains Eric Young's, and as such any Copyright notices in + * the code are not to be removed. + * If this package is used in a product, Eric Young should be given attribution + * as the author of the parts of the library used. + * This can be in the form of a textual message at program startup or + * in documentation (online or textual) provided with the package. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 3. All advertising materials mentioning features or use of this software + * must display the following acknowledgement: + * "This product includes cryptographic software written by + * Eric Young (eay@cryptsoft.com)" + * The word 'cryptographic' can be left out if the rouines from the library + * being used are not cryptographic related :-). + * 4. If you include any Windows specific code (or a derivative thereof) from + * the apps directory (application code) you must include an acknowledgement: + * "This product includes software written by Tim Hudson (tjh@cryptsoft.com)" + * + * THIS SOFTWARE IS PROVIDED BY ERIC YOUNG ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + * The licence and distribution terms for any publically available version or + * derivative of this code cannot be changed. i.e. this code cannot simply be + * copied and put under another distribution licence + * [including the GNU Public Licence.] + */ + +-------------------------------------------------------------------------------- + +This project includes code from the rtools-backports project. + +* ci/scripts/PKGBUILD and ci/scripts/r_windows_build.sh are based on code + from the rtools-backports project. + +Copyright: Copyright (c) 2013 - 2019, АлДĐșсДĐč and Jeroen Ooms. +All rights reserved. +Homepage: https://github.com/r-windows/rtools-backports +License: 3-clause BSD + +-------------------------------------------------------------------------------- + +Some code from pandas has been adapted for the pyarrow codebase. pandas is +available under the 3-clause BSD license, which follows: + +pandas license +============== + +Copyright (c) 2011-2012, Lambda Foundry, Inc. and PyData Development Team +All rights reserved. + +Copyright (c) 2008-2011 AQR Capital Management, LLC +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of the copyright holder nor the names of any + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +Some bits from DyND, in particular aspects of the build system, have been +adapted from libdynd and dynd-python under the terms of the BSD 2-clause +license + +The BSD 2-Clause License + + Copyright (C) 2011-12, Dynamic NDArray Developers + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Dynamic NDArray Developers list: + + * Mark Wiebe + * Continuum Analytics + +-------------------------------------------------------------------------------- + +Some source code from Ibis (https://github.com/cloudera/ibis) has been adapted +for PyArrow. Ibis is released under the Apache License, Version 2.0. + +-------------------------------------------------------------------------------- + +dev/tasks/homebrew-formulae/apache-arrow.rb has the following license: + +BSD 2-Clause License + +Copyright (c) 2009-present, Homebrew contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +---------------------------------------------------------------------- + +cpp/src/arrow/vendored/base64.cpp has the following license + +ZLIB License + +Copyright (C) 2004-2017 RenĂ© Nyffenegger + +This source code is provided 'as-is', without any express or implied +warranty. In no event will the author be held liable for any damages arising +from the use of this software. + +Permission is granted to anyone to use this software for any purpose, including +commercial applications, and to alter it and redistribute it freely, subject to +the following restrictions: + +1. The origin of this source code must not be misrepresented; you must not + claim that you wrote the original source code. If you use this source code + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + +2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original source code. + +3. This notice may not be removed or altered from any source distribution. + +RenĂ© Nyffenegger rene.nyffenegger@adp-gmbh.ch + +-------------------------------------------------------------------------------- + +This project includes code from Folly. + + * cpp/src/arrow/vendored/ProducerConsumerQueue.h + +is based on Folly's + + * folly/Portability.h + * folly/lang/Align.h + * folly/ProducerConsumerQueue.h + +Copyright: Copyright (c) Facebook, Inc. and its affiliates. +Home page: https://github.com/facebook/folly +License: http://www.apache.org/licenses/LICENSE-2.0 + +-------------------------------------------------------------------------------- + +The file cpp/src/arrow/vendored/musl/strptime.c has the following license + +Copyright © 2005-2020 Rich Felker, et al. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + +The file cpp/cmake_modules/BuildUtils.cmake contains code from + +https://gist.github.com/cristianadam/ef920342939a89fae3e8a85ca9459b49 + +which is made available under the MIT license + +Copyright (c) 2019 Cristian Adam + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- + +The files in cpp/src/arrow/vendored/portable-snippets/ contain code from + +https://github.com/nemequ/portable-snippets + +and have the following copyright notice: + +Each source file contains a preamble explaining the license situation +for that file, which takes priority over this file. With the +exception of some code pulled in from other repositories (such as +”nit, an MIT-licensed project which is used for testing), the code is +public domain, released using the CC0 1.0 Universal dedication (*). + +(*) https://creativecommons.org/publicdomain/zero/1.0/legalcode + +-------------------------------------------------------------------------------- + +The files in cpp/src/arrow/vendored/fast_float/ contain code from + +https://github.com/lemire/fast_float + +which is made available under the Apache License 2.0. + +-------------------------------------------------------------------------------- + +The file python/pyarrow/vendored/docscrape.py contains code from + +https://github.com/numpy/numpydoc/ + +which is made available under the BSD 2-clause license. + +-------------------------------------------------------------------------------- + +The file python/pyarrow/vendored/version.py contains code from + +https://github.com/pypa/packaging/ + +which is made available under both the Apache license v2.0 and the +BSD 2-clause license. + +-------------------------------------------------------------------------------- + +The files in cpp/src/arrow/vendored/pcg contain code from + +https://github.com/imneme/pcg-cpp + +and have the following copyright notice: + +Copyright 2014-2019 Melissa O'Neill , + and the PCG Project contributors. + +SPDX-License-Identifier: (Apache-2.0 OR MIT) + +Licensed under the Apache License, Version 2.0 (provided in +LICENSE-APACHE.txt and at http://www.apache.org/licenses/LICENSE-2.0) +or under the MIT license (provided in LICENSE-MIT.txt and at +http://opensource.org/licenses/MIT), at your option. This file may not +be copied, modified, or distributed except according to those terms. + +Distributed on an "AS IS" BASIS, WITHOUT WARRANTY OF ANY KIND, either +express or implied. See your chosen license for details. + +-------------------------------------------------------------------------------- +r/R/dplyr-count-tally.R (some portions) + +Some portions of this file are derived from code from + +https://github.com/tidyverse/dplyr/ + +which is made available under the MIT license + +Copyright (c) 2013-2019 RStudio and others. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- + +The file src/arrow/util/io_util.cc contains code from the CPython project +which is made available under the Python Software Foundation License Version 2. + +-------------------------------------------------------------------------------- + +3rdparty dependency opentelemetry-cpp is statically linked in certain binary +distributions. opentelemetry-cpp is made available under the Apache License 2.0. + +Copyright The OpenTelemetry Authors +SPDX-License-Identifier: Apache-2.0 + +-------------------------------------------------------------------------------- + +3rdparty dependency UCX is redistributed as a dynamically linked shared +library in certain binary distributions. UCX has the following license: + +Copyright (c) 2014-2015 UT-Battelle, LLC. All rights reserved. +Copyright (C) 2014-2020 Mellanox Technologies Ltd. All rights reserved. +Copyright (C) 2014-2015 The University of Houston System. All rights reserved. +Copyright (C) 2015 The University of Tennessee and The University + of Tennessee Research Foundation. All rights reserved. +Copyright (C) 2016-2020 ARM Ltd. All rights reserved. +Copyright (c) 2016 Los Alamos National Security, LLC. All rights reserved. +Copyright (C) 2016-2020 Advanced Micro Devices, Inc. All rights reserved. +Copyright (C) 2019 UChicago Argonne, LLC. All rights reserved. +Copyright (c) 2018-2020 NVIDIA CORPORATION. All rights reserved. +Copyright (C) 2020 Huawei Technologies Co., Ltd. All rights reserved. +Copyright (C) 2016-2020 Stony Brook University. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. +3. Neither the name of the copyright holder nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------------- + +The file dev/tasks/r/github.packages.yml contains code from + +https://github.com/ursa-labs/arrow-r-nightly + +which is made available under the Apache License 2.0. + +-------------------------------------------------------------------------------- +.github/actions/sync-nightlies/action.yml (some portions) + +Some portions of this file are derived from code from + +https://github.com/JoshPiper/rsync-docker + +which is made available under the MIT license + +Copyright (c) 2020 Joshua Piper + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- +.github/actions/sync-nightlies/action.yml (some portions) + +Some portions of this file are derived from code from + +https://github.com/burnett01/rsync-deployments + +which is made available under the MIT license + +Copyright (c) 2019-2022 Contention +Copyright (c) 2019-2022 Burnett01 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------------- +java/vector/src/main/java/org/apache/arrow/vector/util/IntObjectHashMap.java +java/vector/src/main/java/org/apache/arrow/vector/util/IntObjectMap.java + +These files are derived from code from Netty, which is made available under the +Apache License 2.0. + +-------------------------------------------------------------------------------- +cpp/src/arrow/util/math_internal.cc (some portions) + +Some portions of this file are derived from + +https://github.com/ankane/dist-rust/ + +which is made available under the MIT license + +The MIT License (MIT) + +Copyright (c) 2021-2023 Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +-------------------------------------------------------------------------------- +The files cpp/src/arrow/vendored/whereami/whereami.h, +cpp/src/arrow/vendored/whereami/whereami.cc are adapted from +GrĂ©gory Pakosz's whereami library (https://github.com/gpakosz/whereami) +It is dual licensed under both the WTFPLv2 and MIT licenses. + +The WTFPLv2 License + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + 1. Bla bla bla + 2. Montesqieu et camembert, vive la France, zut alors! + +The MIT License (MIT) +Copyright Gregory Pakosz + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +-------------------------------------------------------------------------------- + +The files in cpp/src/arrow/vendored/safeint/ contain code from + +https://github.com/dcleblanc/SafeInt + +and are made available under the MIT license. + +MIT License + +Copyright (c) 2018 Microsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +### License text 121 + +```text +Apache Arrow +Copyright 2016-2026 The Apache Software Foundation + +This product includes software developed at +The Apache Software Foundation (http://www.apache.org/). + +This product includes software from the SFrame project (BSD, 3-clause). +* Copyright (C) 2015 Dato, Inc. +* Copyright (c) 2009 Carnegie Mellon University. + +This product includes software from the Feather project (Apache 2.0) +https://github.com/wesm/feather + +This product includes software from the DyND project (BSD 2-clause) +https://github.com/libdynd + +This product includes software from the LLVM project + * distributed under the University of Illinois Open Source + +This product includes software from the mman-win32 project + * Copyright https://code.google.com/p/mman-win32/ + * Licensed under the MIT License; + +This product includes software from the LevelDB project + * Copyright (c) 2011 The LevelDB Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * Moved from Kudu http://github.com/cloudera/kudu + +This product includes software from the CMake project + * Copyright 2001-2009 Kitware, Inc. + * Copyright 2012-2014 Continuum Analytics, Inc. + * All rights reserved. + +This product includes software from https://github.com/matthew-brett/multibuild (BSD 2-clause) + * Copyright (c) 2013-2016, Matt Terry and Matthew Brett; all rights reserved. + +This product includes software from the Ibis project (Apache 2.0) + * Copyright (c) 2015 Cloudera, Inc. + * https://github.com/cloudera/ibis + +This product includes software from Dremio (Apache 2.0) + * Copyright (C) 2017-2018 Dremio Corporation + * https://github.com/dremio/dremio-oss + +This product includes software from Google Guava (Apache 2.0) + * Copyright (C) 2007 The Guava Authors + * https://github.com/google/guava + +This product include software from CMake (BSD 3-Clause) + * CMake - Cross Platform Makefile Generator + * Copyright 2000-2019 Kitware, Inc. and Contributors + +The web site includes files generated by Jekyll. + +-------------------------------------------------------------------------------- + +This product includes code from Apache Kudu, which includes the following in +its NOTICE file: + + Apache Kudu + Copyright 2016 The Apache Software Foundation + + This product includes software developed at + The Apache Software Foundation (http://www.apache.org/). + + Portions of this software were developed at + Cloudera, Inc (http://www.cloudera.com/). + +-------------------------------------------------------------------------------- + +This product includes code from Apache ORC, which includes the following in +its NOTICE file: + + Apache ORC + Copyright 2013-2019 The Apache Software Foundation + + This product includes software developed by The Apache Software + Foundation (http://www.apache.org/). + + This product includes software developed by Hewlett-Packard: + (c) Copyright [2014-2015] Hewlett-Packard Development Company, L.P +``` + +### License text 122 + +```text +Copyright (c) 2014-2026, Yue Du +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` diff --git a/pyproject.toml b/pyproject.toml index ced44297e..570346da5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,11 @@ dependencies = [ openenv = [ "openenv>=0.9", ] +# agent_env.training.trl: TRL's trainers on a bundle served as an OpenEnv env. TRL itself is the trainer's to pin. +trl = [ + "agentenv-framework[openenv]", + "datasets>=3", +] # The AWS store backends (S3, Secrets Manager, DynamoDB, ECR), selected by impl pointer in config.toml. aws = [ "boto3>=1.35.0", @@ -93,7 +98,7 @@ daytona = [ "daytona~=0.224.0", # AsyncDaytona sandboxes, sessions, file API, preview links and egress lists validated against 0.224 ] dev = [ - "agentenv-framework[aws,boxd,daytona,gcp,openenv,sail,tensorlake,tenki,vercel]", # exercise portable optional providers and stores in CI + "agentenv-framework[aws,boxd,daytona,gcp,openenv,sail,tensorlake,tenki,trl,vercel]", # exercise portable optional providers and stores in CI "fastmcp>=3.4,<4", # the standalone-fastmcp mount path of the protocol; <4 keeps mcp 1.x "Pillow>=10.4", # the protocol's computer extra, whose tests the unit tier runs "av>=17", # the protocol's computer extra, whose tests the unit tier runs and which the recording tests decode with diff --git a/src/agent_env/openenv/app.py b/src/agent_env/openenv/app.py index 1308da3a2..712153074 100644 --- a/src/agent_env/openenv/app.py +++ b/src/agent_env/openenv/app.py @@ -45,9 +45,14 @@ def build_app( with namespace_routing(): prepared = {split: _prepared(source, sandbox) for split, source in _sources(bundle).items()} served = Served(splits=_served(_splits(bundle, prepared), reward), sandbox=sandbox) + return _app(served, _env_name(prepared), max_concurrent_envs) + + +def _app(served: Served, env_name: str, max_concurrent_envs: int) -> FastAPI: + """OpenEnv's server for what ``served`` serves, with namespace routing on from its startup to its shutdown, which + tears down every open episode.""" app = create_app(functools.partial(AgentEnvEnvironment, served), CallToolAction, CallToolObservation, - env_name=_env_name(prepared), max_concurrent_envs=max_concurrent_envs, - reset_observation_cls=Observation) + env_name=env_name, max_concurrent_envs=max_concurrent_envs, reset_observation_cls=Observation) routing = ExitStack() async def route() -> None: diff --git a/src/agent_env/openenv/environment.py b/src/agent_env/openenv/environment.py index e1021a9ee..4cff30857 100644 --- a/src/agent_env/openenv/environment.py +++ b/src/agent_env/openenv/environment.py @@ -27,6 +27,7 @@ from agent_env.task import EpisodeError, EpisodeSpec, Task from agent_env.task.episode import Episode, Turn +from agent_env.task.episode.rules import DEPLOYS, works_in_sandbox from agent_env.task.episode.run import TurnTools END_TURN = Tool( @@ -47,6 +48,15 @@ class ServedTask: spec: EpisodeSpec reward: Callable[[dict[str, float]], float | None] + @property + def toolset(self) -> dict[str, Any]: + """Where the played agent's tools come from: the envs it works in, its role in them, and whether its turns offer + ``bash`` in its sandbox. Tasks that agree on it offer the same tools, so a trainer can group them.""" + agent = self.spec.trainer_agents[0] + [deploy] = [step for step in self.task.steps if step.type in DEPLOYS and step.agent_name == agent] + return {"envs": list(getattr(deploy, "env_ids", None) or []), "role": getattr(deploy, "role", None), + "bash": works_in_sandbox(self.task.steps, agent)} + @dataclass class Served: @@ -301,7 +311,7 @@ def _describe(self, split: str, index: int) -> dict[str, Any]: served = self._task_at(split, index) prompt, system_prompt = served.spec.prompts[0] return {"id": served.task.id, "name": served.name, "index": index, "prompt": prompt, - "system_prompt": system_prompt, "agent": served.spec.trainer_agents[0]} + "system_prompt": system_prompt, "agent": served.spec.trainer_agents[0], "toolset": served.toolset} def _task_metadata(self) -> dict[str, Any] | None: if self._served_task is None: diff --git a/src/agent_env/training/__init__.py b/src/agent_env/training/__init__.py new file mode 100644 index 000000000..403faa0fd --- /dev/null +++ b/src/agent_env/training/__init__.py @@ -0,0 +1,2 @@ +"""Training on AgentEnv tasks with other projects' trainers, one module per trainer, each behind an extra of its own: +``agent_env.training.trl`` for TRL.""" diff --git a/src/agent_env/training/trl.py b/src/agent_env/training/trl.py new file mode 100644 index 000000000..5bec0af55 --- /dev/null +++ b/src/agent_env/training/trl.py @@ -0,0 +1,566 @@ +"""TRL's trainers on a bundle served as an OpenEnv env (``agent_env.openenv.build_app``, or the package ``openenv import`` +writes): ``AgentEnv`` reads the server's tasks into a dataset, and makes the ``environment_factory`` whose environments +each play one episode of a task over OpenEnv's WebSocket, offer its turn's tools as methods and own its reward. +``GRPOTrainer`` and AsyncGRPO take both as they are. Every session runs on one event-loop thread of this module's own, +since TRL calls an environment synchronously, from its own threads.""" + +from __future__ import annotations + +import asyncio +import contextlib +import inspect +import json +import keyword +import logging +import operator +import re +import threading +import time +import weakref +from collections.abc import Callable, Coroutine, Mapping +from dataclasses import dataclass +from functools import cached_property, reduce +from pathlib import Path +from typing import Any, ClassVar, Literal + +import httpx +import uvicorn + +try: + from datasets import Dataset + from openenv.core.generic_client import GenericEnvClient +except ModuleNotFoundError as exc: + if exc.name not in ("datasets", "openenv"): + raise + raise ImportError("agent_env.training.trl requires pip install 'agentenv-framework[trl]'") from exc + +from agent_env.openenv import build_app + +logger = logging.getLogger(__name__) + +FAILED = "The environment failed to answer, so this episode won't be scored." +ENDED = "The episode has ended." +END_TURN = "end_turn" +CONNECT_SECONDS = 60.0 +CLOSE_SECONDS = 300.0 + +_CAPACITY = re.compile(r"Server at capacity: \d+/(\d+)") +_TYPES: dict[str, Any] = {"string": str, "integer": int, "number": float, "boolean": bool, "object": dict, + "null": type(None)} +_RESERVED = {"reset", "get_reward"} + +_loop: asyncio.AbstractEventLoop | None = None +_loop_lock = threading.Lock() +_environments: weakref.WeakSet[_Environment] = weakref.WeakSet() +_servers: set[_Server] = set() + + +class AgentEnv: + """A server's tasks for TRL. ``dataset`` holds one row per task of ``split``, and ``factory`` is the + ``environment_factory`` whose environments play them: each holds one session on the server, ``reset`` starts the + row's task, its methods call the turn's tools, ``end_turn`` hands over the reply and answers with the next turn's + prompt, and ``get_reward`` is the episode's reward, ``None`` when it ended in an error. A task's turn not ended by + the model ends with an empty reply. Tasks whose agents get different tools take TRL's multi-environment mode: + ``factory`` is then a dict, and each row names its ``environment``. + + Args: + url: The server's base URL. + split: The split ``dataset`` holds. + tool_timeout: One tool call's deadline in seconds. A call that misses it ends its episode unscored. + reset_timeout: How long an episode may take to deploy and open its first turn, and each turn to be ended, + in seconds. + + Example: + + ```python + env = AgentEnv("http://localhost:8000") + trainer = GRPOTrainer( + model="Qwen/Qwen3-4B", + args=GRPOConfig(max_tool_calling_iterations=12), + train_dataset=env.dataset, + environment_factory=env.factory, + ) + ``` + """ + + def __init__(self, url: str, split: str = "all", *, tool_timeout: float = 60.0, reset_timeout: float = 600.0): + self.url = url.rstrip("/") + self.split = split + self._tool_timeout, self._reset_timeout = tool_timeout, reset_timeout + self._server: _Server | None = None + self._shared: list[_Shared] = [] + self._name, self._tasks = _read_tasks(self.url) + self._toolsets = _toolsets(self._name, self._tasks) + self._split(split) + + @classmethod + def local(cls, bundle: str | Path | Mapping[str, str | Path], split: str = "all", *, + reward: str | Callable[[dict[str, float]], float] | None = None, sandbox: str | None = None, + max_concurrent_envs: int = 8, tool_timeout: float = 60.0, reset_timeout: float = 600.0) -> AgentEnv: + """``bundle`` served by ``build_app`` in this process on a loopback port, for a trainer on a machine that can + deploy its envs. ``max_concurrent_envs`` must cover the rollouts the trainer generates at once. The server + stops at ``close``, or when the process exits, ending its episodes.""" + server = _Server(build_app(bundle, reward=reward, sandbox=sandbox, max_concurrent_envs=max_concurrent_envs)) + try: + env = cls(server.url, split, tool_timeout=tool_timeout, reset_timeout=reset_timeout) + except BaseException: + server.stop() + raise + env._server = server + return env + + @cached_property + def dataset(self) -> Dataset: + return self.dataset_for(self.split) + + def dataset_for(self, split: str) -> Dataset: + """``split``'s tasks as TRL's rows: the ``prompt`` as chat messages, and the ``split`` and ``index`` that + ``reset`` plays, with the ``environment`` in multi-environment mode.""" + several = len(self._toolsets) > 1 + return Dataset.from_list([{ + "prompt": _messages(task["system_prompt"], task["prompt"]), "split": split, "index": task["index"], + **({"environment": self._toolsets[_key(task)].name} if several else {}), + } for task in self._split(split)]) + + @cached_property + def factory(self) -> type[_Environment] | dict[str, type[_Environment]]: + """TRL's ``environment_factory``: an environment class per toolset, or the one class when every task has the + same. Building it plays one task of each toolset to list its tools, since TRL reads them from the class.""" + classes = on_loop(_gathered(self._environment(toolset) for toolset in self._toolsets.values()), + self._reset_timeout + CLOSE_SECONDS) + return classes[0] if len(classes) == 1 else {each.__name__: each for each in classes} + + def close(self) -> None: + """Ends the episodes this env's environments hold, closing their sessions, and stops the server ``local`` + started.""" + mine = [environment for environment in list(_environments) if environment._shared in self._shared] + try: + on_loop(_gathered(environment._close() for environment in mine), CLOSE_SECONDS) + finally: + if self._server is not None: + self._server.stop() + + async def _environment(self, toolset: _Toolset) -> type[_Environment]: + split, index = toolset.first + client = await _opened(self.url, self._reset_timeout) + try: + reset = await client.reset(split=split, index=index) + listed = reset if reset.done else await client.step({"type": "list_tools"}) + finally: + with contextlib.suppress(Exception): + await client.close() + if listed.done: + raise RuntimeError(f"task {index} of split {split!r} ended before its tools could be listed: " + f"{_error_of(listed.observation)}") + methods = {tool["name"]: tool_method(tool) for tool in listed.observation["tools"]} + shared = _Shared(url=self.url, offered=frozenset(methods), tool_timeout=self._tool_timeout, + reset_timeout=self._reset_timeout) + self._shared.append(shared) + environment = type(toolset.name, (_Environment,), {**methods, "_shared": shared}) + for method in methods.values(): + method.__qualname__ = f"{toolset.name}.{method.__name__}" + return environment + + def _split(self, split: str) -> list[dict[str, Any]]: + if split not in self._tasks: + raise ValueError(f"the server at {self.url} has no split {split!r}; it serves {', '.join(self._tasks)}") + return self._tasks[split] + + +@dataclass(frozen=True) +class _Toolset: + name: str + first: tuple[str, int] + + +@dataclass(frozen=True, eq=False) +class _Shared: + """What an environment class's instances share: the server, the tools every episode must offer, and the + deadlines.""" + + url: str + offered: frozenset[str] + tool_timeout: float + reset_timeout: float + + +class _Failed(Exception): + pass + + +@dataclass +class _Rollout: + started: asyncio.Task[None] + done: bool = False + reward: float | None = None + failure: str | None = None + + +class _Environment: + """One session on the server, playing one episode at a time. Its public methods other than ``reset`` and + ``get_reward`` are the tools, which ``AgentEnv.factory`` binds per toolset; every call on the session waits its + turn, as AsyncGRPO can call two tools of a completion at once.""" + + _shared: ClassVar[_Shared] + + def __init__(self) -> None: + self._client: GenericEnvClient | None = None + self._rollout: _Rollout | None = None + self._lock = asyncio.Lock() + _environments.add(self) + + def reset(self, *, split: str, index: int, prompt: list[dict[str, Any]] | None = None, **_: Any) -> None: + """Starts the row's episode and returns before it is deployed, since ``GRPOTrainer`` resets its environments + one at a time: the first tool call or ``get_reward`` waits for it. Raises when the server refuses the session, + as one at capacity does.""" + on_loop(self._reset(split, index, _prompt_of(prompt)), CONNECT_SECONDS + CLOSE_SECONDS) + + def get_reward(self) -> float | None: + rollout, self._rollout = self._rollout, None + if rollout is None: + return None + try: + on_loop(self._finish(rollout), None) + except Exception as e: + rollout.failure = rollout.failure or _why(e) + if rollout.failure is not None: + logger.warning("rollout left unscored: %s", rollout.failure) + return None + return rollout.reward + + def _call(self, name: str, arguments: dict[str, Any]) -> str: + return on_loop(self._step(name, arguments), + self._shared.reset_timeout + self._shared.tool_timeout + CLOSE_SECONDS) + + async def _reset(self, split: str, index: int, expected: tuple[str | None, str] | None) -> None: + async with self._lock: + if (rollout := self._rollout) is not None: + rollout.started.cancel() + await asyncio.wait([rollout.started]) + self._rollout = None + if self._client is None: + self._client = await _opened(self._shared.url, self._shared.reset_timeout) + self._rollout = _Rollout(asyncio.ensure_future(self._begin(split, index, expected))) + + async def _begin(self, split: str, index: int, expected: tuple[str | None, str] | None) -> None: + reset = await self._client.reset(split=split, index=index) + if reset.done: + raise _Failed(_error_of(reset.observation)) + metadata = reset.observation.get("metadata") or {} + if expected is not None and (metadata.get("system_prompt") or None, metadata.get("prompt")) != expected: + raise _Failed("the turn's prompt differs from the dataset row's") + listed = await self._client.step({"type": "list_tools"}) + if listed.done: + raise _Failed(_error_of(listed.observation)) + if (offered := {tool["name"] for tool in listed.observation["tools"]}) != self._shared.offered: + raise _Failed(f"the episode offers the tools {sorted(offered)}, not {sorted(self._shared.offered)}") + + async def _step(self, name: str, arguments: dict[str, Any]) -> str: + async with self._lock: + rollout = self._rollout + if rollout is None or rollout.failure is not None: + return FAILED + if rollout.done: + return ENDED + try: + await rollout.started + result = await asyncio.wait_for(self._client.step( + {"type": "call_tool", "tool_name": name, "arguments": arguments}), self._shared.tool_timeout) + except _Failed as e: + await self._fail(rollout, str(e)) + return FAILED + except TimeoutError: + await self._fail(rollout, f"{name} didn't answer within {self._shared.tool_timeout:g}s") + return FAILED + except Exception as e: + await self._fail(rollout, f"{name}: {_why(e)}") + return FAILED + return self._answer(rollout, name, result) + + async def _finish(self, rollout: _Rollout) -> None: + """Ends the turns the model left open with an empty reply, until the episode is scored; each call on the + session has the reset's deadline.""" + async with self._lock: + try: + await rollout.started + while not (rollout.done or rollout.failure): + result = await self._client.step( + {"type": "call_tool", "tool_name": END_TURN, "arguments": {"response": ""}}) + if error := result.observation.get("error"): + rollout.failure = f"{END_TURN}: {error.get('message')}" + self._answer(rollout, END_TURN, result) + except Exception as e: + await self._fail(rollout, str(e) if isinstance(e, _Failed) else _why(e)) + + def _answer(self, rollout: _Rollout, name: str, result: Any) -> str: + """What the model sees of a tool call: the tool's text or error, the next turn's prompt for ``end_turn``, + or that the episode ended, which records its reward or why it has none.""" + observation = result.observation + if error := observation.get("error"): + return error.get("message") or json.dumps(error) + if result.done: + rollout.done = True + if (observation.get("metadata") or {}).get("status") == "error": + rollout.failure = _error_of(observation) + return FAILED + rollout.reward = result.reward + return ENDED + answer = observation.get("result") or {} + if name == END_TURN: + return answer.get("prompt") or "" + return answer["data"] if isinstance(answer.get("data"), str) else json.dumps(answer) + + async def _fail(self, rollout: _Rollout, why: str) -> None: + """Records why the rollout won't be scored and drops the session, which may be out of step with the server; the + server ends a dropped session's episode, and the next reset opens another.""" + rollout.failure = rollout.failure or why + client, self._client = self._client, None + if client is not None: + with contextlib.suppress(Exception): + await client.close() + + async def _close(self) -> None: + rollout, self._rollout = self._rollout, None + if rollout is not None: + rollout.started.cancel() + client, self._client = self._client, None + if client is not None: + with contextlib.suppress(Exception): + await client.close() + + +class _Server: + """An app under uvicorn on a thread of its own, its lifespan included, on a free loopback port.""" + + def __init__(self, app: Any): + self._uvicorn = uvicorn.Server(uvicorn.Config(app, host="127.0.0.1", port=0, log_level="warning")) + self._thread = threading.Thread(target=self._uvicorn.run, name="agentenv-openenv-server", daemon=True) + self._thread.start() + while not self._uvicorn.started: + if not self._thread.is_alive(): + raise RuntimeError("the bundle's server didn't start; its log says why") + time.sleep(0.05) + self.url = f"http://127.0.0.1:{self._uvicorn.servers[0].sockets[0].getsockname()[1]}" + _servers.add(self) + + def stop(self) -> None: + _servers.discard(self) + self._uvicorn.should_exit = True + self._thread.join(CLOSE_SECONDS) + + +def episode_loop() -> asyncio.AbstractEventLoop: + global _loop + with _loop_lock: + if _loop is None: + _loop = asyncio.new_event_loop() + threading.Thread(target=_loop.run_forever, name="agentenv-trl-sessions", daemon=True).start() + return _loop + + +def on_loop(coroutine: Coroutine[Any, Any, Any], timeout: float | None) -> Any: + future = asyncio.run_coroutine_threadsafe(coroutine, episode_loop()) + try: + return future.result(timeout) + except TimeoutError: + future.cancel() + raise + + +async def _gathered(coroutines: Any) -> list[Any]: + return list(await asyncio.gather(*coroutines)) + + +def _close_all() -> None: + """A trainer that stops or crashes ends the episodes it left running, since TRL never closes its environments, + then stops the servers ``AgentEnv.local`` started. It runs before threading's shutdown, while the thread pools an + episode's teardown uses still take work.""" + try: + on_loop(_gathered(environment._close() for environment in list(_environments)), CLOSE_SECONDS) + except Exception as e: + logger.warning("couldn't end the episodes left running: %s", _why(e)) + for server in list(_servers): + server.stop() + + +threading._register_atexit(_close_all) + + +async def _opened(url: str, timeout: float) -> GenericEnvClient: + """A session on the server, opened now rather than at the first reset, so a server at capacity refuses it while + the trainer sets up.""" + client = GenericEnvClient(base_url=url, message_timeout_s=timeout) + try: + await client.state() + except Exception as e: + with contextlib.suppress(Exception): + await client.close() + if match := _CAPACITY.search(str(e)): + raise RuntimeError( + f"the server at {url} is at its limit of {match[1]} sessions at once, and the trainer holds one per " + "rollout it generates at once: serve the bundle with MAX_CONCURRENT_ENVS (or max_concurrent_envs=) " + "at least that many, or generate fewer rollouts at once") from e + raise ConnectionError(f"couldn't open a session on the server at {url}: {_why(e)}") from e + return client + + +def _read_tasks(url: str) -> tuple[str, dict[str, list[dict[str, Any]]]]: + """The server's env name, and each split's tasks as its Task API describes them.""" + with httpx.Client(base_url=url, timeout=CONNECT_SECONDS) as http: + try: + [name] = http.get("/list_environments").raise_for_status().json() + except httpx.TransportError as e: + raise ConnectionError(f"no OpenEnv server answers at {url}: serve the bundle first (`uv run server` in the " + "package `openenv import` wrote), or train on AgentEnv.local(bundle)") from e + splits = [split["name"] for split in http.get(f"/{name}/splits").raise_for_status().json()] + tasks = {split: http.post(f"/{name}/tasks", json={"split": split}).raise_for_status().json()["tasks"] + for split in splits} + if not all("toolset" in task for described in tasks.values() for task in described): + raise ValueError(f"the server at {url} doesn't describe its tasks' toolsets, so it isn't a bundle served by " + "agent_env.openenv.build_app from this version of agentenv-framework or later") + return name, tasks + + +def _toolsets(name: str, tasks: dict[str, list[dict[str, Any]]]) -> dict[str, _Toolset]: + """Each toolset the tasks have, with the first task that has it, named after the server when it's the only one + and otherwise after its envs, and ``bash`` when its turns offer it.""" + firsts: dict[str, tuple[str, int]] = {} + for split, described in tasks.items(): + for task in described: + firsts.setdefault(_key(task), (split, task["index"])) + if len(firsts) == 1: + return {key: _Toolset(name, first) for key, first in firsts.items()} + named: dict[str, _Toolset] = {} + for key, first in firsts.items(): + toolset = json.loads(key) + base = "+".join([*(env.rsplit("/", 1)[-1] for env in toolset["envs"]), *(["bash"] if toolset["bash"] else [])]) + base = base or "agent" + taken = {toolset.name for toolset in named.values()} + name, n = base, 1 + while name in taken: + n += 1 + name = f"{base}-{n}" + named[key] = _Toolset(name, first) + return named + + +def _key(task: Mapping[str, Any]) -> str: + return json.dumps(task["toolset"], sort_keys=True) + + +def _messages(system: str | None, prompt: str) -> list[dict[str, str]]: + return [*([{"role": "system", "content": system}] if system else []), {"role": "user", "content": prompt}] + + +def _prompt_of(messages: list[dict[str, Any]] | None) -> tuple[str | None, str] | None: + """The system prompt and prompt a row of ``dataset_for`` carries.""" + if not messages: + return None + system = next((message["content"] for message in messages if message["role"] == "system"), None) + return system or None, messages[-1]["content"] + + +def _error_of(observation: Mapping[str, Any]) -> str: + return (observation.get("metadata") or {}).get("error") or "the episode ended in an error" + + +def _why(error: BaseException) -> str: + return f"{type(error).__name__}: {error}" if str(error) else type(error).__name__ + + +def tool_method(tool: Mapping[str, Any]) -> Callable[..., str]: + """A method calling ``tool``, typed and documented from its JSON Schema the way transformers' ``get_json_schema`` + reads a function, so TRL shows the model the tool as the env describes it. Built without generating source.""" + name = tool["name"] + if not _callable_name(name) or name in _RESERVED: + raise ValueError(f"the env's tool {name!r} can't be a TRL tool: TRL calls an environment's tools as its " + "methods, so a tool's name must be a public Python identifier other than reset and " + "get_reward. Rename the tool in the env") + schema = tool.get("input_schema") or {} + required = set(schema.get("required") or []) + parameters = [inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD)] + annotations: dict[str, Any] = {} + described = [] + for argument, spec in (schema.get("properties") or {}).items(): + if not _callable_name(argument): + raise ValueError(f"the env's tool {name!r} takes an argument {argument!r}, which TRL can't pass: an " + "argument's name must be a public Python identifier. Rename it in the env") + hint = _hint(spec, f"{name}.{argument}") + default = inspect.Parameter.empty + if argument not in required: + hint, default = hint | None, spec.get("default") + parameters.append(inspect.Parameter(argument, inspect.Parameter.KEYWORD_ONLY, default=default, annotation=hint)) + annotations[argument] = hint + described.append(f" {argument}: {_describe(argument, spec)}") + annotations["return"] = str + + def method(self: _Environment, **arguments: Any) -> str: + return self._call(name, arguments) + + method.__name__ = method.__qualname__ = name + method.__doc__ = (tool.get("description") or f"Call the {name} tool.") + ( + "\n\nArgs:\n" + "\n".join(described) if described else "") + method.__annotations__ = annotations + method.__signature__ = inspect.Signature(parameters, return_annotation=str) + return method + + +def _callable_name(name: str) -> bool: + return name.isidentifier() and not keyword.iskeyword(name) and not name.startswith("_") + + +def _hint(schema: Mapping[str, Any], where: str) -> Any: + if options := schema.get("anyOf") or schema.get("oneOf"): + return _union([_hint(option, where) for option in options]) + if "enum" in schema: + return Literal[tuple(schema["enum"])] + kind = schema.get("type") + if isinstance(kind, list): + return _union([_hint({**schema, "type": each}, where) for each in kind]) + if kind == "array": + items = schema.get("items") + return list[_hint(items, where)] if items else list + if kind is None: + return Any + if kind not in _TYPES: + raise ValueError(f"{where}: JSON Schema type {kind!r} has no Python type TRL can show the model") + return _TYPES[kind] + + +def _union(hints: list[Any]) -> Any: + return reduce(operator.or_, list(dict.fromkeys(hints))) + + +def _describe(name: str, spec: Mapping[str, Any]) -> str: + """One line, since transformers ends an argument's description at the next line that looks like another.""" + text = spec.get("description") or spec.get("title") or name + return " ".join(f"{text} {_fields(spec)}".split()) + + +def _fields(spec: Mapping[str, Any]) -> str: + """The fields of an object argument, or of an array's entries, which a Python type hint can't carry.""" + for option in [spec, *(spec.get("anyOf") or spec.get("oneOf") or [])]: + if option.get("type") == "array" and (items := option.get("items") or {}).get("properties"): + return "Each entry: " + _field_list(items) + if option.get("type") == "object" and option.get("properties"): + return "Fields: " + _field_list(option) + return "" + + +def _field_list(schema: Mapping[str, Any]) -> str: + required = set(schema.get("required") or []) + parts = [] + for name, spec in schema["properties"].items(): + kind = _kind(spec) + (", required" if name in required else "") + parts.append(f"{name} ({kind})" + (f": {spec['description']}" if spec.get("description") else "")) + return "; ".join(parts) + "." + + +def _kind(spec: Mapping[str, Any]) -> str: + if options := spec.get("anyOf") or spec.get("oneOf"): + return " or ".join(_kind(option) for option in options) + kind = spec.get("type", "any") + return " or ".join(kind) if isinstance(kind, list) else kind + + +__all__ = ["AgentEnv"] diff --git a/tst/integration/training/__init__.py b/tst/integration/training/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tst/integration/training/conftest.py b/tst/integration/training/conftest.py new file mode 100644 index 000000000..514d59f4f --- /dev/null +++ b/tst/integration/training/conftest.py @@ -0,0 +1,5 @@ +"""The items server env, its verifiers and bundle writer, and the throwaway registry of the OpenEnv server tests, +which these tests train on.""" + +from tst.integration.env.envs.server_env_local_test import local_stack # noqa: F401 +from tst.integration.openenv.build_app_test import items # noqa: F401 diff --git a/tst/integration/training/trl_test.py b/tst/integration/training/trl_test.py new file mode 100644 index 000000000..26f47bec1 --- /dev/null +++ b/tst/integration/training/trl_test.py @@ -0,0 +1,73 @@ +"""``AgentEnv`` on bundles served by ``AgentEnv.local`` on local Docker, used as TRL's trainers use it: rollouts on one +environment score as the task's verifier scores them, a second turn's prompt is handed over, a task whose agent works +in its sandbox takes a toolset with ``bash`` of its own, and nothing is left running once the env is closed. + +Requires a docker daemon; reuses the items env, verifiers and bundle writer of the OpenEnv server tests.""" + +import inspect +import json +import uuid + +import pytest + +from agent_env.artifact import FileArtifact +from agent_env.training.trl import ENDED, AgentEnv +from tst.integration.openenv.build_app_test import _left + +pytestmark = pytest.mark.integration + + +def _tools(environment): + return {name for name, _ in inspect.getmembers(environment, inspect.isfunction) if not name.startswith("_")} + + +def test_rollouts_on_one_environment_score_as_the_tasks_verifier_scores_them(items): + env = AgentEnv.local(items.bundle("dock", {"crate": items(), "twice": items(turns=2)}), max_concurrent_envs=2) + try: + crate, twice = env.dataset.to_list() + environment = env.factory() + assert {"items_add_item", "items_whoami", "end_turn"} <= _tools(env.factory) + + environment.reset(**crate) + assert json.loads(environment.items_add_item(item="crate")) == {"count": 1} + assert environment.end_turn(response="Added a crate.") == ENDED + assert environment.get_reward() == 1.0 + environment.reset(**crate) + assert environment.get_reward() == 0.0 + environment.reset(**twice) + environment.items_add_item(item="crate") + assert environment.end_turn(response="Added a crate.") == "Add another." + assert environment.end_turn(response="Added another.") == ENDED + assert environment.get_reward() == 1.0 + finally: + env.close() + + assert _left(items.sandboxes) == [] + + +def test_a_task_whose_agent_works_in_its_sandbox_takes_a_toolset_with_bash(items): + uid = uuid.uuid4().hex[:8] + greeting = FileArtifact.put_bytes(id=f"trl-greeting-{uid}", description="trl e2e", filename="greeting.txt", + content=b"hello", content_type="text/plain") + env_step, policy, play, _ = items() + copy = [env_step, policy, {"id": "load", "type": "load_artifact", "agent_name": "policy", "artifact_id": greeting.id, + "destination_path": "/app/inputs"}, play, + {"id": "copied", "type": "verify_sandbox", "agent_name": "policy", "verifier_id": "copied", "criteria": [ + {"type": "probe_file_contains", "criterion": "the greeting is copied", "paths": ["outputs/answer.txt"], + "expected": "^hello$"}]}] + + env = AgentEnv.local(items.bundle("desk", {"copy": copy, "crate": items()})) + try: + plain, sandboxed = env_step["env_id"], env_step["env_id"] + "+bash" + assert {name: "bash" in _tools(each) for name, each in env.factory.items()} == {sandboxed: True, plain: False} + row = next(row for row in env.dataset if row["environment"] == sandboxed) + environment = env.factory[sandboxed]() + environment.reset(**row) + assert environment.bash(command="cat inputs/greeting.txt") == "hello" + environment.bash(command="mkdir -p outputs && cp inputs/greeting.txt outputs/answer.txt") + environment.end_turn(response="Done.") + assert environment.get_reward() == 1.0 + finally: + env.close() + + assert _left(items.sandboxes) == [] diff --git a/tst/unit/openenv/environment_test.py b/tst/unit/openenv/environment_test.py index 547c40240..552591e8d 100644 --- a/tst/unit/openenv/environment_test.py +++ b/tst/unit/openenv/environment_test.py @@ -88,7 +88,8 @@ async def test_the_task_api_lists_each_split_with_each_tasks_prompt_and_agent(te assert env.list_splits() == ["smoke", "all"] assert (env.num_tasks("smoke"), env.num_tasks("all")) == (1, 3) assert env.get_task("all", 2) == {"id": "dock-2", "name": "dock-2", "index": 2, "prompt": "Add crate 2.", - "system_prompt": "You keep the items.", "agent": "alice"} + "system_prompt": "You keep the items.", "agent": "alice", + "toolset": {"envs": ["items"], "role": "alice@example.com", "bash": False}} assert [task["index"] for task in env.get_task_range("all", 1)] == [1, 2] assert [task["id"] for task in env.list_tasks("smoke")] == ["dock-0"] with pytest.raises(IndexError, match="split 'all' has 3 tasks, so it has no task 3"): diff --git a/tst/unit/test_optional_extras_stay_optional.py b/tst/unit/test_optional_extras_stay_optional.py index e8d318850..6407b1f82 100644 --- a/tst/unit/test_optional_extras_stay_optional.py +++ b/tst/unit/test_optional_extras_stay_optional.py @@ -131,9 +131,12 @@ def _closure(extras: frozenset[str]) -> set[str]: def _only_in(extra: str) -> list[str]: - """Module paths the extra installs and neither a core install nor any other extra does: a - distribution two extras share (grpcio, for gcp and tensorlake) stays for the other's modules.""" - others = frozenset(_optional_requirements()) - {extra} + """Module paths the extra installs and neither a core install nor any other extra does, but for an extra that + includes it (``trl`` includes ``openenv``): a distribution two extras share (grpcio, for gcp and tensorlake) stays + for the other's modules.""" + mine = _closure(frozenset({extra})) + others = [other for other in _optional_requirements() + if other != extra and not mine <= _closure(frozenset({other}))] core = _closure(frozenset()) | set().union(*(_closure(frozenset({other})) for other in others)) provided_by_core = set().union(*(_module_paths(d) for d in core)) extra_only = _closure(frozenset({extra})) - core @@ -255,9 +258,10 @@ def test_the_explorer_imports_without_any_extra(): assert _run(_IMPORT, _not_in_core(), "agent_env.explorer.app") == "" -def test_the_openenv_server_without_the_extra_names_the_extra_to_install(): - message = _run(_IMPORT, _only_in("openenv"), "agent_env.openenv") - assert message.endswith("pip install 'agentenv-framework[openenv]'"), message +@pytest.mark.parametrize(("module", "extra"), [("agent_env.openenv", "openenv"), ("agent_env.training.trl", "trl")]) +def test_the_openenv_server_and_the_trl_helper_without_their_extra_name_the_extra_to_install(module, extra): + message = _run(_IMPORT, _only_in(extra), module) + assert message.endswith(f"pip install 'agentenv-framework[{extra}]'"), message @pytest.mark.parametrize("package", _STORE_PACKAGES) diff --git a/tst/unit/test_runtime_dependencies_are_imported.py b/tst/unit/test_runtime_dependencies_are_imported.py index 74034daa5..de57ee567 100644 --- a/tst/unit/test_runtime_dependencies_are_imported.py +++ b/tst/unit/test_runtime_dependencies_are_imported.py @@ -14,6 +14,8 @@ import tomllib from pathlib import Path +from packaging.requirements import Requirement + import agent_env SRC = Path(agent_env.__file__).resolve().parent @@ -38,13 +40,18 @@ def _runtime_requirements() -> list[str]: def _optional_requirements() -> dict[str, list[str]]: - """Each runtime extra's own distributions; ``dev`` is test tooling.""" + """Each runtime extra's distributions, with those of the extras it includes; ``dev`` is test tooling.""" extras = tomllib.loads(PYPROJECT.read_text())["project"]["optional-dependencies"] - return { - extra: [name for dep in deps if (name := _requirement_name(dep)) != SELF] - for extra, deps in extras.items() - if extra != "dev" - } + + def distributions(extra: str) -> list[str]: + found = [] + for dep in extras[extra]: + requirement = Requirement(dep) + found += ([_normalize(requirement.name)] if _normalize(requirement.name) != SELF + else [name for included in sorted(requirement.extras) for name in distributions(included)]) + return found + + return {extra: distributions(extra) for extra in extras if extra != "dev"} @functools.cache @@ -169,17 +176,19 @@ def test_every_optional_dependency_is_imported_by_the_library(): assert all(not deps for deps in unused.values()), f"extra dependencies nothing imports: {unused}" -def test_a_module_using_an_extra_imports_only_what_the_extra_and_core_declare(): - """Otherwise the extra works only while something else happens to install the package.""" +def test_a_module_using_an_extra_imports_only_what_one_extra_and_core_declare(): + """Otherwise the extra works only while something else happens to install the package. A module may use the + extras an extra includes, as ``trl`` includes ``openenv``.""" core = _runtime_requirements() - for extra, deps in _optional_requirements().items(): - own = [d for d in deps if d not in core] - for path in _library_files(): - imports = [c for c in _imports(path) if _third_party(c)] - if not any(_owners(c, own) for c in imports): - continue - undeclared = [c[0] for c in imports if not _owners(c, core + deps)] - assert undeclared == [], ( - f"{path.relative_to(SRC)} uses the {extra!r} extra but imports {undeclared}, " - f"which neither it nor the core dependencies declare" - ) + extras = _optional_requirements() + for path in _library_files(): + imports = [c for c in _imports(path) if _third_party(c)] + used = [extra for extra, deps in extras.items() if any(_owners(c, [d for d in deps if d not in core]) + for c in imports)] + if not used: + continue + undeclared = min(([c[0] for c in imports if not _owners(c, core + extras[extra])] for extra in used), key=len) + assert undeclared == [], ( + f"{path.relative_to(SRC)} uses the {used} extras but imports {undeclared}, which no one of them " + f"declares with the core dependencies" + ) diff --git a/tst/unit/training/__init__.py b/tst/unit/training/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tst/unit/training/conftest.py b/tst/unit/training/conftest.py new file mode 100644 index 000000000..a6fa442e7 --- /dev/null +++ b/tst/unit/training/conftest.py @@ -0,0 +1,6 @@ +"""The Episode API's fake steps, stand-in teardown and tool sessions, which these tests serve as an OpenEnv env on +loopback, and the loopback connections that takes.""" + +from tst.unit.openenv.environment_test import tools # noqa: F401 +from tst.unit.task.episode.conftest import _disable_network # noqa: F401 +from tst.unit.task.episode.run_test import teardowns # noqa: F401 diff --git a/tst/unit/training/trl_test.py b/tst/unit/training/trl_test.py new file mode 100644 index 000000000..ac4183f19 --- /dev/null +++ b/tst/unit/training/trl_test.py @@ -0,0 +1,252 @@ +"""``AgentEnv`` on the OpenEnv server ``build_app`` makes, served on loopback over the Episode API's fake steps and +used as TRL's trainers use it: the dataset and the environment classes, rollouts reset, stepped and scored on a reused +environment, the turns a model leaves open, episodes left unscored, a server at capacity or missing, tasks with other +tools, and a bundle served by ``AgentEnv.local`` whose episodes end at close and at exit. Then the tool methods TRL +reads a tool's schema from.""" + +import asyncio +import inspect +import json +import logging +from typing import Any, Literal + +import pytest +from agentenv_protocol import EnvironmentTool +from agentenv_protocol.a2a_agent import proxy as proxy_module + +from agent_env.openenv.app import _app, _score_at +from agent_env.openenv.environment import Served, ServedTask +from agent_env.task import EpisodeSpec, Task +from agent_env.training import trl +from agent_env.training.trl import ENDED, FAILED, AgentEnv, tool_method +from tst.unit.bundle._support import layout +from tst.unit.openenv.environment_test import AGAIN +from tst.unit.task.episode.run_test import ALICE, ITEMS, PANTRY, PANTRY_MCP, REPLIES, TURN, _tool_sessions + +PLAYED = [ITEMS, ALICE, TURN, REPLIES] +SYSTEM = {"role": "system", "content": "You keep the items."} + + +@pytest.fixture +def served(teardowns, tools): + """Serves ``tasks`` by name, every one in the split ``all`` and those ``splits`` name in theirs, as ``build_app`` + serves a bundle's; closed after the test.""" + opened = [] + + def serve(tasks, splits=None, max_concurrent_envs=8, **kwargs): + by_name = {} + for name, steps in tasks.items(): + task = Task.from_dict({"id": name, "version": 1, "steps": steps}) + by_name[name] = ServedTask(task, name, EpisodeSpec.from_task(task), _score_at("replies")) + named = {split: tuple(by_name[name] for name in names) for split, names in (splits or {}).items()} + server = trl._Server(_app(Served(splits={"all": tuple(by_name.values()), **named}, sandbox=None), "dock", + max_concurrent_envs)) + env = AgentEnv(server.url, **kwargs) + env._server = server + opened.append(env) + return env + + yield serve + for env in opened: + env.close() + + +def _tools(environment): + return [name for name, _ in inspect.getmembers(environment, inspect.isfunction) if not name.startswith("_")] + + +def test_the_dataset_holds_each_tasks_prompt_and_the_factory_offers_its_tools(served): + env = served({"a": PLAYED, "b": [ITEMS, ALICE, {**TURN, "prompt": "Add a barrel."}, REPLIES]}, {"smoke": ["b"]}) + + assert env.dataset.to_list() == [ + {"prompt": [SYSTEM, {"role": "user", "content": "Add a crate."}], "split": "all", "index": 0}, + {"prompt": [SYSTEM, {"role": "user", "content": "Add a barrel."}], "split": "all", "index": 1}] + assert env.dataset_for("smoke").to_list() == [ + {"prompt": [SYSTEM, {"role": "user", "content": "Add a barrel."}], "split": "smoke", "index": 0}] + assert (env.factory.__name__, _tools(env.factory)) == ("dock", ["add_item", "end_turn", "get_reward", "reset"]) + assert inspect.signature(env.factory.end_turn).parameters["response"].annotation == str | None + with pytest.raises(ValueError, match="has no split 'train'; it serves all, smoke"): + env.dataset_for("train") + + +def test_a_rollout_plays_its_rows_task_and_an_environment_plays_one_after_another(served, teardowns): + env = served({"a": PLAYED}) + [row] = env.dataset.to_list() + environment = env.factory() + + assert environment.reset(**row) is None + assert environment.add_item(item="crate") == "add_item {'item': 'crate'}" + assert environment.end_turn(response="Added a crate.") == ENDED + assert environment.add_item(item="crate") == ENDED + assert environment.get_reward() == 1.0 + environment.reset(**row) + assert environment.get_reward() == 0.0 + assert environment.get_reward() is None + assert len(teardowns.finished) == 3 + + +def test_end_turn_answers_with_the_next_turns_prompt_and_a_turn_left_open_ends_empty(served): + env = served({"a": [ITEMS, ALICE, TURN, AGAIN, REPLIES]}) + environment = env.factory() + + environment.reset(**env.dataset[0]) + assert environment.end_turn(response="Added a crate.") == "Add another." + assert environment.get_reward() == 0.0 + environment.reset(**env.dataset[0]) + environment.end_turn(response="Added a crate.") + assert environment.end_turn(response="Added another.") == ENDED + assert environment.get_reward() == 1.0 + + +def test_an_episode_that_fails_or_isnt_its_rows_is_left_unscored_and_the_environment_plays_on(served, caplog): + env = served({"a": PLAYED, "broken": [{**ITEMS, "fail": True}, ALICE, TURN, REPLIES]}) + played, broken = env.dataset.to_list() + environment = env.factory() + + with caplog.at_level(logging.WARNING, logger=trl.__name__): + environment.reset(**broken) + assert environment.add_item(item="crate") == FAILED + assert environment.get_reward() is None + environment.reset(**{**played, "prompt": [{"role": "user", "content": "Add a barrel."}]}) + assert environment.get_reward() is None + environment.reset(**played) + environment.end_turn(response="Added a crate.") + + assert environment.get_reward() == 1.0 + assert [record.getMessage() for record in caplog.records if record.name == trl.__name__] == [ + "rollout left unscored: RuntimeError: the env didn't come up", + "rollout left unscored: the turn's prompt differs from the dataset row's"] + + +class _Hanging: + """Stands in for the proxy's ``ToolSession``: every env offers ``wait``, whose call takes three seconds.""" + + def __call__(self, *, mcp_url, headers=None, timeout): + class _Session: + async def open(self): + return self + + async def list_tools(self): + return [EnvironmentTool(name="wait", inputSchema={"type": "object"})] + + async def call_tool(self, name, arguments=None): + await asyncio.sleep(3) + + async def close(self): + pass + + return _Session() + + +def test_a_tool_that_misses_its_deadline_leaves_the_rollout_unscored_and_the_environment_plays_on( + served, monkeypatch, caplog): + monkeypatch.setattr(proxy_module, "ToolSession", _Hanging()) + env = served({"a": PLAYED}, tool_timeout=0.5) + environment = env.factory() + + environment.reset(**env.dataset[0]) + with caplog.at_level(logging.WARNING, logger=trl.__name__): + assert environment.wait() == FAILED + assert environment.get_reward() is None + environment.reset(**env.dataset[0]) + environment.end_turn(response="Added a crate.") + + assert environment.get_reward() == 1.0 + assert "rollout left unscored: wait didn't answer within 0.5s" in caplog.text + + +def test_a_server_at_capacity_refuses_the_reset_and_says_how_to_serve_more(served): + env = served({"a": PLAYED}, max_concurrent_envs=1) + first, second = env.factory(), env.factory() + + first.reset(**env.dataset[0]) + with pytest.raises(RuntimeError, match="at its limit of 1 sessions at once.* MAX_CONCURRENT_ENVS"): + second.reset(**env.dataset[0]) + first.end_turn(response="Added a crate.") + + assert first.get_reward() == 1.0 + + +def test_no_server_at_the_url_says_how_to_serve_one(): + with pytest.raises(ConnectionError, match=r"no OpenEnv server answers at http://127.0.0.1:9: serve the bundle"): + AgentEnv("http://127.0.0.1:9") + + +def test_tasks_whose_agent_gets_other_tools_take_trls_multi_environment_mode(served, monkeypatch): + _tool_sessions(monkeypatch, {"http://127.0.0.1:20031/items/mcp": ["add_item"], PANTRY_MCP: ["take_item"]}) + env = served({"a": PLAYED, "p": [PANTRY, {**ALICE, "env_ids": ["pantry"]}, TURN, REPLIES]}) + + assert {name: _tools(each) for name, each in env.factory.items()} == { + "items": ["add_item", "end_turn", "get_reward", "reset"], + "pantry": ["end_turn", "get_reward", "reset", "take_item"]} + assert [(row["environment"], row["index"]) for row in env.dataset] == [("items", 0), ("pantry", 1)] + pantry = env.factory["pantry"]() + pantry.reset(**{key: value for key, value in env.dataset[1].items() if key != "environment"}) + assert pantry.take_item() == f"take_item on {PANTRY_MCP}" + pantry.end_turn(response="Took one.") + assert pantry.get_reward() == 1.0 + + +def test_a_tool_trl_cant_call_is_refused_with_how_to_fix_it(served, tools): + tools("add-item") + env = served({"a": PLAYED}) + + with pytest.raises(ValueError, match="tool 'add-item' can't be a TRL tool.*Rename the tool in the env"): + env.factory # noqa: B018 + + +@pytest.mark.parametrize("end", ["close", "exit"]) +def test_a_bundle_served_by_local_plays_and_its_episodes_left_running_end_at_close_and_at_exit( + tmp_path, monkeypatch, teardowns, end): + monkeypatch.setenv("HOME", str(tmp_path)) + unbound = {key: value for key, value in ALICE.items() if key not in ("env_ids", "role")} + env = AgentEnv.local(layout(tmp_path / "dock", {"tasks/a.json": json.dumps([ITEMS, unbound, TURN, REPLIES])})) + environment = env.factory() + environment.reset(**env.dataset[0]) + environment.end_turn(response="Added a crate.") + assert environment.get_reward() == 1.0 + environment.reset(**env.dataset[0]) + assert "offers no tool 'look'" in environment._call("look", {}) + + env.close() if end == "close" else trl._close_all() + + assert (len(teardowns.finished), environment._client, trl._servers) == (3, None, set()) + + +def test_a_tools_arguments_are_typed_and_documented_from_its_schema(): + method = tool_method({"name": "restock", "description": "Restock a shelf.", "input_schema": { + "type": "object", "required": ["shelf", "count"], + "properties": { + "shelf": {"type": "string", "enum": ["top", "bottom"], "description": "which shelf"}, + "count": {"type": "integer"}, + "labels": {"type": "array", "items": {"type": "string"}, "title": "Labels"}, + "note": {"type": ["string", "null"], "default": None, "description": "for the night shift"}, + "options": {"type": "object", "properties": {"rush": {"type": "boolean"}}, "required": ["rush"]}, + }, + }}) + + parameters = inspect.signature(method).parameters + assert {name: (parameter.annotation, parameter.default) for name, parameter in parameters.items()} == { + "self": (inspect.Parameter.empty, inspect.Parameter.empty), + "shelf": (Literal["top", "bottom"], inspect.Parameter.empty), + "count": (int, inspect.Parameter.empty), + "labels": (list[str] | None, None), + "note": (str | None, None), + "options": (dict | None, None), + } + assert method.__doc__ == ( + "Restock a shelf.\n\nArgs:\n shelf: which shelf\n count: count\n labels: Labels\n" + " note: for the night shift\n options: options Fields: rush (boolean, required).") + assert tool_method({"name": "ping"}).__doc__ == "Call the ping tool." + + +@pytest.mark.parametrize(("tool", "message"), [ + ({"name": "get-situation"}, "can't be a TRL tool"), + ({"name": "reset"}, "can't be a TRL tool"), + ({"name": "_inspect"}, "can't be a TRL tool"), + ({"name": "plan", "input_schema": {"properties": {"ship-id": {"type": "integer"}}}}, "argument 'ship-id'"), + ({"name": "upload", "input_schema": {"properties": {"file": {"type": "file"}}}}, r"upload\.file: JSON Schema"), +], ids=lambda value: value["name"] if isinstance(value, dict) else "") +def test_a_tool_trl_cant_show_the_model_is_refused(tool: dict[str, Any], message): + with pytest.raises(ValueError, match=message): + tool_method(tool) diff --git a/uv.lock b/uv.lock index 356c9bc34..8fca09d78 100644 --- a/uv.lock +++ b/uv.lock @@ -85,6 +85,7 @@ dev = [ { name = "boto3" }, { name = "botocore" }, { name = "boxd" }, + { name = "datasets" }, { name = "daytona" }, { name = "fastmcp" }, { name = "google-api-core" }, @@ -133,6 +134,10 @@ tenki = [ tensorlake = [ { name = "tensorlake" }, ] +trl = [ + { name = "datasets" }, + { name = "openenv" }, +] vercel = [ { name = "anyio" }, { name = "httpx2" }, @@ -142,7 +147,8 @@ vercel = [ [package.metadata] requires-dist = [ { name = "a2a-sdk", specifier = "==0.3.26" }, - { name = "agentenv-framework", extras = ["aws", "boxd", "daytona", "gcp", "openenv", "sail", "tensorlake", "tenki", "vercel"], marker = "extra == 'dev'" }, + { name = "agentenv-framework", extras = ["aws", "boxd", "daytona", "gcp", "openenv", "sail", "tensorlake", "tenki", "trl", "vercel"], marker = "extra == 'dev'" }, + { name = "agentenv-framework", extras = ["openenv"], marker = "extra == 'trl'" }, { name = "agentenv-framework-protocol", extras = ["computer"], editable = "packages/agentenv-protocol" }, { name = "anyio", marker = "extra == 'vercel'", specifier = ">=4,<5" }, { name = "av", marker = "extra == 'dev'", specifier = ">=17" }, @@ -152,6 +158,7 @@ requires-dist = [ { name = "certifi", specifier = ">=2024.2.2" }, { name = "click", specifier = ">=8.0.0" }, { name = "cryptography", specifier = ">=42" }, + { name = "datasets", marker = "extra == 'trl'", specifier = ">=3" }, { name = "daytona", marker = "extra == 'daytona'", specifier = "~=0.224.0" }, { name = "e2b", specifier = "~=2.46.4" }, { name = "fastapi", specifier = ">=0.110" }, @@ -192,7 +199,7 @@ requires-dist = [ { name = "uvicorn", specifier = ">=0.47" }, { name = "vercel-sandbox", marker = "extra == 'vercel'", specifier = ">=0.7.0,<0.8" }, ] -provides-extras = ["openenv", "aws", "gcp", "sail", "vercel", "tensorlake", "tenki", "boxd", "daytona", "dev", "smol"] +provides-extras = ["openenv", "trl", "aws", "gcp", "sail", "vercel", "tensorlake", "tenki", "boxd", "daytona", "dev", "smol"] [[package]] name = "agentenv-framework-protocol" @@ -1193,6 +1200,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d4/dd/3f73d04c95f6acc6a54700011637e49362e337139b518be895edf4786f87/cyclopts-5.2.0-py3-none-any.whl", hash = "sha256:5da2a5d65164e03008621fc4795946b88a722b881398b5dfa58afa6218342cff", size = 289196, upload-time = "2026-10-06T15:02:37.57Z" }, ] +[[package]] +name = "datasets" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, + { name = "filelock" }, + { name = "fsspec", extra = ["http"] }, + { name = "huggingface-hub" }, + { name = "multiprocess" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pyarrow" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/e6/b5d8c77f13cc6269660b0f2691e8875777b9d94972e281b7b2c25f25bd9d/datasets-5.1.0.tar.gz", hash = "sha256:1561347daa8841b51351ef2ad1647b4ee4af406852c0f7bd1df587ac1ca7888e", size = 687306, upload-time = "2026-10-05T15:32:32.053Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/cc/7f624629846c709ef1c68aa61bfb8d0ae2638cbe265104a1c2f7a630949e/datasets-5.1.0-py3-none-any.whl", hash = "sha256:86fd08b280485aab82894d2c81842c4151d93b575efea0054315104cb7bc3cec", size = 604432, upload-time = "2026-10-05T15:32:29.892Z" }, +] + [[package]] name = "daytona" version = "0.224.1" @@ -1333,6 +1364,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, ] +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + [[package]] name = "distro" version = "1.9.0" @@ -1680,6 +1720,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/22/4222d7ddf3da30f363edaa98e329c2bce6c65497c9cb2810931c8b2c0fbc/fsspec-2026.6.0-py3-none-any.whl", hash = "sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1", size = 203949, upload-time = "2026-06-16T01:57:26.358Z" }, ] +[package.optional-dependencies] +http = [ + { name = "aiohttp" }, +] + [[package]] name = "google-api-core" version = "2.31.0" @@ -2795,6 +2840,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] +[[package]] +name = "multiprocess" +version = "0.70.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/f2/e783ac7f2aeeed14e9e12801f22529cc7e6b7ab80928d6dcce4e9f00922d/multiprocess-0.70.19.tar.gz", hash = "sha256:952021e0e6c55a4a9fe4cd787895b86e239a40e76802a789d6305398d3975897", size = 2079989, upload-time = "2026-01-19T06:47:39.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/aa/714635c727dbfc251139226fa4eaf1b07f00dc12d9cd2eb25f931adaf873/multiprocess-0.70.19-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1bbf1b69af1cf64cd05f65337d9215b88079ec819cd0ea7bac4dab84e162efe7", size = 144743, upload-time = "2026-01-19T06:47:24.562Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e1/155f6abf5e6b5d9cef29b6d0167c180846157a4aca9b9bee1a217f67c959/multiprocess-0.70.19-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:5be9ec7f0c1c49a4f4a6fd20d5dda4aeabc2d39a50f4ad53720f1cd02b3a7c2e", size = 144738, upload-time = "2026-01-19T06:47:26.636Z" }, + { url = "https://files.pythonhosted.org/packages/af/cb/f421c2869d75750a4f32301cc20c4b63fab6376e9a75c8e5e655bdeb3d9b/multiprocess-0.70.19-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1c3dce098845a0db43b32a0b76a228ca059a668071cfeaa0f40c36c0b1585d45", size = 144741, upload-time = "2026-01-19T06:47:27.985Z" }, + { url = "https://files.pythonhosted.org/packages/e3/45/8004d1e6b9185c1a444d6b55ac5682acf9d98035e54386d967366035a03a/multiprocess-0.70.19-py310-none-any.whl", hash = "sha256:97404393419dcb2a8385910864eedf47a3cadf82c66345b44f036420eb0b5d87", size = 134948, upload-time = "2026-01-19T06:47:32.325Z" }, + { url = "https://files.pythonhosted.org/packages/86/c2/dec9722dc3474c164a0b6bcd9a7ed7da542c98af8cabce05374abab35edd/multiprocess-0.70.19-py311-none-any.whl", hash = "sha256:928851ae7973aea4ce0eaf330bbdafb2e01398a91518d5c8818802845564f45c", size = 144457, upload-time = "2026-01-19T06:47:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/71/70/38998b950a97ea279e6bd657575d22d1a2047256caf707d9a10fbce4f065/multiprocess-0.70.19-py312-none-any.whl", hash = "sha256:3a56c0e85dd5025161bac5ce138dcac1e49174c7d8e74596537e729fd5c53c28", size = 150281, upload-time = "2026-01-19T06:47:35.037Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/d2c27e03cb84251dfe7249b8e82923643c6d48fa4883b9476b025e7dc7eb/multiprocess-0.70.19-py313-none-any.whl", hash = "sha256:8d5eb4ec5017ba2fab4e34a747c6d2c2b6fecfe9e7236e77988db91580ada952", size = 156414, upload-time = "2026-01-19T06:47:35.915Z" }, + { url = "https://files.pythonhosted.org/packages/a0/61/af9115673a5870fd885247e2f1b68c4f1197737da315b520a91c757a861a/multiprocess-0.70.19-py314-none-any.whl", hash = "sha256:e8cc7fbdff15c0613f0a1f1f8744bef961b0a164c0ca29bdff53e9d2d93c5e5f", size = 160318, upload-time = "2026-01-19T06:47:37.497Z" }, + { url = "https://files.pythonhosted.org/packages/7e/82/69e539c4c2027f1e1697e09aaa2449243085a0edf81ae2c6341e84d769b6/multiprocess-0.70.19-py39-none-any.whl", hash = "sha256:0d4b4397ed669d371c81dcd1ef33fd384a44d6c3de1bd0ca7ac06d837720d3c5", size = 133477, upload-time = "2026-01-19T06:47:38.619Z" }, +] + [[package]] name = "numpy" version = "2.4.6" @@ -3702,6 +3767,63 @@ memory = [ { name = "cachetools" }, ] +[[package]] +name = "pyarrow" +version = "26.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/34/17c34cb38e5d940e38f0f0d9fdfa0e8a506676409ea9b85aff7e3079f831/pyarrow-26.0.0.tar.gz", hash = "sha256:0cccd36e00ea3afeb52ded61f2721ce71f604853d70c45365c58324eb773d6ae", size = 1239433, upload-time = "2026-10-09T08:26:25.315Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/68/e0707097cee93be7f693e7e89495fabfeb8bf95ee30619063f8b30fffc29/pyarrow-26.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:fcdd1e04982637c6042337d3e24d472f938f01fdc502e2b994844b726d12c3f4", size = 36370896, upload-time = "2026-10-09T08:13:28.874Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f0/591211c00612aef83236daff1620412b24aeb07c646de08c18a8a6c95a39/pyarrow-26.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:f800e9e722c145ccd18012d82a864cb21bfee4ba4ceffde77100d25eced511a9", size = 38709806, upload-time = "2026-10-09T08:13:33.417Z" }, + { url = "https://files.pythonhosted.org/packages/50/ea/9b035a9d1556e06e64ea86169d9a985d0fc092d427ac5edbb3af7183289c/pyarrow-26.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:7aa12ab8e236789b1ecd2d6ecaef036b4e63d675ddf1864a43c6799d18f2d028", size = 50885975, upload-time = "2026-10-09T08:13:37.737Z" }, + { url = "https://files.pythonhosted.org/packages/e1/81/8e685683897a6d3d5887c3e2fd24f3c14bc5d6d6bb3a2387484e665c580e/pyarrow-26.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:6e89dee53aaeb50505ed6152ea55bc7ddfd4f4df264f5427ea255288d8f0e580", size = 53904793, upload-time = "2026-10-09T08:13:42.984Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ad/d474a0b1b00110f3a879aa5df654f857c81929a32b2a4222869240de5220/pyarrow-26.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1c1b4263fd13abbc339a16f2bf19f3a5cbf2a620853d812b1256f03c5342cb8", size = 54458010, upload-time = "2026-10-09T08:13:47.778Z" }, + { url = "https://files.pythonhosted.org/packages/d4/86/2c2861e905810c59fed4d98c85b994c21e8613730c5c3b436781d89110f2/pyarrow-26.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ff1e816af7abff71f289242e109217036723ce36aca74ad6691e52d964a74afa", size = 57368406, upload-time = "2026-10-09T08:13:52.651Z" }, + { url = "https://files.pythonhosted.org/packages/0e/02/823e606633c15155bb965c7a0f3750c4f20dd47c4ab48213c7693df0e0ba/pyarrow-26.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:13b0972a3dc71b642050d1bc72664a3916e14f59c943d8c1368154d6e4b0c2d5", size = 28522657, upload-time = "2026-10-09T08:13:56.513Z" }, + { url = "https://files.pythonhosted.org/packages/b3/60/6793778f2617cce469383dac0ba08c4f2401cf342df0c7b9ca53939d9b46/pyarrow-26.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:90ddaf7c625307ad52f31a9b25c34fe5e4897c7529ee3481135822b2b6842ff1", size = 36333953, upload-time = "2026-10-09T08:14:00.387Z" }, + { url = "https://files.pythonhosted.org/packages/db/81/f944cc63ce8a753e5fbff25de6d1d475ebd7fffdf9cf98c65130294fc896/pyarrow-26.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:ee341973f78a0b46e073d065e88e75026a9c584051e97f98a0d05d96c6bac7dd", size = 38688456, upload-time = "2026-10-09T08:14:04.344Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2d/7e5c722fa5d5d9f3b75e62fe11694b34217664d4f05ac88031197166b277/pyarrow-26.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:01c863a18bd9c8412453dd0d92de6d0ee7b2b3d6fb079d9734a4b2a3c8bd4453", size = 50867603, upload-time = "2026-10-09T08:14:09.115Z" }, + { url = "https://files.pythonhosted.org/packages/88/e4/9cd356d906e71bd79b0c3fc5c9a54e01a0020dcf14c152ccfbcb503c7298/pyarrow-26.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6a628922ba20705fa964ca73e4ef959c2fb2f14b9bbec5589a6a1e68e6257c85", size = 53931932, upload-time = "2026-10-09T08:14:24.051Z" }, + { url = "https://files.pythonhosted.org/packages/bb/e4/5bae3133b7fe04c24907a20f3bc1fba388cbbde659199e7b76445982047a/pyarrow-26.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:954d971b363b16ee41f89389a4053315dc71265f2ce5c2468eb0a910b1166268", size = 54444720, upload-time = "2026-10-09T08:14:31.214Z" }, + { url = "https://files.pythonhosted.org/packages/ba/b4/ee422493bb6dafdbef776cfe2c2a73106a1063a79bf4e78d1e5f51176885/pyarrow-26.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5d5768d03426abe6526d5274adefa00abf00a7f81118c46e98b5a46390f5549e", size = 57388949, upload-time = "2026-10-09T08:14:38.964Z" }, + { url = "https://files.pythonhosted.org/packages/54/3c/1783aab1dac28e175dcf26dfc7123725efc474caecaed91e8a34cb89cad0/pyarrow-26.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:cc903e1069e9dd5e9dcf780324c0112e27e051e422ecfaff574fb33ed65d9160", size = 28567581, upload-time = "2026-10-09T08:14:44.279Z" }, + { url = "https://files.pythonhosted.org/packages/4d/35/ca95493712af97c46a312945c8e9d16b21c5fe2f148be5466168d0290505/pyarrow-26.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a6ca849f90cf73fe361f08a5762c783ead9671e4548c1f558cc637b54c9103f2", size = 36336700, upload-time = "2026-10-09T08:14:51.399Z" }, + { url = "https://files.pythonhosted.org/packages/69/ef/b1a675f79c9babfd4fcd99af62141d3c2d1a78a524e311b0c6b80110445a/pyarrow-26.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:c2ba350957076b1b3a22f549261dc3e9c67ca20816d8bd5f79d7b9c69be4c4c2", size = 38698502, upload-time = "2026-10-09T08:14:57.114Z" }, + { url = "https://files.pythonhosted.org/packages/3b/7c/cea852a832a327a8de797b3a68e5c25ce0f5aa1d20503807671bd90ec642/pyarrow-26.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e3b190ba1d3d22a5a8758597f797111b77d433473744352a184a5ee0a42d672e", size = 50865064, upload-time = "2026-10-09T08:20:01.614Z" }, + { url = "https://files.pythonhosted.org/packages/4f/d6/e95834b29360092376fe4da9956ba41bb7b021869efe6ee9d4172d05cb15/pyarrow-26.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:240bd18a7487f8767616a948a69dd4e740a8bc36a1c9da49e4dc9a32c5c2faed", size = 53926722, upload-time = "2026-10-09T08:23:10.829Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7f/98257444e2aea2e1fddceee3af3bd2077236d550428413f80393bd1f888d/pyarrow-26.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2b5fcd69c0e1107b79e55839877db5a6ed04651b73fd6fec581d09e230bed5e4", size = 54443093, upload-time = "2026-10-09T08:23:16.971Z" }, + { url = "https://files.pythonhosted.org/packages/88/ca/dac99cfb25cfa62bf7194600cc99abc14a6bd2af50d7fdb7f15eeaf6e202/pyarrow-26.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f7444ea6975c49a857c68f9bd8fa11acae96dede63d120ffb3bf0a603ea82516", size = 57381937, upload-time = "2026-10-09T08:23:24.95Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ed/138d29fddaf803b90f4527e124bb6aaddc18aaf4a6c50fd0a5f577c94989/pyarrow-26.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:3de30a7432b48b98b9decbd9e25a53bb9251d202c2e6c5a29a50869592ccb117", size = 28478571, upload-time = "2026-10-09T08:23:30.535Z" }, + { url = "https://files.pythonhosted.org/packages/8c/32/01858422a37f083911c2bb4d15cc32c5eeaa9d9b2bf5ddedee995a7146a6/pyarrow-26.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:5780d487ff6c6ed7b42298609680d87fe0036e529a9dc2e1105364bce9697f50", size = 36378402, upload-time = "2026-10-09T08:23:36.537Z" }, + { url = "https://files.pythonhosted.org/packages/00/85/f6b5976c2878b752d0804d371684e0495a71de296b6dc6559e6fbaa4311a/pyarrow-26.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:a0e4e92eeb088f1d7c2c04d6c7de8434c75abb4b4ccf0bbcd045aa7164c68d93", size = 38733074, upload-time = "2026-10-09T08:23:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/81/bc/c90fcbbcf893631e23dab1b0fb3fa29a508a8614326571b03c0894eda00b/pyarrow-26.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:eaf9e7cc7ab59f6c760232bbde18f64d559bbc50544841303bfb32be53533297", size = 50929201, upload-time = "2026-10-09T08:23:50.507Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c1/0c1ff38ab7df1b2cf54cf0ad9f19a516c4e416c6c9b4c966cc2c9d587f77/pyarrow-26.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ab6914db225d7f399652ae1f08588dfbc9efe617612715701e3d9d5cfa5ca19f", size = 53951865, upload-time = "2026-10-09T08:23:57.692Z" }, + { url = "https://files.pythonhosted.org/packages/9f/70/6a6b170496925472adad45a32528770fc8632db35fc60d4edd1e9ce1be0b/pyarrow-26.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:41dd3661ef40790a78870052ad7a58ad827b27c67a4511f06962eb9e9b74d19b", size = 54496388, upload-time = "2026-10-09T08:24:05.23Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/033ef9dba80976820190e292a10a5a23e9406572b76bbeb4d685d90e5c8d/pyarrow-26.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6e949744dcfc2d379808f7013c5f9cafaf0f817656dff7d46c6931528dd1784b", size = 57411588, upload-time = "2026-10-09T08:24:12.043Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ff/a74892c50aaf1f9f744a84493e08a2f99221e77c39d2d4a926de21a99edf/pyarrow-26.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:4a5fa8dc70dd50808990ff36faf44088e357b353d86c7682dd92d4b78d4c97d5", size = 29237858, upload-time = "2026-10-09T08:24:58.106Z" }, + { url = "https://files.pythonhosted.org/packages/03/10/f0ee0976ef08a851a743c57608917ac9a47623f688b9ee0efe5429975ba1/pyarrow-26.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:e2a1856e9565fe2679863b372478c681806aebbf7d0a6e72f33e77f804e647d6", size = 36495870, upload-time = "2026-10-09T08:24:16.479Z" }, + { url = "https://files.pythonhosted.org/packages/27/ca/0bc431a509bf10b4472dbb94f4184752ecbbddeb7f467152dac0fdaed469/pyarrow-26.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:4bcba83299cb2b8f8e443d36c6ba6269a5034431879015fb0719495df8a14de2", size = 38819754, upload-time = "2026-10-09T08:24:20.875Z" }, + { url = "https://files.pythonhosted.org/packages/61/59/2be41d26af7a07fb71581fb753cae396403ba1a2978355fd553929d44a9a/pyarrow-26.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:3a4d235876f14b4136b4d616ec42eb469ea0d6ead336cae631aa1dd29b21c962", size = 50933671, upload-time = "2026-10-09T08:24:27.199Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cb/b6d5048cf3178be9678f5c9c60040199894b2f69c3439c87ced91fd24da9/pyarrow-26.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:210cc9b83888b87cdc8f793eebb264f22b20d0dedbedefc73b9687a7047b4747", size = 53906419, upload-time = "2026-10-09T08:24:33.536Z" }, + { url = "https://files.pythonhosted.org/packages/09/2b/23e30fbd776c81d18d134d2592eb60daca13e8a57ab087d0fa042f9d9f3d/pyarrow-26.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ca77c43ca55bfc9a4eeb1f0cd5f093f08731b77c24cdba0829035f084959b0bb", size = 54527960, upload-time = "2026-10-09T08:24:41.292Z" }, + { url = "https://files.pythonhosted.org/packages/e2/23/fce251cd6b0546dfc181b00d5c8ef1c95a8c4cae83266bc3dfd5f719c62c/pyarrow-26.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:290a74c48e9491b436fd5edacfadf357943f82aa45c81110bd83a69aab33d1cf", size = 57388010, upload-time = "2026-10-09T08:24:48.186Z" }, + { url = "https://files.pythonhosted.org/packages/44/a5/0126fb0ef8d59bf257bdd68bb41623b72afc6e81790a0b4ac863a0f58861/pyarrow-26.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:515a10dae2a1d236bc9c9209d0317acb6746ea63cd4f98704904af7156d90ed1", size = 29406123, upload-time = "2026-10-09T08:24:53.387Z" }, + { url = "https://files.pythonhosted.org/packages/ed/66/8ada1b5165359d84b4b9b5384742304d1081da670f77d458fd9c9b8a2161/pyarrow-26.0.0-cp315-cp315-macosx_12_0_arm64.whl", hash = "sha256:e890816e5ee89c74a0f8b9379fe8b5ba83f46132b2a0bbb9b1c21359ec30dfda", size = 36373215, upload-time = "2026-10-09T08:25:03.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/83/74f10c3d803a6834b2acab21847724d4bdbc74d246eb17321432844707f3/pyarrow-26.0.0-cp315-cp315-macosx_12_0_x86_64.whl", hash = "sha256:9db18a9dc0af52135c9eac549d80a7a882696efbe5406cf882b044525d4ecc2e", size = 38730866, upload-time = "2026-10-09T08:25:07.924Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5a/ea2fa2163b1bd8ff73efd39c4060be63fd6ddec03e7887a471acd1e042a4/pyarrow-26.0.0-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:734312d3d99088d9ec28c5b17bad40389bd8373a1afc10acb60b83fd217af087", size = 50924443, upload-time = "2026-10-09T08:25:13.864Z" }, + { url = "https://files.pythonhosted.org/packages/78/80/8c47b6cf8cfd42826df65193eff026c1cc81fa6cb213a3c3f5d203e6f67a/pyarrow-26.0.0-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:24f892fdf1ae1942d69d3f7742e2f49960ec95277cfb1a70b8a1d91f4a96d935", size = 53948540, upload-time = "2026-10-09T08:25:19.305Z" }, + { url = "https://files.pythonhosted.org/packages/69/1f/3a506a76d944ec5c5e4b7f01d8d0446b392a6fb384de627a12e503f616b4/pyarrow-26.0.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:879331ddea2a26479fa18fade71e6facf684a6cf19f67daec3775c871569e8e5", size = 54494863, upload-time = "2026-10-09T08:25:24.517Z" }, + { url = "https://files.pythonhosted.org/packages/3d/50/08c4bb04d651788d2eaca78065743f4f6ded974d4ef96ae3c473993e9d0c/pyarrow-26.0.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:5b827650e874f1f9f9392524ea3e9e3e8a245de5ba64acca1f81ab188090afb9", size = 57409877, upload-time = "2026-10-09T08:25:31.157Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f3/c64781fbd7b6d3c07993b698c14944d0d195f07e800fa931c486ae6ab36a/pyarrow-26.0.0-cp315-cp315-win_amd64.whl", hash = "sha256:8e8e28c464552b5ca03e30d4504168c4425ce383884f8611b00e972f9fd933fc", size = 29236658, upload-time = "2026-10-09T08:26:22.607Z" }, + { url = "https://files.pythonhosted.org/packages/06/55/2ee3729daea999f19f061f03898d4895a242c4cd94f26e1324e5fdfbfe10/pyarrow-26.0.0-cp315-cp315t-macosx_12_0_arm64.whl", hash = "sha256:ce28748cbeb0f29c3ce9603782979c7117580fc76f16aa3ca448b38a22281adb", size = 36489011, upload-time = "2026-10-09T08:25:37.64Z" }, + { url = "https://files.pythonhosted.org/packages/6a/7d/3eb17f601f2bf13eda5f2ed28956379ca628b4dda97619cbb1cb1721622d/pyarrow-26.0.0-cp315-cp315t-macosx_12_0_x86_64.whl", hash = "sha256:106bb9290fc6fd9a84138a9440038ef184bac86463543c5ff099229cb30d996c", size = 38808480, upload-time = "2026-10-09T08:25:43.579Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e3/f0047360b0f4bfc031b256dc0aec3837a61f245b2fb70f8363438e2db665/pyarrow-26.0.0-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:2e4a413046eba9896e632925066c74095182200ba32e19ff0166bf64d2f936ac", size = 50923273, upload-time = "2026-10-09T08:25:51.445Z" }, + { url = "https://files.pythonhosted.org/packages/38/d9/56d9fb91210407df31cbeb9b91138601c88c7c8fb5f6bf773b20d65509bf/pyarrow-26.0.0-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:d58798c4d8d629700058e9afc1e16b9801023f3ce4dc1c92d945e79b5ffe4e98", size = 53900905, upload-time = "2026-10-09T08:25:59.554Z" }, + { url = "https://files.pythonhosted.org/packages/cf/40/8e8a7e9e027c731520c7eb179dd00a153b76ebf0bc11d213c6c8f8502851/pyarrow-26.0.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:645917e976671debabf854abab6e2b75c571ca4f82adc33a2d338697f7c27d93", size = 54518345, upload-time = "2026-10-09T08:26:07.125Z" }, + { url = "https://files.pythonhosted.org/packages/be/89/1e768a3fdb88d34e708ad2dc00dbf8e4e30290784eb84198d59308963bea/pyarrow-26.0.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7c3fda041e7078802589cf257750323ee3d0cd1e56e53a9b20ec845697fb3d28", size = 57379403, upload-time = "2026-10-09T08:26:13.624Z" }, + { url = "https://files.pythonhosted.org/packages/96/be/7b81a44d6a8e70581dcc1d6f01541f9000a973b1e5d75394aec91e7b179a/pyarrow-26.0.0-cp315-cp315t-win_amd64.whl", hash = "sha256:68cd662e9e2b00876a131950cf32336ace2d0865e1f9418763e3d3be8481dfa4", size = 29389953, upload-time = "2026-10-09T08:26:18.277Z" }, +] + [[package]] name = "pyasn1" version = "0.6.4" @@ -5552,6 +5674,187 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/34/98a2f52245f4d47be93b580dae5f9861ef58977d73a79eb47c58f1ad1f3a/xmltodict-1.0.4-py3-none-any.whl", hash = "sha256:a4a00d300b0e1c59fc2bfccb53d7b2e88c32f200df138a0dd2229f842497026a", size = 13580, upload-time = "2026-02-22T02:21:21.039Z" }, ] +[[package]] +name = "xxhash" +version = "4.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/e6/ed6c428055ffd3f9299db3ec8671fd78b70c24173c040b8faafb6348ebbb/xxhash-4.0.2.tar.gz", hash = "sha256:af3d1b3cfa5280e84d8968e9f30c9eca75781db4915eb6d6acb0dd4fc93e0f47", size = 101549, upload-time = "2026-10-19T06:00:08.459Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/fb/09726eee9268e78494cf031d6a37404f4efef2bcdf6b0c2ec6f737db089d/xxhash-4.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ec851cf4a64d98e09101a18190fd20de2928b2ae1541093236664a47fb101271", size = 38453, upload-time = "2026-10-19T06:01:15.916Z" }, + { url = "https://files.pythonhosted.org/packages/3f/28/d581b2308cb9c731644a86f25b33cba6e9e540d1d05e3c51fcc274f2df69/xxhash-4.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f2b2839a83b9a9d03ddf89f440bb43627832e77b9a4276988d8aa0bb49aed0df", size = 36206, upload-time = "2026-10-19T05:59:08.408Z" }, + { url = "https://files.pythonhosted.org/packages/cf/86/e2a23ad2eb1cf7155ec72b6af92a258c2b863c3f0337b1719d82c972a357/xxhash-4.0.2-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4c3834e32060ab46ad9cfe4981d48c306cd1f124b8efe0aed559a765f6e1d332", size = 253273, upload-time = "2026-10-19T05:58:00.641Z" }, + { url = "https://files.pythonhosted.org/packages/99/03/9380983dacb2ecd290044315433cd147be6821011621c05acd011eaf8be9/xxhash-4.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fa1febd6e8ad9eedfbbeac0cb5d86346ed0b78a846d822c3af36fddde503516b", size = 276523, upload-time = "2026-10-19T05:59:48.356Z" }, + { url = "https://files.pythonhosted.org/packages/98/c7/1dfd984f0be6554483d76f1c9622a0c1204805a781b2f0563feb7fd74b0a/xxhash-4.0.2-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:60208a7c31d936045e911ff0c3170ac0e432611b6800c220cc66b908c33b16dd", size = 296294, upload-time = "2026-10-19T05:58:19.019Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1b/7feb8790f30ab8215a047711216b77fae337993e8c70acb694aa577fb168/xxhash-4.0.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9b7b91d072e239d4d0c6d868ae4a2f75dee55856227638488db156a4b462588", size = 279949, upload-time = "2026-10-19T05:59:10.145Z" }, + { url = "https://files.pythonhosted.org/packages/f8/14/94373f5b7b59705e6d28d4bd819ec67a56615e4cb00d91520c9ab3031d09/xxhash-4.0.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e79bc2bef5c7ca6cf4c2561aab00340d369b80ea56976a4a4c6ce5e3ed8fb59d", size = 509831, upload-time = "2026-10-19T05:58:02.705Z" }, + { url = "https://files.pythonhosted.org/packages/f1/25/1a894b324cd7b8b9d05b69a546752e6e378667910ad25bf01dcd7b2e129e/xxhash-4.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f8e86a0b5e28418162dfa68d6dc4bee873ea2efbd6ee67789903a0ee37a81c14", size = 260984, upload-time = "2026-10-19T06:01:17.492Z" }, + { url = "https://files.pythonhosted.org/packages/d2/6a/aa0164da8cbcc491a8242f9dda4eb0d98631670b03e5967690b695da4716/xxhash-4.0.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:52fd50828b49caf80824fff67bdbd8cd2db7e033dd8b3edf024c98b326d2a8c4", size = 339603, upload-time = "2026-10-19T05:59:10.683Z" }, + { url = "https://files.pythonhosted.org/packages/5a/fb/4d2b91f69c72e96aa21ac6711576c55d040c800ee1d14cbbbe6bb3149b04/xxhash-4.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3e0d6d7ec5072ed81e54de4fa778e6c1c8fe70cf8ccfc3907abde59147fac84e", size = 272538, upload-time = "2026-10-19T05:58:02.16Z" }, + { url = "https://files.pythonhosted.org/packages/79/18/4ebad4cb309fbb1b2d6a64a667a5779da6ed947e4e71d5db9146a4e0ce75/xxhash-4.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3473e7ac274a9a63c90acd365dd4b481659c6e061db153851f6e5d7b61e264cf", size = 300403, upload-time = "2026-10-19T05:59:50.681Z" }, + { url = "https://files.pythonhosted.org/packages/10/79/8eec32702b7095adb7aa6103048bc781e8d5eb37dd3b2fe911b32fd65d57/xxhash-4.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:16e30b4a045f1456af45016a888fda09a45b5adb5606da0463a3a0d58130852d", size = 259267, upload-time = "2026-10-19T05:58:21.066Z" }, + { url = "https://files.pythonhosted.org/packages/fc/bf/03dd3e533918d6ec421b8b39b74d8f245ef3adc42dec3c6116ae6aeee088/xxhash-4.0.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b3428dc057d1d483723d597e60e778bc58dd117daf23b866c1a6b5debb8eb300", size = 278210, upload-time = "2026-10-19T05:59:12.221Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fe/6cc7aa5d2ca3c11e0c603384c478061df4c0803c8fbc1e1f121e3fc55a1b/xxhash-4.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5da15e375188f4239277559e4efaab4cf4434162e622b2ba1dea956457994cf7", size = 329829, upload-time = "2026-10-19T05:58:04.509Z" }, + { url = "https://files.pythonhosted.org/packages/2d/60/8933222747118222f3ab61123f831c0c4f5cc0ac21a7d344d10cf643c54c/xxhash-4.0.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:838dae9c2efa4f4cd0be082a1a4077aa3043b407d8bc72f82864cd0807bd9945", size = 477260, upload-time = "2026-10-19T06:01:19.353Z" }, + { url = "https://files.pythonhosted.org/packages/5c/92/36e01f03557bccfe3fa52c2d6478ade7b96b47171d0806bdac1045bc76bb/xxhash-4.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:42fcaec8c705d1ea362d4cf011aea0dad1ebe13096ed227f9cfecfcac396c72c", size = 257535, upload-time = "2026-10-19T05:59:13.346Z" }, + { url = "https://files.pythonhosted.org/packages/ff/75/97e4d3ac92fa7d46f1a8840d55acee5126f281260eca03b22d5fa3e18e57/xxhash-4.0.2-cp311-cp311-win32.whl", hash = "sha256:4d1fa6f15a913dbafa50f3ee143a13440cd0b9849a1bde290c72ebde7838c937", size = 34614, upload-time = "2026-10-19T05:58:03.604Z" }, + { url = "https://files.pythonhosted.org/packages/eb/20/6c0271610bec93dc62a24c15f023f81d1781bb1ee9df15daf8b649116d92/xxhash-4.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:c60fa1038814c3c682a8e3b68aeca598e7b90eb4a65d4a48b7e0e6fbae825507", size = 36982, upload-time = "2026-10-19T05:59:52.994Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ce/b99ca9bdb39ae49f44f29ef8fbf7fc01eabe7c9f526c6b61c5537907bb85/xxhash-4.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:3a5529d43d09851dffa38936040f19e938e8368ecaddc646035a1dc7801bfa09", size = 33209, upload-time = "2026-10-19T05:58:23.485Z" }, + { url = "https://files.pythonhosted.org/packages/58/32/7f74a5661a3029102178950b24f9b1baa259429f97a5d8ef855935472f58/xxhash-4.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cb66d346593cb6851498b9ed554582aede153e32c9835edb76995746e563ac6b", size = 38428, upload-time = "2026-10-19T05:59:15.002Z" }, + { url = "https://files.pythonhosted.org/packages/bf/79/0ad1d456bbfb30761f6fc174dca610dcd46ef1280a708ce1bdb5de8b5368/xxhash-4.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aeda53dfb6bcae94adb1f5df4246fe13b9c4202f531dd9d39e0d55b36ba8db0e", size = 36174, upload-time = "2026-10-19T05:58:05.938Z" }, + { url = "https://files.pythonhosted.org/packages/30/fd/ea481dcc75bdacdb4d4b1039007bc60a0c0d7cd999d28e3e19b61099a6fb/xxhash-4.0.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e956b1a4e8fd8ea6fdb823670bf6ee3eade44c5ba3bbca06aa67805e4815a9e6", size = 253093, upload-time = "2026-10-19T06:01:20.933Z" }, + { url = "https://files.pythonhosted.org/packages/19/9c/39619aa3a1d569ff5fd8b5ea34600451310a63bf23d158f74a4fbc9fdcd3/xxhash-4.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fce8df7958f0828bd78d2ecfd9b11510abff9edc0e28fa7f7ca0ddcee2f0a8b", size = 276506, upload-time = "2026-10-19T05:59:15.76Z" }, + { url = "https://files.pythonhosted.org/packages/9e/01/d9e445ac597f8b6acd6aefdfa21230919e9aa7f14bee3ee0d634c3f3bcb7/xxhash-4.0.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1ab636ddc4f257bb2713c078d3f6112f194e92d8a32442233ea957fdddb51de6", size = 297700, upload-time = "2026-10-19T05:58:05.221Z" }, + { url = "https://files.pythonhosted.org/packages/62/14/e7ceb1cdb9a643ebf6ca6d8b3a63b9bb43dbee8b362d0c6f9b3dd8318dec/xxhash-4.0.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9c25738cbab7ca51763f724db95bc7f50f3174793389fee7f97e154bd9c83", size = 280234, upload-time = "2026-10-19T05:59:55.319Z" }, + { url = "https://files.pythonhosted.org/packages/be/16/9c30c6d7caafb654632a5fdfb45dfe4133254749dfaccef88bcdd033efe1/xxhash-4.0.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:60f30791faec3e96c8ed5921045231f7d3355649af47809e5a75c8a17dba8b6b", size = 511017, upload-time = "2026-10-19T05:58:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c6/3a85fc0b821ed2dfcf6fb7cfdf07e0b261ada40374583a50abb3480f2863/xxhash-4.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c7c95dba490be3b4e5a2fa5595b4d4f088fe7b8726e5998b8c1d83b1f9eb3e4", size = 261797, upload-time = "2026-10-19T05:59:17.727Z" }, + { url = "https://files.pythonhosted.org/packages/bc/75/dce80778193b5d099ba01bc03725bf8095280e5b6ec434fdcc62124f7ecc/xxhash-4.0.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f53479d8e13f93e1e3ef0e933a1901cbb4d8745c61c3876ff5b7c544e40f13ed", size = 339860, upload-time = "2026-10-19T05:58:07.481Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/5fc8b33217e25a37f8dfad398f602d404781535759f7d9578871349bb476/xxhash-4.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d42a7b02f204d5133d38d173766ff47a5da46fcf2c3876718b819f1f5500a55f", size = 272850, upload-time = "2026-10-19T06:01:22.258Z" }, + { url = "https://files.pythonhosted.org/packages/47/f9/ddf4f2bbc143886edd7c66e73c0adc6be9fb9f138a12c77bea2665be744c/xxhash-4.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6049b09b27d9226dfe0a8b0dee2b867063c8df9a7ef64748bf48110d8fc896c2", size = 301421, upload-time = "2026-10-19T05:59:18.262Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ad/862e0c20ca8cbbe624e5c168ca0fccd0ab6b1e83d13dd9b4aa8b3e575264/xxhash-4.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:97903f3df852ca5840a26cc27beef15f9f93b0071e2aa550a03d8ee5c65e7765", size = 260139, upload-time = "2026-10-19T05:58:06.923Z" }, + { url = "https://files.pythonhosted.org/packages/f4/19/4c5a3b6698504159b7908662dc61b1f3ebed25135235a023a088a5483e85/xxhash-4.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:873ee53a7d8f020ddfcd3bc101707ae46fcd1eb702f809ff29a765d0f88b654f", size = 278211, upload-time = "2026-10-19T05:59:57.7Z" }, + { url = "https://files.pythonhosted.org/packages/86/92/732f5f9810132d6f9afb19160980a48d1fa51caa724352a4f170bf960a4a/xxhash-4.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e24e9093771e8a27089176f202991199ebedad8f4791a6e2abf88acd5c69bd9d", size = 330247, upload-time = "2026-10-19T05:58:29.31Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e2/137dbd91bd93281f45bf53e4319874a09692bc07380755913e4dd938db49/xxhash-4.0.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c06c5fd89304f2c274d5129bd3a555b78f7a8af5ea16edd5b1360010f3359bd4", size = 478537, upload-time = "2026-10-19T05:59:20.059Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9b/41b99dfc87602358fc4dbfa1517b7f72b5a9232bce1aa32d4208acaeb160/xxhash-4.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c37fcacb7f5b8590c822bbf3bc41690e40a7581d8c4bcea7b15148f0743e716d", size = 258155, upload-time = "2026-10-19T05:58:09.407Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b3/2bfc3c9121a522f6a5abb8f57800345c23a159822b64c7ea69debdf4846d/xxhash-4.0.2-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:79cfd336c2ecaceef1061e7a7b3da079eab5920ba1352a28be690e857f67f2ea", size = 20749, upload-time = "2026-10-19T06:01:23.513Z" }, + { url = "https://files.pythonhosted.org/packages/cc/25/85d482d35d3f778d25719fc704146e8c984955d44735cfbdf955db240c23/xxhash-4.0.2-cp312-cp312-win32.whl", hash = "sha256:515a5f4405a29bc447bcee2ee13455dfc67ce41bf8f1da5258bc75bbdfb6dee6", size = 34657, upload-time = "2026-10-19T05:59:20.564Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/1beef4ee02cad4b8f97d707c6632e80f35e935c6b880960985e038f42aba/xxhash-4.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:043dcb74ba30a068a2821c136b61262db67f1e1d174ff4dbe83a3e24996861e7", size = 37059, upload-time = "2026-10-19T05:58:08.545Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4d/075f92b562ac13469edd51acb676ac4dbfa3d85a03f842e4a026e29a5ae1/xxhash-4.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:202c18875cf93d652c506d0e7c8a50847713f963a8aa9c06ce5117bdd14ba771", size = 33219, upload-time = "2026-10-19T06:00:00.128Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cd/4fa514d99643a90d4f4886ed387689a4e47c7f9cc541611b16336e13c44a/xxhash-4.0.2-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:570ba2952f1fe3b4f7a272314db64903e5c9121032e1dbe20800c7e0e5a13145", size = 43621, upload-time = "2026-10-19T05:58:31.906Z" }, + { url = "https://files.pythonhosted.org/packages/ef/97/6634acaad90e17490974f44d9aa7ba99f8edc7bd1c59df4e0f6c59d4ee14/xxhash-4.0.2-cp313-cp313-android_24_x86_64.whl", hash = "sha256:5223b3d133b81197fdbede79601b9b0f0bdf07818a71b751e9d2480bdba72a70", size = 40639, upload-time = "2026-10-19T05:59:22.154Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ac/23fa9e10335f7000a053ed9818e3fa03b57b88bb6eaecc43d8d3097d7f9a/xxhash-4.0.2-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b91b42f62e6cf6c1aaec9cea6b9e4b8ae18119517242d22ffbd967b275f9d12d", size = 34691, upload-time = "2026-10-19T05:58:10.854Z" }, + { url = "https://files.pythonhosted.org/packages/09/d8/2f2db4b6166e5c982ed071821f40ffb15ea0ab45bd768f7e24eba814bb27/xxhash-4.0.2-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e553d545d05094f3950ef36b328f75f5ef8568a0438c26966d9a0f1fc187a42b", size = 35636, upload-time = "2026-10-19T06:01:24.654Z" }, + { url = "https://files.pythonhosted.org/packages/80/9b/605e93a007a03fe5f314a5033f3860f0597e7ea552f4a56006c4b9b72ca5/xxhash-4.0.2-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:56ee43c284daffc12d269b3fb36b1fc624c7ab007064a24d54099049ee71ce80", size = 37935, upload-time = "2026-10-19T05:59:22.612Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d2/426b35da1dda15fe12bd91d02031186036d7500e9c5c462825193621fe9f/xxhash-4.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d9cd501f5d6c737e26b8d45f0facff43d3b7f2f5a8c2ef1c88800f6f310da418", size = 38014, upload-time = "2026-10-19T05:58:10.101Z" }, + { url = "https://files.pythonhosted.org/packages/0c/4a/ad4ffa5a8189a809abee76b8541b71b26402e9d84dfe49090082edd3f919/xxhash-4.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f64380470531ca49aceb15179ea47dd7be2fc38e41207eca4f7d05e4d6c9f795", size = 35875, upload-time = "2026-10-19T06:00:02.345Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/2600f32d4dfc5ace4fd3f999a7ef1fd2c3e37d2be9888b1511594a5646e4/xxhash-4.0.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1837c43b06cd2823b44ad27e80e51bbceed172a4afd0c3cc2ef543f2be74155e", size = 259451, upload-time = "2026-10-19T05:58:33.978Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/69559862b9c5cb95b7e9d1851903d53ddc5a6e614ff4e951ac7d3ed45e4d/xxhash-4.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c04c1aeff5cbe56714dfa49aa5d2edbb69c1a1a798926307b8d6af86549e7534", size = 283951, upload-time = "2026-10-19T05:59:24.224Z" }, + { url = "https://files.pythonhosted.org/packages/56/cb/11026759b7a5e25864000df34ab9231ad7fb4f5ae675d60cc4ecab7d9756/xxhash-4.0.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b7f62733495384b424bae45ac457fc80d2143c80de2564416a2a7cd8a4dded4e", size = 303880, upload-time = "2026-10-19T05:58:12.205Z" }, + { url = "https://files.pythonhosted.org/packages/63/a5/6f7d0f3396c7c2157f24fc7d33ee270e769661375924a06f4b6ac63413d3/xxhash-4.0.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d8d913ecbd21dff2f47f458a8cd37beccb3e41504004341f42234829b973309", size = 287225, upload-time = "2026-10-19T06:01:26.165Z" }, + { url = "https://files.pythonhosted.org/packages/08/22/d226987ffd9f21ce8d1298fc7cc6280450f24cf6d866fe474d80291430d6/xxhash-4.0.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40fab713c1d2f5c5e7450b8731615d8580757599ebf1f8683b211310ed8e5ad4", size = 519846, upload-time = "2026-10-19T05:59:24.631Z" }, + { url = "https://files.pythonhosted.org/packages/7c/0b/223e8d9da7c857f6f906202f3d15337a2dee8db530b88aeec6ae45cc9665/xxhash-4.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61ded7dee4f95da69295c228fc18a65447f5741c45a315aa3f9a3adda6038611", size = 268559, upload-time = "2026-10-19T05:58:11.709Z" }, + { url = "https://files.pythonhosted.org/packages/60/a9/0f1586426d56234f0041e1caf80737b4c3d16aca7f352ddbd4d90a41d7af/xxhash-4.0.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9478e4ae8b1db3f6fd72a5d17fc5805fc0ad576e31b58e2acb6bdbeefda2a549", size = 344952, upload-time = "2026-10-19T06:00:04.581Z" }, + { url = "https://files.pythonhosted.org/packages/c7/91/30aaff8fab52f5bc1d179a0de0dd59684b98f556a290f7130c1c7f9771f2/xxhash-4.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:14873a759b512ff875e01e7f8bd187ff613362cf189c44ba604e5cff833a308f", size = 279938, upload-time = "2026-10-19T05:58:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/37/cf/92a6c96dd93c951bd566e6d2ca92dcf605a248c39313643aac03b6a8fd44/xxhash-4.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:17c05e116555586f584c6dce34af2517b6648d56ef5ae7e4d84ea1a19d6743ef", size = 307560, upload-time = "2026-10-19T05:59:26.172Z" }, + { url = "https://files.pythonhosted.org/packages/47/0a/80d985d851ca25d4a4dce47f6694389536e5914f67657b0a9933d3d0b7ff/xxhash-4.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d8dc7e4071794c0405dc6a40559dbe77d7eec1e4bbf6eed5dbebc4e6577d12f3", size = 265830, upload-time = "2026-10-19T05:58:13.82Z" }, + { url = "https://files.pythonhosted.org/packages/f8/09/c6c2142414380e2c3dba1f760e74cad2f1a18cb5fd6dbbbca61696dd935d/xxhash-4.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:983a14a70a018dcccd847e780ba630771d5525d7604b6f519c963634414180c9", size = 284401, upload-time = "2026-10-19T06:01:27.829Z" }, + { url = "https://files.pythonhosted.org/packages/aa/8e/577df6e23e50d19ab206b0b77143b52fff9572b2528fab0505945b537814/xxhash-4.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9a23f0c1dd31aa4136ef8a678a40e73dbaf070b54ee6f2a907b78ba40331b3c3", size = 335902, upload-time = "2026-10-19T05:59:26.864Z" }, + { url = "https://files.pythonhosted.org/packages/cc/35/a1a28980948d2835078e8f00ca7fe2c8573ee72ce667d5f3b2a5058fb9c2/xxhash-4.0.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4d5f0e2bf5257900f60339cf439a23b35fd29a45cb7effed4b9f1b87c094df20", size = 487007, upload-time = "2026-10-19T05:58:13.081Z" }, + { url = "https://files.pythonhosted.org/packages/95/7c/23fb89ca8fbf8919982f3d3c73e15ca2baeae1f423fe72a2723dceaefeef/xxhash-4.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:892a66641657cf9aa0ae5c2c88258bc281221a33bd4af34bed786eea23e06d83", size = 264315, upload-time = "2026-10-19T06:00:07.131Z" }, + { url = "https://files.pythonhosted.org/packages/02/d0/9a9af3ee99bca69378190f0c284d06e466cdfde4f31445db4ffd6b50bc95/xxhash-4.0.2-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:050c1d66587e8f1ce70ccb7f661ec117e01497d48da817c14543beba0b8c677e", size = 20561, upload-time = "2026-10-19T05:58:38.487Z" }, + { url = "https://files.pythonhosted.org/packages/73/94/010a372e7b00df7e4ddc46982d1ee600b5bb8a1379fd15e9f436f7aa9773/xxhash-4.0.2-cp313-cp313-win32.whl", hash = "sha256:9c22ed1ea72cc23cec070fc5b9c3574844e67236f3f6e5bf7058e512abefe086", size = 34435, upload-time = "2026-10-19T05:59:28.819Z" }, + { url = "https://files.pythonhosted.org/packages/4d/a0/f9b70971bfe7e2a6aeb606df800d564466c6ae0f0e9db5d7025eafc61f33/xxhash-4.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:99fec645143af9dc697620aebff07e99388c16d4208a2ee8b6a2a9bb7d68a21b", size = 36502, upload-time = "2026-10-19T05:58:16.297Z" }, + { url = "https://files.pythonhosted.org/packages/a6/1d/9790306d9ecabc6e1ff125bb9982fdf327fa6ff5e71b352de66551fdf94d/xxhash-4.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:12d5a3c0687c990124a98a1eb027655075deebd985388b4e8ba4302992b90850", size = 33291, upload-time = "2026-10-19T06:01:29.149Z" }, + { url = "https://files.pythonhosted.org/packages/48/3e/29973fa7eac59122c9104d71add006689657f4a4733684bd2b25fe4d2195/xxhash-4.0.2-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:52357b9fee08af3a5c81a793c97110d8be63baba91b88584beee400a2a0113c6", size = 43726, upload-time = "2026-10-19T05:59:29.603Z" }, + { url = "https://files.pythonhosted.org/packages/07/71/f859087e54bba4df27a3e16c380ba8071dd7b87771b9b9cb78dda01e93c7/xxhash-4.0.2-cp314-cp314-android_24_x86_64.whl", hash = "sha256:7779b0f388972029f0de4c83dee4523544b3ee33272425683a34c801c8afe6a3", size = 40728, upload-time = "2026-10-19T05:58:15.452Z" }, + { url = "https://files.pythonhosted.org/packages/41/9a/4b2751b5d9bd6f8000ec1ffe3ccdd6bbde958a9fb0d96ebfa5ec5a3ea553/xxhash-4.0.2-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:f99686036e0575b4bd1947f9efcf12c5bdf310b2348fb72db84a4861287df6fb", size = 34719, upload-time = "2026-10-19T06:00:09.211Z" }, + { url = "https://files.pythonhosted.org/packages/f5/bf/cb0eca1c15825dceef333e983dcc668ab6fc20577fe66e0c1cf3805b1b88/xxhash-4.0.2-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5d51b2159376d98e99f7a33e19aed3d58d30d24ca641086e2ca0d54875d92ea6", size = 35655, upload-time = "2026-10-19T05:58:40.819Z" }, + { url = "https://files.pythonhosted.org/packages/02/81/a9b1d02517ed1ed1418170e2dec78115f73bf332ba455dd76fff363973f3/xxhash-4.0.2-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:846189d5cc9a4e6815a04c595148b2a82721da1fcdacfdff13fb0225d0945ea3", size = 38077, upload-time = "2026-10-19T05:59:30.482Z" }, + { url = "https://files.pythonhosted.org/packages/d7/38/0df9b7001ef43d4ef8907a30ad49662d6fda45ff88a0684b169943d4dcff/xxhash-4.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf93a290a49285f75d0d6ec801aa247592a16bd043892a7b48d93f718dfc99c8", size = 38225, upload-time = "2026-10-19T05:58:18.369Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ea/6f587cfa43ac41c793bd1b30d2fed4ffbde2dfbff013059e898292fad323/xxhash-4.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:28ae2c583d923aa57c0a4301ea4e071bccf1f84b5e16a31f86b854d9f8943151", size = 35889, upload-time = "2026-10-19T06:01:31.327Z" }, + { url = "https://files.pythonhosted.org/packages/c3/4b/0f773dae678b5995010528461d06509a23eb35a6f43f7424c1c0634dc3fe/xxhash-4.0.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:66fda3f72176bd7d1d3aa71de1f4bc116276e3ca3ac583bf06e5f561f2b1fc74", size = 259575, upload-time = "2026-10-19T05:59:31.014Z" }, + { url = "https://files.pythonhosted.org/packages/94/b6/09d7ce830dd09e3f645eb8b53f45df3ab6187012b4114d652686806afedb/xxhash-4.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:829ae333a35219c24584755b8f5c718b87e4713ac4233c8531a4e5d3d67fa7b7", size = 284280, upload-time = "2026-10-19T05:58:17.847Z" }, + { url = "https://files.pythonhosted.org/packages/78/23/952144b4332e66319552dea8c33a4ddfe277d3808f59fc2cd62529fa95cc/xxhash-4.0.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:131d73e1467e9f782048d7f4901eb5ced6997e55ca761b503b598618271da8d8", size = 303957, upload-time = "2026-10-19T06:00:11.924Z" }, + { url = "https://files.pythonhosted.org/packages/81/f5/a5ca9a14090c2a4e1e7002755129ce155687130ed83d358376f1ab6fba79/xxhash-4.0.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3bdbde088f54a5cccb92f0d199e018bd676dbe2e5d2eabf1f9dc8148c6f297e2", size = 287505, upload-time = "2026-10-19T05:58:43.058Z" }, + { url = "https://files.pythonhosted.org/packages/48/56/2c4df935522c7928ff0c14708feed446cc3b81683664340a636c2f8af90d/xxhash-4.0.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dd86600a7083aecd5e0fee0680e5a328cbd1bf18046faa217e2a76cf2e01faf4", size = 519240, upload-time = "2026-10-19T05:59:32.166Z" }, + { url = "https://files.pythonhosted.org/packages/15/b7/f8075159633b5f2d47ab0376f2925c2cb184f4ffd3a88aa330fac7950ee8/xxhash-4.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318e6c3a31d75ccacd1f9b864e3b857d44c9f4bed5e9d5fb5db1dc2fec82f019", size = 268172, upload-time = "2026-10-19T05:58:20.346Z" }, + { url = "https://files.pythonhosted.org/packages/8f/5b/bbd53a3c9167d3e906d9801b0d2746d30c28c3e408a8f004bd3dcb327134/xxhash-4.0.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:360343b1d61efb44be005c2180cb19f8c0351d7dc63c8ebbdd8e219ca5cbbb4a", size = 345552, upload-time = "2026-10-19T06:01:32.499Z" }, + { url = "https://files.pythonhosted.org/packages/f1/bc/75c3e035d73c39cc45d128cacb5d7127e5d9da44a709994e59d009ee9889/xxhash-4.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:beb27fc243c6975ea44f74ea2e2c8c5014f9956ba0bf95ed6ee6dd3707e0add2", size = 280523, upload-time = "2026-10-19T05:59:32.621Z" }, + { url = "https://files.pythonhosted.org/packages/3c/92/654eb7b726a61f5cc77f2b8520d90af836611782475b67ff8d59c844b58f/xxhash-4.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:605b582e3895fc240b972d9a28606c53fb5444c118c9f6392bffe8795a4c2467", size = 307180, upload-time = "2026-10-19T05:58:19.838Z" }, + { url = "https://files.pythonhosted.org/packages/09/13/e1b71ca763cbc7c49e46d6d58e20cfa60adc13fde8b6d5cad25e2aa6689c/xxhash-4.0.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:68ed6108f9ef16bf90752582aadaef4c14caa00a50f8b340e282348365a351eb", size = 265863, upload-time = "2026-10-19T06:00:13.335Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/f59a9c24403c21bdb843130f61d9b0db2ef368cf64de031fd353c80bf15c/xxhash-4.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:07aa7644899f97e4ff208f54b9d57fe4c077bfdf2dea5bd5a4a165e9b57309f1", size = 284533, upload-time = "2026-10-19T05:58:45.808Z" }, + { url = "https://files.pythonhosted.org/packages/c6/2b/510b04251771d37c0928b72b290f7f58990007deb413d78f45ee0127004f/xxhash-4.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b97cc2b874d0656e88385fd7ba4c2dedd903f8e23767f69207fbdab79bd0861a", size = 336003, upload-time = "2026-10-19T05:59:34.961Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4d/67dd96c1e4a84b8e4261449ca922bc41a91a37029356095979f7b3446573/xxhash-4.0.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:987a83e7a7762bd8d40d6ee75bb047a33f6ed34a4269dd6bdda561666c3e0c54", size = 486652, upload-time = "2026-10-19T05:58:22.75Z" }, + { url = "https://files.pythonhosted.org/packages/09/51/f09c756ce81d13185e11ba20ea99e1f4ff7269e78ae1d7d3194bdbde95b6/xxhash-4.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f2b936ead8be8e1771b3a4508f5b8ba4087a57c59e3f6e91bbbb6849000ee302", size = 263737, upload-time = "2026-10-19T06:01:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f1/cd84ce202b42e3185ca8f74cb07e83d9191fbf1647fc273c2447bbfbd2fe/xxhash-4.0.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:c9d48e67fc623850ac65b350742b758085ea3aeada2f79aacd0200676abc29b3", size = 20542, upload-time = "2026-10-19T05:59:33.855Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f7/1be4a72302122cd65d44ba7845b2c49030ca85ed0367101002208688cebd/xxhash-4.0.2-cp314-cp314-win32.whl", hash = "sha256:74a92b9104c4af158ce37021b32cb165aa74e69c39bd10bacf569f39604bf2c9", size = 35133, upload-time = "2026-10-19T05:58:21.914Z" }, + { url = "https://files.pythonhosted.org/packages/df/db/95d66e6530e2983579a8eceabc40b812dba752c993eaece618f2c3bed64a/xxhash-4.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:2b5849d45e5c12ddd23316c999eafda73c3ad00b589c474285dcf4135205921e", size = 37133, upload-time = "2026-10-19T06:00:14.606Z" }, + { url = "https://files.pythonhosted.org/packages/3c/e8/142c229cd93414feaf84a02c6d6af1bcc111611117c8cb9ba0eaa44185ba/xxhash-4.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:eb90b2efd8d76777995ffdf3a134f079a79d59727a3301ca98f818fc142e841a", size = 34368, upload-time = "2026-10-19T05:58:48.006Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fa/ea1fbc3a8a9667f3458eaae7f9b461276b971ee2842d9c12393967bd4c46/xxhash-4.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:e83c54a9b8477079d6dbd3814ab444f43819ae1aa06b5098c1020d77747897c0", size = 38632, upload-time = "2026-10-19T05:59:36.344Z" }, + { url = "https://files.pythonhosted.org/packages/a0/0f/d83d50c32da714f714f5813335e2e7b2656e44c9da84fc903c41a2ffcab0/xxhash-4.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8c24094617758a4953fbf56e96637262b56d5007fcf34756ecb8d0b60231f53e", size = 36287, upload-time = "2026-10-19T05:58:25.044Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/31dc032654742df420546c0eafddb5e2e08a19774265ff876e4ae23b0c45/xxhash-4.0.2-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:88fb30a81b5c7cb4b4dac3b4974deb00de06f5ed4b15fbacf5c7e14d670ec049", size = 273710, upload-time = "2026-10-19T06:01:35.155Z" }, + { url = "https://files.pythonhosted.org/packages/5f/79/4ea0c32a32b2b2ad9cac1152463939c5453a870367d6ee9d809c645923ee/xxhash-4.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9c7c18d235991edcfa2275889e84a1458dff0b9315cb7351f5368c5b942bc0d", size = 301060, upload-time = "2026-10-19T05:59:35.389Z" }, + { url = "https://files.pythonhosted.org/packages/a7/5f/263f715b93b5b6a35da9eafce6bc385b94720c023e81a64dfa0e4cd8ad59/xxhash-4.0.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4d3a90ec615e72934be372bf734190626614929054809ac12e59950aca91d8a4", size = 312708, upload-time = "2026-10-19T05:58:24.197Z" }, + { url = "https://files.pythonhosted.org/packages/01/7e/9786187749ecf216baf7c6c4897756e83cd8e8aeb67bce53b118cd65e62d/xxhash-4.0.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdec072bdb457c65e4b96eab7610af72d1535ce38adb32b4e02fa6957d7cd2b8", size = 301401, upload-time = "2026-10-19T06:00:15.736Z" }, + { url = "https://files.pythonhosted.org/packages/43/65/edb9dabb49c9438922980a1ac76f28f047b1126e9bfb1355c6467f14631a/xxhash-4.0.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6efd3d8a3ee0b86dd7ce3e6ffe47dcc115f71c38cd9f7392e6387fa541a243e", size = 534468, upload-time = "2026-10-19T05:58:50.235Z" }, + { url = "https://files.pythonhosted.org/packages/f3/3c/973412a0e43b02a3538ba6c71ad1732f3b6ccff0dc1855655b36b6c98be4/xxhash-4.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52dff0cfe99e1f03614a40867e39d5c5a1d55047e114265db4ec2355d65fc542", size = 279586, upload-time = "2026-10-19T05:59:37.848Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c9/8b04ad1d7a5dd0e3141947897660abd429c9743ee83b2931fcb9ecaadc77/xxhash-4.0.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:49b93898657ba0dd4517576b5c94fb0aa6738200f6d1ec2dbc9d0981154828dc", size = 358893, upload-time = "2026-10-19T05:58:28.361Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e7/13900a89cbd65f9dd31032fe6df11aa385195cedcd9fd3c8346edb7eb57c/xxhash-4.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5065d7e74b7b3e3df03e11b47d9b14bdcdd0171603ae77cf39c89df140e3e948", size = 295297, upload-time = "2026-10-19T06:01:36.691Z" }, + { url = "https://files.pythonhosted.org/packages/dd/15/bc6b5a50c696575094b6081ec637a22170f2ad7b702b056729c1bf2e0110/xxhash-4.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:763a9b3447ece8a5a1e9e27749ef3910a2f6e8ac251b8983a3da2e0801956e02", size = 320215, upload-time = "2026-10-19T05:59:37.064Z" }, + { url = "https://files.pythonhosted.org/packages/9f/09/34efaa3a992a52ad2142a11a547c075af15705292a7d99c2e1f77d443773/xxhash-4.0.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f1a31825aa626279c25d797c105641d398865a93e96a042cb89c7faaa8905e90", size = 279403, upload-time = "2026-10-19T05:58:27.814Z" }, + { url = "https://files.pythonhosted.org/packages/ba/27/751da8c167e1410fc776e3da485a82f54696b1ac2c555cd9e4fbd4677338/xxhash-4.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3e229bf3c7fa330c84ca25bbc29a83a6d078ef2cf3b7b826a5c9b3828aac6f07", size = 297577, upload-time = "2026-10-19T06:00:16.937Z" }, + { url = "https://files.pythonhosted.org/packages/29/28/7eebb4bb64e8fa23b09a48e49470dd2090ae33beb93991d7be865119cdd1/xxhash-4.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fe86ae86241d0aee7561b19bf892c9250a976e4a56fc052405cd537180e7af15", size = 348508, upload-time = "2026-10-19T05:58:53.232Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/0cf94b43a664ad1cd24cacc6f37ede95b3f705e3b103b492cb9ce29c1c1a/xxhash-4.0.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:3d8b5da050138043edb95fbd4b79e240a0994d2f272e872cf074dc6acd68249b", size = 499287, upload-time = "2026-10-19T05:59:39.431Z" }, + { url = "https://files.pythonhosted.org/packages/8e/91/22aaa89dd14620376c17c9cee12aa94c3e7d746ab99af4b48c0482add9f5/xxhash-4.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf390edab741574cc770e0b587cc2d6f2573152ba09bf85fdae10e556333ac77", size = 274977, upload-time = "2026-10-19T05:58:30.499Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d6/43ed2405230cb1137c5c451151a3cfb5427423bbf6ad1b20e0de45e1804c/xxhash-4.0.2-cp314-cp314t-win32.whl", hash = "sha256:768aba59c7fb2e3e976560c91223cb01f00f83809088e451e59f05b841486fbf", size = 35463, upload-time = "2026-10-19T06:01:38.092Z" }, + { url = "https://files.pythonhosted.org/packages/ff/74/a20b6ee566d4ea3d81cbe99bc04a116f8c97246e1404f13b930b2ad9cdc4/xxhash-4.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:ca848e5edfe3aa31ea7355df790b25e55f910dbb540d587e738e1b0c644e1263", size = 37474, upload-time = "2026-10-19T05:59:38.345Z" }, + { url = "https://files.pythonhosted.org/packages/4c/9a/98b69e482b71a7c956fe7a748b118df4b268edb31b10b35e63878a467397/xxhash-4.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:04fe6c267bb6a04d8e5f56d8058eac0621359dc8fc03faa0c03cf1dd4343304e", size = 34621, upload-time = "2026-10-19T05:58:31.122Z" }, + { url = "https://files.pythonhosted.org/packages/d4/df/58b65a0a654bfa4da9a19168338fc89e30337f60ccb7b7d695fc2ebf7e67/xxhash-4.0.2-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:bdee724c52954f6d3a551adc30eac1555a6ab1444dd3e02bffcc51832cbcd5da", size = 43729, upload-time = "2026-10-19T06:00:18.211Z" }, + { url = "https://files.pythonhosted.org/packages/07/aa/d25b1a9f4f7dcd2330651a2253f6bc8aee5efdedd4839349bb734dcc2156/xxhash-4.0.2-cp315-cp315-android_24_x86_64.whl", hash = "sha256:79d832049ab43b88846a13161351887892f3d8cb0bcd3e7c91b3f125caada384", size = 40737, upload-time = "2026-10-19T05:58:55.45Z" }, + { url = "https://files.pythonhosted.org/packages/db/77/bac055f63b9be647f28d81ec040db14ee448ebac4c3eb2066b4170b68ea9/xxhash-4.0.2-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:fb3dce09d52103f29a4648649df1ab217e28ccc2e9a9c08e8daab472e5cda9c9", size = 34768, upload-time = "2026-10-19T05:59:40.791Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a3/eab44082caa9fc734eac6777a705b659c8de634c2a08da51765e48ccb639/xxhash-4.0.2-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:58d8fc67453d337a8cbeed4f5543493c3792cb5f3839c17bc08ecf307592466f", size = 35722, upload-time = "2026-10-19T05:58:32.75Z" }, + { url = "https://files.pythonhosted.org/packages/9a/0c/fd2f4090ee2466e9af1f5dd34b298f3d97463fbb537de066beb47ce961aa/xxhash-4.0.2-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4a5955614bf744ac52faaf535ff94df3e935ec057c6242a3d8aa4e1007efa0f0", size = 38079, upload-time = "2026-10-19T06:01:39.191Z" }, + { url = "https://files.pythonhosted.org/packages/73/77/dba9d1377dbab3c37f7302dccaf874fb7dcda0850ac6ca409aa95df39556/xxhash-4.0.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:eff48c792788e33ace8e8e3ef6a0db45741d6acdc35c9b89dc914469fa040536", size = 38228, upload-time = "2026-10-19T05:59:39.965Z" }, + { url = "https://files.pythonhosted.org/packages/5b/1a/d38f9b0876959b5dbb3940efd404ff7ff37297b9a494a4152f022292672d/xxhash-4.0.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:b881b67c01929df867d30fed94d4221205639072a63049818732c1bae54e74f0", size = 35966, upload-time = "2026-10-19T05:58:33.361Z" }, + { url = "https://files.pythonhosted.org/packages/f6/3c/f65cd98552eb469de81b74db65ad848a94f5259b915ead99ab84080b139d/xxhash-4.0.2-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7016f753d9c79d2ed53a1517b3eb1bd475f05d026fa3fbc3acec393d14ddd396", size = 264586, upload-time = "2026-10-19T06:00:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/83/84/324228db6b7b0ddeb527f0936d2aeb72664e0a6ce4129abdfb0e78444460/xxhash-4.0.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5a509325aea8802d2ab710a5f2fb7c4d7b1a6c90f9a148dbc9f84311a1aa15e", size = 285892, upload-time = "2026-10-19T05:58:57.311Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e4/52f06d36a699084c6743b18fdad46429a657aad83479a88f9a639041fdd7/xxhash-4.0.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c1087b8dc4a08709e1ce358a79671f9aa4d029211aff537d0f821e4a6483305", size = 306548, upload-time = "2026-10-19T05:59:42.377Z" }, + { url = "https://files.pythonhosted.org/packages/73/50/1945f2a0e39a80ac84ab40bb9fde7b83b4039077478119b504ac162d160c/xxhash-4.0.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:531a1056fdfae837ef56c81e62bb4a0ce3274b813a3164bb558151d913684cce", size = 287958, upload-time = "2026-10-19T05:58:34.589Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f2/8df0a257de50f8550d047b6c03b7df336147f33163dc1c5455ab5c325b44/xxhash-4.0.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fe3ae82b1c7d031da7bb300188fe31018fbc05e1e496e0edb7c930d6ecf88d1a", size = 520071, upload-time = "2026-10-19T06:01:40.79Z" }, + { url = "https://files.pythonhosted.org/packages/0a/26/8a5349232374825f418141292a3edda80ac7c5d818126eee5c594fffdb97/xxhash-4.0.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5fb7bfdd0d1136fb73e4efa9a1eaff71a72f45ff7409647f601bf714d7838922", size = 268246, upload-time = "2026-10-19T05:59:41.356Z" }, + { url = "https://files.pythonhosted.org/packages/d0/1f/3248f97b75c221d352bd37d70d5f96384f4cfa9ee375a7ed7956f7d1657f/xxhash-4.0.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9a1f6598dc8f7ccdc9bd5ac51264c71431185194e8c88042502a04c1f53b5780", size = 350146, upload-time = "2026-10-19T05:58:35.269Z" }, + { url = "https://files.pythonhosted.org/packages/a7/65/682ffa31455530fb402947eb8956406378dc853185348750c7cd1a1724a7/xxhash-4.0.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:329bf14bab110bf111aa7211c613a94e97e4554407d2cc964c01338278cd4213", size = 281953, upload-time = "2026-10-19T06:00:20.692Z" }, + { url = "https://files.pythonhosted.org/packages/66/cb/57c69db6dd0b9959b9ed28a2c73a659f5c621987ab153a824ae76f331d52/xxhash-4.0.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:fbf323d9c22c6ac25cd8c4633566393282fa22db583ef0320f82bded10d7cc2e", size = 307854, upload-time = "2026-10-19T05:58:59.316Z" }, + { url = "https://files.pythonhosted.org/packages/ba/63/755416cb522db17097e32f4d6871a358ba977bba77849efede09bd30a230/xxhash-4.0.2-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:1ba7e798c28e327b221270af073f7319c771daaf5974d18856c0ed3d0fa1c6de", size = 268216, upload-time = "2026-10-19T05:59:44.779Z" }, + { url = "https://files.pythonhosted.org/packages/27/77/d10ddc5642addbdd11c7e7ee4f02d2876d00b437bfba8e49cc2eff96e4f7/xxhash-4.0.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:b9bff92881cfd1d4f98f27e1275f125f90ddf324571ac9a3e35d1dfcf6f42d19", size = 284949, upload-time = "2026-10-19T05:58:36.835Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/dcbd582e7f6bdac6673a93ad7eff2309ad2a06d2f5f92922389a90d8a708/xxhash-4.0.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bb99a2c5c35cad78284e057f8207856c911ac088ff89d0807dccf07b65775895", size = 338056, upload-time = "2026-10-19T06:01:42.241Z" }, + { url = "https://files.pythonhosted.org/packages/55/28/3ab7cb9ec21f590cf56200facc8850260dfb96d5099919df7f93d4613983/xxhash-4.0.2-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:730fba89df435ac081bc2ae28e4fa0ca5009a9eb5f4947b648abc27acd96e7b3", size = 487603, upload-time = "2026-10-19T05:59:43.003Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d1/f48e74dbb0ca9d40565965845a5a6384216e238b7aa016f8a67dfe929e32/xxhash-4.0.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2ca60825069a19e381f4ed03d0e036907b2b98460d33d4f8d1a93cf2d7e37c7b", size = 263834, upload-time = "2026-10-19T05:58:37.629Z" }, + { url = "https://files.pythonhosted.org/packages/cf/72/cddec8f3243d53d2387e9529a9556e8fe820b411f87c791852ca2056c128/xxhash-4.0.2-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:89d02277a7d5ce8b3fe21182ce4e32cca54963768c4b9609ce29e1ac82922acf", size = 20549, upload-time = "2026-10-19T06:00:21.926Z" }, + { url = "https://files.pythonhosted.org/packages/26/6f/50f89c9174c2b5fae56a6ec1b81f1d8065ced4792974f9575daf5ef2629b/xxhash-4.0.2-cp315-cp315-win32.whl", hash = "sha256:b4c405512c813a47cf70e47c9886b07702a38e991756216b65ac19f9cbb4f8f8", size = 35136, upload-time = "2026-10-19T05:59:01.461Z" }, + { url = "https://files.pythonhosted.org/packages/77/a4/06a47cf94314277d27f2468888ab93d1b82e8334ad69281775d8b21a3944/xxhash-4.0.2-cp315-cp315-win_amd64.whl", hash = "sha256:30ffd0deaf0e9ea112c38656922323ae23dbaa90878cebdb243f646d121e52db", size = 37154, upload-time = "2026-10-19T05:59:47.022Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ec/9450480bef24ecb6dae89f69456b2d891c59cccf86a4756b2cbe70fdd4ad/xxhash-4.0.2-cp315-cp315-win_arm64.whl", hash = "sha256:6010eab32dd57dcb8070b81a5d459c422d09de3ffd1bf6f11f0ee381582fe5a5", size = 34365, upload-time = "2026-10-19T05:58:39.414Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6a/89f7d7b924e82b852815b04175923d7a2a83b7b993484003b5c9a09b8468/xxhash-4.0.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:fdc8799f1a4835b40c615cbc9ce73c8a42f51c3218726d285367dcf853f36128", size = 38589, upload-time = "2026-10-19T06:01:43.576Z" }, + { url = "https://files.pythonhosted.org/packages/28/c8/35487956f53073b94621cf0a3e157baaf4d9ed79a4b37aae73f26e8ef617/xxhash-4.0.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:aa60df2de8dc4f59ae4faddd77237514ff88d15c2afd6d501e21dbe82a4711db", size = 36357, upload-time = "2026-10-19T05:59:45.367Z" }, + { url = "https://files.pythonhosted.org/packages/9b/64/30e6b89adc1b37b0c40885d73682ec5f0a939ded3628f53e6b81e735daa1/xxhash-4.0.2-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2cf332a7b823e867cc0d8e4c69eb5c82ae96ed827677fe4d96f990a6a1fd86b8", size = 273536, upload-time = "2026-10-19T05:58:40.318Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ce/9dd64e3c06ef7116b3a10ee78fb56276f4382191b008772bd3ceaca80c0f/xxhash-4.0.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5846aa6fe15c5b1cda6738ef9b1ba3cf49028b729507ce44a1e9fc2642586b06", size = 299112, upload-time = "2026-10-19T06:00:23.08Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e2/e58e25d55f5d3d4786b50b537078c1eb57a2efe8aa079bbc55d7f785d196/xxhash-4.0.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:07abbc84c3b22064294e8d7f8e8d71a3c0dd3ae9aca8dfda55ad1977d3a7af61", size = 314503, upload-time = "2026-10-19T05:59:03.402Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a6/1d8948e5ebbd7d408b7e06edb0ae4f4fcbb9ac61fcb558bc49d28231a989/xxhash-4.0.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0c9ce49f326c85958df18789486d4c28812b52890e74d833b121735200d04bce", size = 299177, upload-time = "2026-10-19T05:59:49.215Z" }, + { url = "https://files.pythonhosted.org/packages/d3/a0/2b3b1c252264965ed7878d927547bf9969d553d75b92a272fba91d268cac/xxhash-4.0.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da1a626793a5fde9c97466d443ae7c62f3d9bdc8658fd8363f435852952996d3", size = 531942, upload-time = "2026-10-19T05:58:41.625Z" }, + { url = "https://files.pythonhosted.org/packages/20/18/7163affae311b049315549e3b894983289091dc62c9f72959607b5748bb1/xxhash-4.0.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:250048412cb196e1f705134dadbc415eca399e74d2678b935660be0b2e4fc380", size = 278740, upload-time = "2026-10-19T06:01:44.69Z" }, + { url = "https://files.pythonhosted.org/packages/42/48/0f318837dff3b56ed434ae0939da047c280805092b07d993750175f3ba74/xxhash-4.0.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b5fbcdf157caa642de731565310bae6a5c3b1e15f28507feb3eddf04bd4a56a9", size = 359859, upload-time = "2026-10-19T05:59:47.605Z" }, + { url = "https://files.pythonhosted.org/packages/99/2f/78c271392a474b5d24f3148ba111a74e36e91718c782a455b847453279af/xxhash-4.0.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:4d86d5aaae8312de06d25b91fbc901b891b3b5ce2f5c8359b340b4bcc3d01b8f", size = 293523, upload-time = "2026-10-19T05:58:42.495Z" }, + { url = "https://files.pythonhosted.org/packages/11/16/839eacbb24390476e95a03f6738b30a745a6b79c117e6d02c7e3367e7907/xxhash-4.0.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:2bc6cfaa4a39bfca4c31878ad769986d2d039673eaf527068f668e88ff65ab99", size = 318759, upload-time = "2026-10-19T06:00:24.336Z" }, + { url = "https://files.pythonhosted.org/packages/a3/ba/c7750692cb427911d3aad8447ff715b9721bcc8ae1de344a9153c7982d6b/xxhash-4.0.2-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:d695979588da837a36d5195d10812c4118515d33e4f41a9b4d0cd92bf7c2f716", size = 277562, upload-time = "2026-10-19T05:59:06.611Z" }, + { url = "https://files.pythonhosted.org/packages/29/65/70fefbf75329b5afb33b2bbf8035ca24eeba52f0f836e1b3d21aae153ac7/xxhash-4.0.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:00329536c281e13725283421dcdf4633f5710123e06a608b910d6a6f3bc891cf", size = 295093, upload-time = "2026-10-19T05:59:51.738Z" }, + { url = "https://files.pythonhosted.org/packages/09/3a/fbe36c6fa093c481cdc8299ba63f57c786b01a2bfbc3f2fe3d5caab56731/xxhash-4.0.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:8113a8cb51a5121b8017f275e9aff2cb9203d57e614c989f943f81937442896b", size = 347048, upload-time = "2026-10-19T05:58:44.217Z" }, + { url = "https://files.pythonhosted.org/packages/45/7f/587b3d99d5948889ecaa01a9d99cc7208ab150dead683f60345a28a48ef1/xxhash-4.0.2-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:438fdfda0fb1198b7e9a5b4da6b073f36cec5dd7129dd486bbe72c8ecd623adf", size = 497187, upload-time = "2026-10-19T06:01:46.21Z" }, + { url = "https://files.pythonhosted.org/packages/2b/87/738a71217aad9138fada7d16fc69b03325a202aebc95bb0b257d86ed9682/xxhash-4.0.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2f164b788017a4a766884aa9acb3490bbc5f03a79adc7d02894393df44675c1e", size = 274316, upload-time = "2026-10-19T05:59:49.967Z" }, + { url = "https://files.pythonhosted.org/packages/6c/c9/4c59e7436d2d4ce0a84e58cf03cbf6fac603052d1135ed67f125d26f0562/xxhash-4.0.2-cp315-cp315t-win32.whl", hash = "sha256:3d7f484b338ddf6a8e58ffb73c4f9d13ea157f01152fdfdf9c44c9a59666f3ac", size = 35462, upload-time = "2026-10-19T05:58:44.88Z" }, + { url = "https://files.pythonhosted.org/packages/6b/17/78057741b329c90877b242fafffcfbb0fb4d719b7ce706fd401ee033ae73/xxhash-4.0.2-cp315-cp315t-win_amd64.whl", hash = "sha256:33f89d2a64716d7108f49b38d92ebe5344d3c50440f15f48f0f65cbcf4ffb781", size = 37478, upload-time = "2026-10-19T06:00:25.786Z" }, + { url = "https://files.pythonhosted.org/packages/6b/07/c785f081244dbac609607f1f7ab883db45b26cd291d937d9bd8b67326db0/xxhash-4.0.2-cp315-cp315t-win_arm64.whl", hash = "sha256:26f8343b7a5d65f704029a0ebeb7e98a47149cf76dcab0072f0874307181bfb5", size = 34617, upload-time = "2026-10-19T05:59:09.137Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ab/f5e56048d06c1a42834978a4c17a004a202b092e2f72a632e68868fb9f52/xxhash-4.0.2-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:fe5a0d96cb339d383d3f720ffe94d8a7a51ccc8abf4d348a3ee058b5625f948e", size = 31816, upload-time = "2026-10-19T06:00:30.318Z" }, + { url = "https://files.pythonhosted.org/packages/17/b1/2351ddc0b45f983f5ea1d8b91024c932ce939a271b08d7f3bc8a54b3a81b/xxhash-4.0.2-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:86fdc2ae36f5d11446896c4456883b3740be2ec3ff56f700a7dc7aeb635e236f", size = 34431, upload-time = "2026-10-19T05:59:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/7d/9f/dc3d43fa9ae82435a5fae9647925e40ca2e6198f279a044f6119542cbf33/xxhash-4.0.2-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792e7c12ac0d300908a99f644e583f4091c3a5a0035ab7b1b76aadc908480381", size = 38470, upload-time = "2026-10-19T06:00:01.225Z" }, + { url = "https://files.pythonhosted.org/packages/8b/52/45d2a2ac310da915e1ef06cc3a823d904d49c7049fdd830c46b8f5c038bc/xxhash-4.0.2-graalpy312-graalpy250_312_native-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e359ff2f38be3c856a129c1c5c4cbd70e219188456c1bf266fd4f94e2baae278", size = 38187, upload-time = "2026-10-19T05:58:54.013Z" }, + { url = "https://files.pythonhosted.org/packages/84/77/a8779629eac4fd450dfd0f5a507941419dddb1a2b088288b790c497102bf/xxhash-4.0.2-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:6e736b549811153e3b54779708725c68e105a6e6860f9f30fcba896a5f28ef0c", size = 37109, upload-time = "2026-10-19T06:01:51.414Z" }, + { url = "https://files.pythonhosted.org/packages/fa/08/7273d1afe165f2035a6d9c3006d5f752df380e238f872d974c856ddc1c0c/xxhash-4.0.2-graalpy313-graalpy253_313_native-macosx_11_0_arm64.whl", hash = "sha256:ce0fd7c57fa4fe9e0a9c390a04e1668c09fc09253e8fd76f1785069508686059", size = 33915, upload-time = "2026-10-19T05:59:59.311Z" }, + { url = "https://files.pythonhosted.org/packages/95/0a/613ed5b8ded6fc2d42f54cf79f4318df1fec7d8fb0d3a9c189337220ad50/xxhash-4.0.2-graalpy313-graalpy253_313_native-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c45f489790caf482840d2c108d50b8b0d9e77f2f6d17e2b345e2ae3dc48849ff", size = 38377, upload-time = "2026-10-19T05:58:54.679Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4f/2325ba05fc5470dcc0c69e8621d82fe4474271448ba9d0f7599ab492a1ad/xxhash-4.0.2-graalpy313-graalpy253_313_native-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:87255720aa1a1be35256b688c5ee4151d3273894186cda5c8a5b82f1dc05debf", size = 37784, upload-time = "2026-10-19T06:00:31.759Z" }, + { url = "https://files.pythonhosted.org/packages/db/c6/2ca65d85a105f200e60a812fb53f70a4e4a40c2263763397a367186e6c07/xxhash-4.0.2-graalpy313-graalpy253_313_native-win_amd64.whl", hash = "sha256:2a99c2d36df2d9b238a27e1fd8547311dba51c997e4c7681a383a98c0831aee0", size = 36832, upload-time = "2026-10-19T05:59:18.738Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e6/f287c5a23fca3d9d67b4dbce2f95d6bb3b71324a33562ad89b78dc44f732/xxhash-4.0.2-pp311-pypy311_pp80-macosx_10_15_x86_64.whl", hash = "sha256:fe765b3c7e7fcd14ef9b77836295847cb73229cdbe7eced958ed013d2f1c4025", size = 36314, upload-time = "2026-10-19T05:59:21.327Z" }, + { url = "https://files.pythonhosted.org/packages/a5/af/a59b29a52c89c33a3eaa422d8bab34efb54be29c16d6ed00d9f4c05bd076/xxhash-4.0.2-pp311-pypy311_pp80-macosx_11_0_arm64.whl", hash = "sha256:eff2109cbac3a8b04000d1b687331ff538d9d72d266c5aede545fd6bf90d303d", size = 33498, upload-time = "2026-10-19T06:00:05.326Z" }, + { url = "https://files.pythonhosted.org/packages/0b/c7/d45fd7535e653f9049c0bb298cdf64c1a3686c91db0e4e9be215fb9dddb5/xxhash-4.0.2-pp311-pypy311_pp80-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ba2daf714d2c87374cfdab5ff22736b19cc12a200130b64610feb3b0b08bbe11", size = 47978, upload-time = "2026-10-19T05:58:57.911Z" }, + { url = "https://files.pythonhosted.org/packages/98/e1/0ef240bea04b7812054e672af744f782bb3ccbdf16507fdaba9337959937/xxhash-4.0.2-pp311-pypy311_pp80-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2b98f3de3e6ec78333c6d4db6b8d40634ce6dc05140815a195916385f912176", size = 42706, upload-time = "2026-10-19T06:01:53.886Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ed/f002761b873c372de3505d909cbd92b44d0e0e8fb68c04737c4cc9a2c1c1/xxhash-4.0.2-pp311-pypy311_pp80-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180b349bbc89af61240adab8519426e0cb6546c2487e4b26b464364bf59dcbed", size = 39511, upload-time = "2026-10-19T06:00:03.78Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1c/b9962ed46a2b08dde44425b115f83eac204fcaba97f76961bf093d9bf99a/xxhash-4.0.2-pp311-pypy311_pp80-win_amd64.whl", hash = "sha256:03b1e97d14fd1b185c75808f72dfc7d9ea5866df3f8661566f2a23d0d07dcf7e", size = 84328, upload-time = "2026-10-19T05:58:58.596Z" }, + { url = "https://files.pythonhosted.org/packages/2c/80/6608694dd7817963db9d9c611143d94888c3e7420d8901c962acd7fdfc1c/xxhash-4.0.2-pp312-pypy312_pp80-macosx_10_15_x86_64.whl", hash = "sha256:c9f3129e086297db6b24b83db59a0dbfdd8f325ffc7dcc184fc573473b13293f", size = 36460, upload-time = "2026-10-19T06:00:34.489Z" }, + { url = "https://files.pythonhosted.org/packages/a3/d7/a1acbd775c01f720bbe6368af480e26e3b40525a7eaf17810c0ce5e9e13f/xxhash-4.0.2-pp312-pypy312_pp80-macosx_11_0_arm64.whl", hash = "sha256:2acdb1cabc41140e97d4adba7422dbeb7fabef9caace27d56f8de662d505fdfa", size = 33690, upload-time = "2026-10-19T05:59:23.349Z" }, + { url = "https://files.pythonhosted.org/packages/9b/13/15190b981f474117adefe78cd30acceb24e6f83352069402d3d9c24cd53b/xxhash-4.0.2-pp312-pypy312_pp80-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2076eee9f06fcb611754f98b9c272046e4a8a3fcfde353575dd8e6e2f33e80ed", size = 47950, upload-time = "2026-10-19T06:00:07.951Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ff/28666972b4a78dca99a537cbd42abe50dfdeca4d6388984c841cfe1f933f/xxhash-4.0.2-pp312-pypy312_pp80-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b244e3e1d6eadb5061ce777632a822ed75c2fb6174c65882f5829f31b46e8c1", size = 42584, upload-time = "2026-10-19T05:59:00.064Z" }, + { url = "https://files.pythonhosted.org/packages/0a/30/0b46bb908376d75733ca70cd4124f74abeea0f50732749dafa848f42f385/xxhash-4.0.2-pp312-pypy312_pp80-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63522739ce94e4e73c1605aa60a52b3863e77e835f6292cc8a5e7e7b626ff00c", size = 39472, upload-time = "2026-10-19T06:01:55.015Z" }, + { url = "https://files.pythonhosted.org/packages/4c/23/6c0aa47774513c1237f7ea198daf36f2d2609f1e233249a9ea97d5b55c38/xxhash-4.0.2-pp312-pypy312_pp80-win_amd64.whl", hash = "sha256:0b96b2694c97084c5e2bff8a6a3dc3c2fb3b9a07178286e8d69869efe76eeb50", size = 84214, upload-time = "2026-10-19T06:00:06.386Z" }, +] + [[package]] name = "yarl" version = "1.25.1" From b9fa0c5c69d7949857ce97cc7b79491488126824 Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Tue, 27 Oct 2026 19:06:54 -0700 Subject: [PATCH 2/9] fix(training): end_turn waits for the score, probes go one at a time, and tool access joins the toolset - end_turn takes the reset deadline, not a tool's. The server answers it once the next turn opens or the verifiers have scored the episode. - factory lists each toolset's tools in turn, so a server that allows one session still sets up. - A task's toolset now includes its modify_env_tool_access changes for the played agent's role. Tasks that hide different tools from the same role get classes of their own. Co-Authored-By: Claude Opus 5.5 --- src/agent_env/openenv/environment.py | 10 ++++++--- src/agent_env/training/trl.py | 18 +++++++++++----- tst/unit/openenv/environment_test.py | 8 ++++++- tst/unit/training/trl_test.py | 31 +++++++++++++++++++++++++++- 4 files changed, 57 insertions(+), 10 deletions(-) diff --git a/src/agent_env/openenv/environment.py b/src/agent_env/openenv/environment.py index 4cff30857..4cd281621 100644 --- a/src/agent_env/openenv/environment.py +++ b/src/agent_env/openenv/environment.py @@ -50,11 +50,15 @@ class ServedTask: @property def toolset(self) -> dict[str, Any]: - """Where the played agent's tools come from: the envs it works in, its role in them, and whether its turns offer - ``bash`` in its sandbox. Tasks that agree on it offer the same tools, so a trainer can group them.""" + """Where the played agent's tools come from: the envs it works in, its role in them, the changes the task makes + to that role's tool access, and whether its turns offer ``bash`` in its sandbox. Tasks that agree on it offer + the same tools, so a trainer can group them.""" agent = self.spec.trainer_agents[0] [deploy] = [step for step in self.task.steps if step.type in DEPLOYS and step.agent_name == agent] - return {"envs": list(getattr(deploy, "env_ids", None) or []), "role": getattr(deploy, "role", None), + role = getattr(deploy, "role", None) + return {"envs": list(getattr(deploy, "env_ids", None) or []), "role": role, + "access": [{"env": step.env_id, "action": step.action, "tools": step.tools} for step in self.task.steps + if step.type == "modify_env_tool_access" and step.role == role], "bash": works_in_sandbox(self.task.steps, agent)} diff --git a/src/agent_env/training/trl.py b/src/agent_env/training/trl.py index 5bec0af55..11ddb8f73 100644 --- a/src/agent_env/training/trl.py +++ b/src/agent_env/training/trl.py @@ -124,10 +124,10 @@ def dataset_for(self, split: str) -> Dataset: @cached_property def factory(self) -> type[_Environment] | dict[str, type[_Environment]]: - """TRL's ``environment_factory``: an environment class per toolset, or the one class when every task has the - same. Building it plays one task of each toolset to list its tools, since TRL reads them from the class.""" - classes = on_loop(_gathered(self._environment(toolset) for toolset in self._toolsets.values()), - self._reset_timeout + CLOSE_SECONDS) + """TRL's ``environment_factory``: an environment class per toolset of the server's tasks, or the one class when + every task has the same. Building it plays one task of each toolset in turn to list its tools, since TRL reads + them from the class.""" + classes = on_loop(self._environments(), len(self._toolsets) * (self._reset_timeout + CLOSE_SECONDS)) return classes[0] if len(classes) == 1 else {each.__name__: each for each in classes} def close(self) -> None: @@ -140,6 +140,9 @@ def close(self) -> None: if self._server is not None: self._server.stop() + async def _environments(self) -> list[type[_Environment]]: + return [await self._environment(toolset) for toolset in self._toolsets.values()] + async def _environment(self, toolset: _Toolset) -> type[_Environment]: split, index = toolset.first client = await _opened(self.url, self._reset_timeout) @@ -265,7 +268,7 @@ async def _step(self, name: str, arguments: dict[str, Any]) -> str: try: await rollout.started result = await asyncio.wait_for(self._client.step( - {"type": "call_tool", "tool_name": name, "arguments": arguments}), self._shared.tool_timeout) + {"type": "call_tool", "tool_name": name, "arguments": arguments}), self._deadline(name)) except _Failed as e: await self._fail(rollout, str(e)) return FAILED @@ -277,6 +280,11 @@ async def _step(self, name: str, arguments: dict[str, Any]) -> str: return FAILED return self._answer(rollout, name, result) + def _deadline(self, name: str) -> float | None: + """A tool's deadline; ``end_turn`` has the session's, the reset deadline, since the server answers it once the + task's next turn opens or its verifiers have scored the episode.""" + return None if name == END_TURN else self._shared.tool_timeout + async def _finish(self, rollout: _Rollout) -> None: """Ends the turns the model left open with an empty reply, until the episode is scored; each call on the session has the reset's deadline.""" diff --git a/tst/unit/openenv/environment_test.py b/tst/unit/openenv/environment_test.py index 552591e8d..d03a8d254 100644 --- a/tst/unit/openenv/environment_test.py +++ b/tst/unit/openenv/environment_test.py @@ -89,7 +89,13 @@ async def test_the_task_api_lists_each_split_with_each_tasks_prompt_and_agent(te assert (env.num_tasks("smoke"), env.num_tasks("all")) == (1, 3) assert env.get_task("all", 2) == {"id": "dock-2", "name": "dock-2", "index": 2, "prompt": "Add crate 2.", "system_prompt": "You keep the items.", "agent": "alice", - "toolset": {"envs": ["items"], "role": "alice@example.com", "bash": False}} + "toolset": {"envs": ["items"], "role": "alice@example.com", "access": [], + "bash": False}} + hide = {"id": "hide", "type": "modify_env_tool_access", "env_id": "items", "action": "disable", + "role": "alice@example.com", "tools": ["add_item"]} + hidden = _task(ITEMS, ALICE, hide, {**hide, "id": "other", "role": "bob@example.com"}, TURN, REPLIES) + assert ServedTask(hidden, "hidden", EpisodeSpec.from_task(hidden), _score_at("replies")).toolset["access"] == [ + {"env": "items", "action": "disable", "tools": ["add_item"]}] assert [task["index"] for task in env.get_task_range("all", 1)] == [1, 2] assert [task["id"] for task in env.list_tasks("smoke")] == ["dock-0"] with pytest.raises(IndexError, match="split 'all' has 3 tasks, so it has no task 3"): diff --git a/tst/unit/training/trl_test.py b/tst/unit/training/trl_test.py index ac4183f19..9fbe0c1e2 100644 --- a/tst/unit/training/trl_test.py +++ b/tst/unit/training/trl_test.py @@ -14,9 +14,11 @@ from agentenv_protocol import EnvironmentTool from agentenv_protocol.a2a_agent import proxy as proxy_module +from agent_env.config.runtime import Config from agent_env.openenv.app import _app, _score_at from agent_env.openenv.environment import Served, ServedTask from agent_env.task import EpisodeSpec, Task +from agent_env.task_step.task_step import TaskStep from agent_env.training import trl from agent_env.training.trl import ENDED, FAILED, AgentEnv, tool_method from tst.unit.bundle._support import layout @@ -155,6 +157,33 @@ def test_a_tool_that_misses_its_deadline_leaves_the_rollout_unscored_and_the_env assert "rollout left unscored: wait didn't answer within 0.5s" in caplog.text +class _Slow(TaskStep): + """Takes a second, as a verifier that grades the episode after its last turn does.""" + + type = "trl_test_slow" + entity_refs = () + + @classmethod + def from_dict(cls, data): + return cls(**cls._base_from_dict(data)) + + async def execute(self, context): + await asyncio.sleep(1) + return context + + +def test_end_turn_waits_for_the_episodes_score_past_a_tools_deadline(served, monkeypatch): + registry = Config().task_step_registry() + monkeypatch.setattr(Config, "task_step_registry", lambda self: {**registry, _Slow.type: _Slow}) + env = served({"a": [ITEMS, ALICE, TURN, {"id": "slow", "type": _Slow.type}, REPLIES]}, tool_timeout=0.2) + environment = env.factory() + + environment.reset(**env.dataset[0]) + + assert environment.end_turn(response="Added a crate.") == ENDED + assert environment.get_reward() == 1.0 + + def test_a_server_at_capacity_refuses_the_reset_and_says_how_to_serve_more(served): env = served({"a": PLAYED}, max_concurrent_envs=1) first, second = env.factory(), env.factory() @@ -174,7 +203,7 @@ def test_no_server_at_the_url_says_how_to_serve_one(): def test_tasks_whose_agent_gets_other_tools_take_trls_multi_environment_mode(served, monkeypatch): _tool_sessions(monkeypatch, {"http://127.0.0.1:20031/items/mcp": ["add_item"], PANTRY_MCP: ["take_item"]}) - env = served({"a": PLAYED, "p": [PANTRY, {**ALICE, "env_ids": ["pantry"]}, TURN, REPLIES]}) + env = served({"a": PLAYED, "p": [PANTRY, {**ALICE, "env_ids": ["pantry"]}, TURN, REPLIES]}, max_concurrent_envs=1) assert {name: _tools(each) for name, each in env.factory.items()} == { "items": ["add_item", "end_turn", "get_reward", "reset"], From 51f62ce2c9d503cd1f3a9d4533d093cc05f88f9a Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Wed, 28 Oct 2026 08:21:03 -0700 Subject: [PATCH 3/9] fix(training): tools a later turn offers become methods, and changed instructions reach the model - factory plays its probe task through every turn, ending each one empty, and builds the class from every tool any turn offers. A tool a turn doesn't offer answers with the server's not-offered message, and each rollout's first turn is still checked against the probe's. - end_turn puts the next turn's system prompt ahead of its prompt when it changed, since TRL can't change the conversation's own. Co-Authored-By: Claude Opus 5.5 --- src/agent_env/training/trl.py | 58 ++++++++++++++++++++++++----------- tst/unit/training/trl_test.py | 44 +++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 19 deletions(-) diff --git a/src/agent_env/training/trl.py b/src/agent_env/training/trl.py index 11ddb8f73..b637a58ba 100644 --- a/src/agent_env/training/trl.py +++ b/src/agent_env/training/trl.py @@ -18,7 +18,7 @@ import time import weakref from collections.abc import Callable, Coroutine, Mapping -from dataclasses import dataclass +from dataclasses import dataclass, field from functools import cached_property, reduce from pathlib import Path from typing import Any, ClassVar, Literal @@ -125,8 +125,9 @@ def dataset_for(self, split: str) -> Dataset: @cached_property def factory(self) -> type[_Environment] | dict[str, type[_Environment]]: """TRL's ``environment_factory``: an environment class per toolset of the server's tasks, or the one class when - every task has the same. Building it plays one task of each toolset in turn to list its tools, since TRL reads - them from the class.""" + every task has the same. Building it plays one task of each toolset through, in turn, ending each of its turns + empty, since TRL reads the tools from the class: its methods are every tool a turn offers, and one a turn + doesn't offer answers so.""" classes = on_loop(self._environments(), len(self._toolsets) * (self._reset_timeout + CLOSE_SECONDS)) return classes[0] if len(classes) == 1 else {each.__name__: each for each in classes} @@ -146,17 +147,28 @@ async def _environments(self) -> list[type[_Environment]]: async def _environment(self, toolset: _Toolset) -> type[_Environment]: split, index = toolset.first client = await _opened(self.url, self._reset_timeout) + first, tools = None, {} try: - reset = await client.reset(split=split, index=index) - listed = reset if reset.done else await client.step({"type": "list_tools"}) + turn = await client.reset(split=split, index=index) + while not turn.done: + listed = await client.step({"type": "list_tools"}) + if listed.done: + turn = listed + break + first = first or frozenset(tool["name"] for tool in listed.observation["tools"]) + tools |= {tool["name"]: tool for tool in listed.observation["tools"] if tool["name"] not in tools} + turn = await client.step({"type": "call_tool", "tool_name": END_TURN, "arguments": {"response": ""}}) + if error := turn.observation.get("error"): + raise RuntimeError(f"task {index} of split {split!r} refused an empty {END_TURN}: " + f"{error.get('message')}") finally: with contextlib.suppress(Exception): await client.close() - if listed.done: + if first is None: raise RuntimeError(f"task {index} of split {split!r} ended before its tools could be listed: " - f"{_error_of(listed.observation)}") - methods = {tool["name"]: tool_method(tool) for tool in listed.observation["tools"]} - shared = _Shared(url=self.url, offered=frozenset(methods), tool_timeout=self._tool_timeout, + f"{_error_of(turn.observation)}") + methods = {name: tool_method(tool) for name, tool in tools.items()} + shared = _Shared(url=self.url, offered=first, tool_timeout=self._tool_timeout, reset_timeout=self._reset_timeout) self._shared.append(shared) environment = type(toolset.name, (_Environment,), {**methods, "_shared": shared}) @@ -178,8 +190,8 @@ class _Toolset: @dataclass(frozen=True, eq=False) class _Shared: - """What an environment class's instances share: the server, the tools every episode must offer, and the - deadlines.""" + """What an environment class's instances share: the server, the tools every episode's first turn must offer, and + the deadlines.""" url: str offered: frozenset[str] @@ -193,7 +205,8 @@ class _Failed(Exception): @dataclass class _Rollout: - started: asyncio.Task[None] + started: asyncio.Task[None] = field(init=False) + system: str | None = None done: bool = False reward: float | None = None failure: str | None = None @@ -243,14 +256,18 @@ async def _reset(self, split: str, index: int, expected: tuple[str | None, str] self._rollout = None if self._client is None: self._client = await _opened(self._shared.url, self._shared.reset_timeout) - self._rollout = _Rollout(asyncio.ensure_future(self._begin(split, index, expected))) + rollout = _Rollout() + rollout.started = asyncio.ensure_future(self._begin(rollout, split, index, expected)) + self._rollout = rollout - async def _begin(self, split: str, index: int, expected: tuple[str | None, str] | None) -> None: + async def _begin(self, rollout: _Rollout, split: str, index: int, + expected: tuple[str | None, str] | None) -> None: reset = await self._client.reset(split=split, index=index) if reset.done: raise _Failed(_error_of(reset.observation)) metadata = reset.observation.get("metadata") or {} - if expected is not None and (metadata.get("system_prompt") or None, metadata.get("prompt")) != expected: + rollout.system = metadata.get("system_prompt") or None + if expected is not None and (rollout.system, metadata.get("prompt")) != expected: raise _Failed("the turn's prompt differs from the dataset row's") listed = await self._client.step({"type": "list_tools"}) if listed.done: @@ -301,8 +318,9 @@ async def _finish(self, rollout: _Rollout) -> None: await self._fail(rollout, str(e) if isinstance(e, _Failed) else _why(e)) def _answer(self, rollout: _Rollout, name: str, result: Any) -> str: - """What the model sees of a tool call: the tool's text or error, the next turn's prompt for ``end_turn``, - or that the episode ended, which records its reward or why it has none.""" + """What the model sees of a tool call: the tool's text or error, the next turn's prompt for ``end_turn``, after + its system prompt when that changed, since TRL can't change the conversation's own; or that the episode ended, + which records its reward or why it has none.""" observation = result.observation if error := observation.get("error"): return error.get("message") or json.dumps(error) @@ -315,7 +333,11 @@ def _answer(self, rollout: _Rollout, name: str, result: Any) -> str: return ENDED answer = observation.get("result") or {} if name == END_TURN: - return answer.get("prompt") or "" + prompt, system = answer.get("prompt") or "", answer.get("system_prompt") or None + if system is None or system == rollout.system: + return prompt + rollout.system = system + return f"{system}\n\n{prompt}" return answer["data"] if isinstance(answer.get("data"), str) else json.dumps(answer) async def _fail(self, rollout: _Rollout, why: str) -> None: diff --git a/tst/unit/training/trl_test.py b/tst/unit/training/trl_test.py index 9fbe0c1e2..858160c92 100644 --- a/tst/unit/training/trl_test.py +++ b/tst/unit/training/trl_test.py @@ -11,7 +11,7 @@ from typing import Any, Literal import pytest -from agentenv_protocol import EnvironmentTool +from agentenv_protocol import EnvironmentTool, ToolResult from agentenv_protocol.a2a_agent import proxy as proxy_module from agent_env.config.runtime import Config @@ -120,6 +120,48 @@ def test_an_episode_that_fails_or_isnt_its_rows_is_left_unscored_and_the_environ "rollout left unscored: the turn's prompt differs from the dataset row's"] +class _SecondTurnTools: + """Stands in for the proxy's ``ToolSession``: a turn opens one session, and every second one opened also offers + ``take_item``, so an episode's second turn offers a tool its first doesn't.""" + + def __init__(self): + self.opened = 0 + + def __call__(self, *, mcp_url, headers=None, timeout): + self.opened += 1 + offered = ["add_item", *(["take_item"] if self.opened % 2 == 0 else [])] + + class _Session: + async def open(self): + return self + + async def list_tools(self): + return [EnvironmentTool(name=name, inputSchema={"type": "object"}) for name in offered] + + async def call_tool(self, name, arguments=None): + return ToolResult(content=[{"type": "text", "text": f"{name} done"}]) + + async def close(self): + pass + + return _Session() + + +def test_a_tool_a_later_turn_offers_is_a_method_and_its_new_instructions_reach_the_model(served, monkeypatch): + monkeypatch.setattr(proxy_module, "ToolSession", _SecondTurnTools()) + again = {**AGAIN, "system_prompt": "Now keep the barrels."} + env = served({"a": [ITEMS, ALICE, TURN, again, REPLIES]}) + environment = env.factory() + + assert _tools(env.factory) == ["add_item", "end_turn", "get_reward", "reset", "take_item"] + environment.reset(**env.dataset[0]) + assert "offers no tool 'take_item'" in environment.take_item() + assert environment.end_turn(response="Added a crate.") == "Now keep the barrels.\n\nAdd another." + assert environment.take_item() == "take_item done" + environment.end_turn(response="Took one.") + assert environment.get_reward() == 1.0 + + class _Hanging: """Stands in for the proxy's ``ToolSession``: every env offers ``wait``, whose call takes three seconds.""" From 37a156f97633236627c9ba02b1487dccfb2ab7cc Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Wed, 28 Oct 2026 08:34:11 -0700 Subject: [PATCH 4/9] fix(training): a probe's calls each have the reset deadline, with none on the whole probe Co-Authored-By: Claude Opus 5.5 --- src/agent_env/training/trl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/agent_env/training/trl.py b/src/agent_env/training/trl.py index b637a58ba..95d27a0ed 100644 --- a/src/agent_env/training/trl.py +++ b/src/agent_env/training/trl.py @@ -127,8 +127,8 @@ def factory(self) -> type[_Environment] | dict[str, type[_Environment]]: """TRL's ``environment_factory``: an environment class per toolset of the server's tasks, or the one class when every task has the same. Building it plays one task of each toolset through, in turn, ending each of its turns empty, since TRL reads the tools from the class: its methods are every tool a turn offers, and one a turn - doesn't offer answers so.""" - classes = on_loop(self._environments(), len(self._toolsets) * (self._reset_timeout + CLOSE_SECONDS)) + doesn't offer answers so. Each call on a probe's session has the reset deadline.""" + classes = on_loop(self._environments(), None) return classes[0] if len(classes) == 1 else {each.__name__: each for each in classes} def close(self) -> None: From 31489bb874505b6b8bb6fcec17249d77576449bb Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Wed, 28 Oct 2026 09:23:55 -0700 Subject: [PATCH 5/9] refactor(training): one wire contract, the tool methods in their own module, and transformers' read of them tested - trl.py takes end_turn's name from agent_env.openenv's END_TURN, and the Task API's toolset is a Toolset TypedDict that both sides use. The session's list_tools, call_tool and end_turn messages are built in one place each. - The JSON-Schema-to-method code moves to agent_env/training/_tools.py, which no trainer owns; trl.py keeps the TRL API and the sessions. - tools_test.py checks that transformers' get_json_schema, which TRL shows a model the tools through, reads each generated method back as the env describes it. transformers joins dev. - The README and the module say which TRL is verified (1.16, GRPOTrainer) and needed (1.15 or later). Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- pyproject.toml | 1 + src/agent_env/openenv/environment.py | 22 +++-- src/agent_env/training/_tools.py | 112 +++++++++++++++++++++ src/agent_env/training/trl.py | 141 +++++---------------------- tst/unit/training/tools_test.py | 84 ++++++++++++++++ tst/unit/training/trl_test.py | 45 +-------- uv.lock | 91 +++++++++++++---- 8 files changed, 316 insertions(+), 182 deletions(-) create mode 100644 src/agent_env/training/_tools.py create mode 100644 tst/unit/training/tools_test.py diff --git a/README.md b/README.md index 815432763..7deab3b39 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ A package of your own can add env types, providers, task steps and stores to age Every task in a bundle is an [OpenEnv](https://github.com/openenv/OpenEnv) env, with the `openenv` extra installed (`pip install 'agentenv-framework[openenv]'`). `agent_env.openenv.build_app(bundle)` returns the OpenEnv server for a bundle, a folder or an installed bundle's name, or for a mapping of split names to bundles. Any OpenEnv client resets it to a task, which deploys the task's envs and opens its agent's turn, acts through the envs' tools and an added `end_turn` tool, and gets the task's verifier score as its reward. -TRL's `GRPOTrainer` and AsyncGRPO train on such a server with the `trl` extra (`pip install 'agentenv-framework[trl]'`). `AgentEnv(url)` from `agent_env.training.trl` gives `dataset`, one row per task, and `factory`, the `environment_factory` whose environments play the tasks' episodes and own their reward, so the trainer needs no `reward_funcs`. `AgentEnv.local(bundle, max_concurrent_envs=...)` serves the bundle in the training process instead. The server must allow a session for every rollout the trainer generates at once (`MAX_CONCURRENT_ENVS`). OpenEnv 0.10 holds the trainer to mcp 1, and so to vLLM 0.29 or older. +TRL's trainers train on such a server with the `trl` extra (`pip install 'agentenv-framework[trl]'`): `GRPOTrainer`, verified with TRL 1.16, and AsyncGRPO, which takes the same `environment_factory`. Either needs TRL 1.15 or later, for an environment that owns its reward. `AgentEnv(url)` from `agent_env.training.trl` gives `dataset`, one row per task, and `factory`, the `environment_factory` whose environments play the tasks' episodes and own their reward, so the trainer needs no `reward_funcs`. `AgentEnv.local(bundle, max_concurrent_envs=...)` serves the bundle in the training process instead. The server must allow a session for every rollout the trainer generates at once (`MAX_CONCURRENT_ENVS`). OpenEnv 0.10 holds the trainer to mcp 1, and so to vLLM 0.29 or older. ```python from trl import GRPOConfig, GRPOTrainer diff --git a/pyproject.toml b/pyproject.toml index 47ecb0950..916b7fdc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,6 +100,7 @@ daytona = [ dev = [ "agentenv-framework[aws,boxd,daytona,gcp,openenv,sail,tensorlake,tenki,trl,vercel]", # exercise portable optional providers and stores in CI "fastmcp>=3.4,<4", # the standalone-fastmcp mount path of the protocol; <4 keeps mcp 1.x + "transformers>=5.2", # get_json_schema, which reads agent_env.training's tool methods as TRL shows them to a model "Pillow>=10.4", # the protocol's computer extra, whose tests the unit tier runs "av>=17", # the protocol's computer extra, whose tests the unit tier runs and which the recording tests decode with "griffelib==2.3.0", # the plugin API check; exact, since what it reports as a break changes between releases diff --git a/src/agent_env/openenv/environment.py b/src/agent_env/openenv/environment.py index 4cd281621..eef3788c7 100644 --- a/src/agent_env/openenv/environment.py +++ b/src/agent_env/openenv/environment.py @@ -9,7 +9,7 @@ from collections.abc import Callable, Coroutine from contextlib import AsyncExitStack from dataclasses import dataclass, field -from typing import Any +from typing import Any, TypedDict import httpx from openenv.core.env_server.interfaces import Environment @@ -39,6 +39,15 @@ NOT_RESET = "no episode has started; call reset before stepping" +class Toolset(TypedDict): + """Where a task's played agent gets its tools, as the Task API names it in each task's ``toolset``.""" + + envs: list[str] + role: str | None + access: list[dict[str, Any]] + bash: bool + + @dataclass(frozen=True) class ServedTask: """A task served as an episode, and its reward as a function of the scores it records, by step id.""" @@ -49,17 +58,18 @@ class ServedTask: reward: Callable[[dict[str, float]], float | None] @property - def toolset(self) -> dict[str, Any]: + def toolset(self) -> Toolset: """Where the played agent's tools come from: the envs it works in, its role in them, the changes the task makes to that role's tool access, and whether its turns offer ``bash`` in its sandbox. Tasks that agree on it offer the same tools, so a trainer can group them.""" agent = self.spec.trainer_agents[0] [deploy] = [step for step in self.task.steps if step.type in DEPLOYS and step.agent_name == agent] role = getattr(deploy, "role", None) - return {"envs": list(getattr(deploy, "env_ids", None) or []), "role": role, - "access": [{"env": step.env_id, "action": step.action, "tools": step.tools} for step in self.task.steps - if step.type == "modify_env_tool_access" and step.role == role], - "bash": works_in_sandbox(self.task.steps, agent)} + return Toolset(envs=list(getattr(deploy, "env_ids", None) or []), role=role, + access=[{"env": step.env_id, "action": step.action, "tools": step.tools} + for step in self.task.steps + if step.type == "modify_env_tool_access" and step.role == role], + bash=works_in_sandbox(self.task.steps, agent)) @dataclass diff --git a/src/agent_env/training/_tools.py b/src/agent_env/training/_tools.py new file mode 100644 index 000000000..6df1359e4 --- /dev/null +++ b/src/agent_env/training/_tools.py @@ -0,0 +1,112 @@ +"""Python methods for an env's tools, typed and documented from each tool's JSON Schema the way transformers' +``get_json_schema`` reads a function, which is how TRL shows a model an environment's tools. Built without generating +source; a method calls its instance's ``_call(name, arguments)``.""" + +from __future__ import annotations + +import inspect +import keyword +import operator +from collections.abc import Callable, Mapping +from functools import reduce +from typing import Any, Literal + +_TYPES: dict[str, Any] = {"string": str, "integer": int, "number": float, "boolean": bool, "object": dict, + "null": type(None)} +#: The names TRL reads as an environment's own methods rather than its tools. +RESERVED = frozenset({"reset", "get_reward"}) + + +def tool_method(tool: Mapping[str, Any]) -> Callable[..., str]: + """A method calling ``tool``, typed and documented from its JSON Schema the way transformers' ``get_json_schema`` + reads a function, so TRL shows the model the tool as the env describes it. Built without generating source.""" + name = tool["name"] + if not _callable_name(name) or name in RESERVED: + raise ValueError(f"the env's tool {name!r} can't be a TRL tool: TRL calls an environment's tools as its " + "methods, so a tool's name must be a public Python identifier other than reset and " + "get_reward. Rename the tool in the env") + schema = tool.get("input_schema") or {} + required = set(schema.get("required") or []) + parameters = [inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD)] + annotations: dict[str, Any] = {} + described = [] + for argument, spec in (schema.get("properties") or {}).items(): + if not _callable_name(argument): + raise ValueError(f"the env's tool {name!r} takes an argument {argument!r}, which TRL can't pass: an " + "argument's name must be a public Python identifier. Rename it in the env") + hint = _hint(spec, f"{name}.{argument}") + default = inspect.Parameter.empty + if argument not in required: + hint, default = hint | None, spec.get("default") + parameters.append(inspect.Parameter(argument, inspect.Parameter.KEYWORD_ONLY, default=default, annotation=hint)) + annotations[argument] = hint + described.append(f" {argument}: {_describe(argument, spec)}") + annotations["return"] = str + + def method(self: Any, **arguments: Any) -> str: + return self._call(name, arguments) + + method.__name__ = method.__qualname__ = name + method.__doc__ = (tool.get("description") or f"Call the {name} tool.") + ( + "\n\nArgs:\n" + "\n".join(described) if described else "") + method.__annotations__ = annotations + method.__signature__ = inspect.Signature(parameters, return_annotation=str) + return method + + +def _callable_name(name: str) -> bool: + return name.isidentifier() and not keyword.iskeyword(name) and not name.startswith("_") + + +def _hint(schema: Mapping[str, Any], where: str) -> Any: + if options := schema.get("anyOf") or schema.get("oneOf"): + return _union([_hint(option, where) for option in options]) + if "enum" in schema: + return Literal[tuple(schema["enum"])] + kind = schema.get("type") + if isinstance(kind, list): + return _union([_hint({**schema, "type": each}, where) for each in kind]) + if kind == "array": + items = schema.get("items") + return list[_hint(items, where)] if items else list + if kind is None: + return Any + if kind not in _TYPES: + raise ValueError(f"{where}: JSON Schema type {kind!r} has no Python type TRL can show the model") + return _TYPES[kind] + + +def _union(hints: list[Any]) -> Any: + return reduce(operator.or_, list(dict.fromkeys(hints))) + + +def _describe(name: str, spec: Mapping[str, Any]) -> str: + """One line, since transformers ends an argument's description at the next line that looks like another.""" + text = spec.get("description") or spec.get("title") or name + return " ".join(f"{text} {_fields(spec)}".split()) + + +def _fields(spec: Mapping[str, Any]) -> str: + """The fields of an object argument, or of an array's entries, which a Python type hint can't carry.""" + for option in [spec, *(spec.get("anyOf") or spec.get("oneOf") or [])]: + if option.get("type") == "array" and (items := option.get("items") or {}).get("properties"): + return "Each entry: " + _field_list(items) + if option.get("type") == "object" and option.get("properties"): + return "Fields: " + _field_list(option) + return "" + + +def _field_list(schema: Mapping[str, Any]) -> str: + required = set(schema.get("required") or []) + parts = [] + for name, spec in schema["properties"].items(): + kind = _kind(spec) + (", required" if name in required else "") + parts.append(f"{name} ({kind})" + (f": {spec['description']}" if spec.get("description") else "")) + return "; ".join(parts) + "." + + +def _kind(spec: Mapping[str, Any]) -> str: + if options := spec.get("anyOf") or spec.get("oneOf"): + return " or ".join(_kind(option) for option in options) + kind = spec.get("type", "any") + return " or ".join(kind) if isinstance(kind, list) else kind diff --git a/src/agent_env/training/trl.py b/src/agent_env/training/trl.py index 95d27a0ed..cd888ab80 100644 --- a/src/agent_env/training/trl.py +++ b/src/agent_env/training/trl.py @@ -1,27 +1,25 @@ """TRL's trainers on a bundle served as an OpenEnv env (``agent_env.openenv.build_app``, or the package ``openenv import`` writes): ``AgentEnv`` reads the server's tasks into a dataset, and makes the ``environment_factory`` whose environments each play one episode of a task over OpenEnv's WebSocket, offer its turn's tools as methods and own its reward. -``GRPOTrainer`` and AsyncGRPO take both as they are. Every session runs on one event-loop thread of this module's own, -since TRL calls an environment synchronously, from its own threads.""" +``GRPOTrainer`` takes both as they are, verified with TRL 1.16, and AsyncGRPO takes the same ``environment_factory``; +either needs TRL 1.15 or later, for an environment that owns its reward. Every session runs on one event-loop thread of +this module's own, since TRL calls an environment synchronously, from its own threads.""" from __future__ import annotations import asyncio import contextlib -import inspect import json -import keyword import logging -import operator import re import threading import time import weakref from collections.abc import Callable, Coroutine, Mapping from dataclasses import dataclass, field -from functools import cached_property, reduce +from functools import cached_property from pathlib import Path -from typing import Any, ClassVar, Literal +from typing import Any, ClassVar import httpx import uvicorn @@ -35,19 +33,19 @@ raise ImportError("agent_env.training.trl requires pip install 'agentenv-framework[trl]'") from exc from agent_env.openenv import build_app +from agent_env.openenv.environment import END_TURN as END_TURN_TOOL +from agent_env.openenv.environment import Toolset +from agent_env.training._tools import tool_method logger = logging.getLogger(__name__) FAILED = "The environment failed to answer, so this episode won't be scored." ENDED = "The episode has ended." -END_TURN = "end_turn" +END_TURN = END_TURN_TOOL.name CONNECT_SECONDS = 60.0 CLOSE_SECONDS = 300.0 _CAPACITY = re.compile(r"Server at capacity: \d+/(\d+)") -_TYPES: dict[str, Any] = {"string": str, "integer": int, "number": float, "boolean": bool, "object": dict, - "null": type(None)} -_RESERVED = {"reset", "get_reward"} _loop: asyncio.AbstractEventLoop | None = None _loop_lock = threading.Lock() @@ -151,13 +149,13 @@ async def _environment(self, toolset: _Toolset) -> type[_Environment]: try: turn = await client.reset(split=split, index=index) while not turn.done: - listed = await client.step({"type": "list_tools"}) + listed = await _list_tools(client) if listed.done: turn = listed break first = first or frozenset(tool["name"] for tool in listed.observation["tools"]) tools |= {tool["name"]: tool for tool in listed.observation["tools"] if tool["name"] not in tools} - turn = await client.step({"type": "call_tool", "tool_name": END_TURN, "arguments": {"response": ""}}) + turn = await _end_turn(client, "") if error := turn.observation.get("error"): raise RuntimeError(f"task {index} of split {split!r} refused an empty {END_TURN}: " f"{error.get('message')}") @@ -269,7 +267,7 @@ async def _begin(self, rollout: _Rollout, split: str, index: int, rollout.system = metadata.get("system_prompt") or None if expected is not None and (rollout.system, metadata.get("prompt")) != expected: raise _Failed("the turn's prompt differs from the dataset row's") - listed = await self._client.step({"type": "list_tools"}) + listed = await _list_tools(self._client) if listed.done: raise _Failed(_error_of(listed.observation)) if (offered := {tool["name"] for tool in listed.observation["tools"]}) != self._shared.offered: @@ -284,8 +282,7 @@ async def _step(self, name: str, arguments: dict[str, Any]) -> str: return ENDED try: await rollout.started - result = await asyncio.wait_for(self._client.step( - {"type": "call_tool", "tool_name": name, "arguments": arguments}), self._deadline(name)) + result = await asyncio.wait_for(_call_tool(self._client, name, arguments), self._deadline(name)) except _Failed as e: await self._fail(rollout, str(e)) return FAILED @@ -309,8 +306,7 @@ async def _finish(self, rollout: _Rollout) -> None: try: await rollout.started while not (rollout.done or rollout.failure): - result = await self._client.step( - {"type": "call_tool", "tool_name": END_TURN, "arguments": {"response": ""}}) + result = await _end_turn(self._client, "") if error := result.observation.get("error"): rollout.failure = f"{END_TURN}: {error.get('message')}" self._answer(rollout, END_TURN, result) @@ -434,6 +430,18 @@ async def _opened(url: str, timeout: float) -> GenericEnvClient: return client +async def _list_tools(client: GenericEnvClient) -> Any: + return await client.step({"type": "list_tools"}) + + +async def _call_tool(client: GenericEnvClient, name: str, arguments: dict[str, Any]) -> Any: + return await client.step({"type": "call_tool", "tool_name": name, "arguments": arguments}) + + +async def _end_turn(client: GenericEnvClient, response: str) -> Any: + return await _call_tool(client, END_TURN, {"response": response}) + + def _read_tasks(url: str) -> tuple[str, dict[str, list[dict[str, Any]]]]: """The server's env name, and each split's tasks as its Task API describes them.""" with httpx.Client(base_url=url, timeout=CONNECT_SECONDS) as http: @@ -462,7 +470,7 @@ def _toolsets(name: str, tasks: dict[str, list[dict[str, Any]]]) -> dict[str, _T return {key: _Toolset(name, first) for key, first in firsts.items()} named: dict[str, _Toolset] = {} for key, first in firsts.items(): - toolset = json.loads(key) + toolset: Toolset = json.loads(key) base = "+".join([*(env.rsplit("/", 1)[-1] for env in toolset["envs"]), *(["bash"] if toolset["bash"] else [])]) base = base or "agent" taken = {toolset.name for toolset in named.values()} @@ -498,99 +506,4 @@ def _why(error: BaseException) -> str: return f"{type(error).__name__}: {error}" if str(error) else type(error).__name__ -def tool_method(tool: Mapping[str, Any]) -> Callable[..., str]: - """A method calling ``tool``, typed and documented from its JSON Schema the way transformers' ``get_json_schema`` - reads a function, so TRL shows the model the tool as the env describes it. Built without generating source.""" - name = tool["name"] - if not _callable_name(name) or name in _RESERVED: - raise ValueError(f"the env's tool {name!r} can't be a TRL tool: TRL calls an environment's tools as its " - "methods, so a tool's name must be a public Python identifier other than reset and " - "get_reward. Rename the tool in the env") - schema = tool.get("input_schema") or {} - required = set(schema.get("required") or []) - parameters = [inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD)] - annotations: dict[str, Any] = {} - described = [] - for argument, spec in (schema.get("properties") or {}).items(): - if not _callable_name(argument): - raise ValueError(f"the env's tool {name!r} takes an argument {argument!r}, which TRL can't pass: an " - "argument's name must be a public Python identifier. Rename it in the env") - hint = _hint(spec, f"{name}.{argument}") - default = inspect.Parameter.empty - if argument not in required: - hint, default = hint | None, spec.get("default") - parameters.append(inspect.Parameter(argument, inspect.Parameter.KEYWORD_ONLY, default=default, annotation=hint)) - annotations[argument] = hint - described.append(f" {argument}: {_describe(argument, spec)}") - annotations["return"] = str - - def method(self: _Environment, **arguments: Any) -> str: - return self._call(name, arguments) - - method.__name__ = method.__qualname__ = name - method.__doc__ = (tool.get("description") or f"Call the {name} tool.") + ( - "\n\nArgs:\n" + "\n".join(described) if described else "") - method.__annotations__ = annotations - method.__signature__ = inspect.Signature(parameters, return_annotation=str) - return method - - -def _callable_name(name: str) -> bool: - return name.isidentifier() and not keyword.iskeyword(name) and not name.startswith("_") - - -def _hint(schema: Mapping[str, Any], where: str) -> Any: - if options := schema.get("anyOf") or schema.get("oneOf"): - return _union([_hint(option, where) for option in options]) - if "enum" in schema: - return Literal[tuple(schema["enum"])] - kind = schema.get("type") - if isinstance(kind, list): - return _union([_hint({**schema, "type": each}, where) for each in kind]) - if kind == "array": - items = schema.get("items") - return list[_hint(items, where)] if items else list - if kind is None: - return Any - if kind not in _TYPES: - raise ValueError(f"{where}: JSON Schema type {kind!r} has no Python type TRL can show the model") - return _TYPES[kind] - - -def _union(hints: list[Any]) -> Any: - return reduce(operator.or_, list(dict.fromkeys(hints))) - - -def _describe(name: str, spec: Mapping[str, Any]) -> str: - """One line, since transformers ends an argument's description at the next line that looks like another.""" - text = spec.get("description") or spec.get("title") or name - return " ".join(f"{text} {_fields(spec)}".split()) - - -def _fields(spec: Mapping[str, Any]) -> str: - """The fields of an object argument, or of an array's entries, which a Python type hint can't carry.""" - for option in [spec, *(spec.get("anyOf") or spec.get("oneOf") or [])]: - if option.get("type") == "array" and (items := option.get("items") or {}).get("properties"): - return "Each entry: " + _field_list(items) - if option.get("type") == "object" and option.get("properties"): - return "Fields: " + _field_list(option) - return "" - - -def _field_list(schema: Mapping[str, Any]) -> str: - required = set(schema.get("required") or []) - parts = [] - for name, spec in schema["properties"].items(): - kind = _kind(spec) + (", required" if name in required else "") - parts.append(f"{name} ({kind})" + (f": {spec['description']}" if spec.get("description") else "")) - return "; ".join(parts) + "." - - -def _kind(spec: Mapping[str, Any]) -> str: - if options := spec.get("anyOf") or spec.get("oneOf"): - return " or ".join(_kind(option) for option in options) - kind = spec.get("type", "any") - return " or ".join(kind) if isinstance(kind, list) else kind - - __all__ = ["AgentEnv"] diff --git a/tst/unit/training/tools_test.py b/tst/unit/training/tools_test.py new file mode 100644 index 000000000..f65e00d59 --- /dev/null +++ b/tst/unit/training/tools_test.py @@ -0,0 +1,84 @@ +"""The tool methods TRL calls an env's tools through: their signatures and docstrings from each tool's JSON Schema, the +schema transformers' ``get_json_schema`` reads back from them, which is what TRL shows a model, and the tools refused.""" + +import inspect +from typing import Any, Literal + +import pytest +from transformers.utils import get_json_schema + +from agent_env.training._tools import tool_method + +RESTOCK = {"name": "restock", "description": "Restock a shelf.", "input_schema": { + "type": "object", "required": ["shelf", "count"], + "properties": { + "shelf": {"type": "string", "enum": ["top", "bottom"], "description": "which shelf"}, + "count": {"type": "integer"}, + "labels": {"type": "array", "items": {"type": "string"}, "title": "Labels"}, + "note": {"type": ["string", "null"], "default": None, "description": "for the night shift"}, + "price": {"type": "number", "default": 1.5}, + "options": {"type": "object", "properties": {"rush": {"type": "boolean"}}, "required": ["rush"]}, + }, +}} +PLAN = {"name": "check_plan", "description": "Check a plan.", "input_schema": { + "type": "object", "required": ["plan"], + "properties": {"plan": {"description": "one entry per ship", "anyOf": [ + {"type": "array", "items": {"type": "object", "required": ["ship"], "properties": { + "ship": {"type": "integer", "description": "the ship's id"}, + "cranes": {"anyOf": [{"type": "integer"}, {"type": "null"}]}}}}, + {"type": "string"}]}}, +}} + + +def _shown(tool: dict[str, Any]) -> dict[str, Any]: + environment = type("Env", (), {tool["name"]: tool_method(tool)})() + return get_json_schema(getattr(environment, tool["name"]))["function"] + + +def test_a_tools_arguments_are_typed_and_documented_from_its_schema(): + method = tool_method(RESTOCK) + + parameters = inspect.signature(method).parameters + assert {name: (parameter.annotation, parameter.default) for name, parameter in parameters.items()} == { + "self": (inspect.Parameter.empty, inspect.Parameter.empty), + "shelf": (Literal["top", "bottom"], inspect.Parameter.empty), + "count": (int, inspect.Parameter.empty), + "labels": (list[str] | None, None), + "note": (str | None, None), + "price": (float | None, 1.5), + "options": (dict | None, None), + } + assert method.__doc__ == ( + "Restock a shelf.\n\nArgs:\n shelf: which shelf\n count: count\n labels: Labels\n" + " note: for the night shift\n price: price\n options: options Fields: rush (boolean, required).") + assert tool_method({"name": "ping"}).__doc__ == "Call the ping tool." + + +def test_transformers_reads_back_each_tool_as_the_env_describes_it(): + assert _shown(RESTOCK) == {"name": "restock", "description": "Restock a shelf.", "parameters": { + "type": "object", "required": ["shelf", "count"], "properties": { + "shelf": {"type": "string", "enum": ["top", "bottom"], "description": "which shelf"}, + "count": {"type": "integer", "description": "count"}, + "labels": {"type": "array", "items": {"type": "string"}, "nullable": True, "description": "Labels"}, + "note": {"type": "string", "nullable": True, "description": "for the night shift"}, + "price": {"type": "number", "nullable": True, "description": "price"}, + "options": {"type": "object", "nullable": True, "description": "options Fields: rush (boolean, required)."}, + }}, "return": {"type": "string"}} + assert _shown(PLAN)["parameters"] == {"type": "object", "required": ["plan"], "properties": {"plan": { + "anyOf": [{"type": "array", "items": {"type": "object"}}, {"type": "string"}], + "description": "one entry per ship Each entry: ship (integer, required): the ship's id; cranes (integer or " + "null)."}}} + assert _shown({"name": "ping"}) == {"name": "ping", "description": "Call the ping tool.", + "parameters": {"type": "object", "properties": {}}, "return": {"type": "string"}} + + +@pytest.mark.parametrize(("tool", "message"), [ + ({"name": "get-situation"}, "can't be a TRL tool"), + ({"name": "reset"}, "can't be a TRL tool"), + ({"name": "_inspect"}, "can't be a TRL tool"), + ({"name": "plan", "input_schema": {"properties": {"ship-id": {"type": "integer"}}}}, "argument 'ship-id'"), + ({"name": "upload", "input_schema": {"properties": {"file": {"type": "file"}}}}, r"upload\.file: JSON Schema"), +], ids=lambda value: value["name"] if isinstance(value, dict) else "") +def test_a_tool_trl_cant_show_the_model_is_refused(tool: dict[str, Any], message): + with pytest.raises(ValueError, match=message): + tool_method(tool) diff --git a/tst/unit/training/trl_test.py b/tst/unit/training/trl_test.py index 858160c92..6a195eb71 100644 --- a/tst/unit/training/trl_test.py +++ b/tst/unit/training/trl_test.py @@ -1,14 +1,12 @@ """``AgentEnv`` on the OpenEnv server ``build_app`` makes, served on loopback over the Episode API's fake steps and used as TRL's trainers use it: the dataset and the environment classes, rollouts reset, stepped and scored on a reused environment, the turns a model leaves open, episodes left unscored, a server at capacity or missing, tasks with other -tools, and a bundle served by ``AgentEnv.local`` whose episodes end at close and at exit. Then the tool methods TRL -reads a tool's schema from.""" +tools, and a bundle served by ``AgentEnv.local`` whose episodes end at close and at exit.""" import asyncio import inspect import json import logging -from typing import Any, Literal import pytest from agentenv_protocol import EnvironmentTool, ToolResult @@ -20,7 +18,7 @@ from agent_env.task import EpisodeSpec, Task from agent_env.task_step.task_step import TaskStep from agent_env.training import trl -from agent_env.training.trl import ENDED, FAILED, AgentEnv, tool_method +from agent_env.training.trl import ENDED, FAILED, AgentEnv from tst.unit.bundle._support import layout from tst.unit.openenv.environment_test import AGAIN from tst.unit.task.episode.run_test import ALICE, ITEMS, PANTRY, PANTRY_MCP, REPLIES, TURN, _tool_sessions @@ -282,42 +280,3 @@ def test_a_bundle_served_by_local_plays_and_its_episodes_left_running_end_at_clo env.close() if end == "close" else trl._close_all() assert (len(teardowns.finished), environment._client, trl._servers) == (3, None, set()) - - -def test_a_tools_arguments_are_typed_and_documented_from_its_schema(): - method = tool_method({"name": "restock", "description": "Restock a shelf.", "input_schema": { - "type": "object", "required": ["shelf", "count"], - "properties": { - "shelf": {"type": "string", "enum": ["top", "bottom"], "description": "which shelf"}, - "count": {"type": "integer"}, - "labels": {"type": "array", "items": {"type": "string"}, "title": "Labels"}, - "note": {"type": ["string", "null"], "default": None, "description": "for the night shift"}, - "options": {"type": "object", "properties": {"rush": {"type": "boolean"}}, "required": ["rush"]}, - }, - }}) - - parameters = inspect.signature(method).parameters - assert {name: (parameter.annotation, parameter.default) for name, parameter in parameters.items()} == { - "self": (inspect.Parameter.empty, inspect.Parameter.empty), - "shelf": (Literal["top", "bottom"], inspect.Parameter.empty), - "count": (int, inspect.Parameter.empty), - "labels": (list[str] | None, None), - "note": (str | None, None), - "options": (dict | None, None), - } - assert method.__doc__ == ( - "Restock a shelf.\n\nArgs:\n shelf: which shelf\n count: count\n labels: Labels\n" - " note: for the night shift\n options: options Fields: rush (boolean, required).") - assert tool_method({"name": "ping"}).__doc__ == "Call the ping tool." - - -@pytest.mark.parametrize(("tool", "message"), [ - ({"name": "get-situation"}, "can't be a TRL tool"), - ({"name": "reset"}, "can't be a TRL tool"), - ({"name": "_inspect"}, "can't be a TRL tool"), - ({"name": "plan", "input_schema": {"properties": {"ship-id": {"type": "integer"}}}}, "argument 'ship-id'"), - ({"name": "upload", "input_schema": {"properties": {"file": {"type": "file"}}}}, r"upload\.file: JSON Schema"), -], ids=lambda value: value["name"] if isinstance(value, dict) else "") -def test_a_tool_trl_cant_show_the_model_is_refused(tool: dict[str, Any], message): - with pytest.raises(ValueError, match=message): - tool_method(tool) diff --git a/uv.lock b/uv.lock index 7b3ac0867..822db7e44 100644 --- a/uv.lock +++ b/uv.lock @@ -110,6 +110,7 @@ dev = [ { name = "tenki", extra = ["async"] }, { name = "tensorlake" }, { name = "tomli-w" }, + { name = "transformers" }, { name = "vercel-sandbox" }, ] gcp = [ @@ -196,6 +197,7 @@ requires-dist = [ { name = "tenki", extras = ["async"], marker = "extra == 'tenki'", specifier = "~=1.4.1" }, { name = "tensorlake", marker = "extra == 'tensorlake'", specifier = "~=0.5.140" }, { name = "tomli-w", marker = "extra == 'dev'", specifier = "==1.2.0" }, + { name = "transformers", marker = "extra == 'dev'", specifier = ">=5.2" }, { name = "uvicorn", specifier = ">=0.47" }, { name = "vercel-sandbox", marker = "extra == 'vercel'", specifier = ">=0.7.0,<0.8" }, ] @@ -4756,6 +4758,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2e/a3/0f0b7d78e2f1eb9e8e1afbff1d2bff8d60144aee17aca51c065b516743dd/safehttpx-0.1.7-py3-none-any.whl", hash = "sha256:c4f4a162db6993464d7ca3d7cc4af0ffc6515a606dfd220b9f82c6945d869cde", size = 8959, upload-time = "2025-10-24T18:30:08.733Z" }, ] +[[package]] +name = "safetensors" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/45/06/f955dbbb1859e3bd23c8ac6141af5106e7ad5fedec4a3a6e3d60f94b7001/safetensors-0.8.0.tar.gz", hash = "sha256:fabaf3e0f18a6618d9b36560682562157f77c2b71fcffc7b432be2baed9d753d", size = 325846, upload-time = "2026-06-09T07:52:25.563Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/a0/f718cda65b05407d228f97602cf60dca269c979867aa5beb25410de26cd3/safetensors-0.8.0-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c554f85858e05226d3c2828e32395e677434685d6d94594a41643361c5e837f0", size = 473568, upload-time = "2026-06-09T07:52:18.829Z" }, + { url = "https://files.pythonhosted.org/packages/f5/b1/fa7c600e7dceae12e9606c7578cbc9ff1e1ed55844883ee5c92205e86226/safetensors-0.8.0-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:c80201d22cbf405b80647a60ada77bba06c8fba2da2743ba1e89cdcc39a81f25", size = 484562, upload-time = "2026-06-09T07:52:17.518Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/65a7de0af421317bb36a067241e4235fff194eed60b961ed6d3f59a3fc60/safetensors-0.8.0-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a46e5ff292c356d6991e60942ba7f79817682d3a2cef0702136448cb9c4d235", size = 502844, upload-time = "2026-06-09T07:52:07.624Z" }, + { url = "https://files.pythonhosted.org/packages/91/4f/3175c9d75634e0e0dda0082794193521035edd7c70a6f212bf33ca06ddf4/safetensors-0.8.0-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4124502b78f03534117c848f87a39b8f31e577b15eff423bf8bfb95f2a8c30d0", size = 511823, upload-time = "2026-06-09T07:52:09.565Z" }, + { url = "https://files.pythonhosted.org/packages/20/87/846c289e7aa2299eff406335717cf43ce8777194ece8aad75772e0411615/safetensors-0.8.0-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bc0a787ba8a35be368ee3574edfa2b1ad389eebd0a72e482ae275490e3f6c98", size = 633461, upload-time = "2026-06-09T07:52:11.128Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/8d64d9df2c45d5ded401df889d0ad90882804ca172d79ec4f0df8f727fe0/safetensors-0.8.0-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:040070828e36dc8e122178bbbd5830ff9e97920affb84cbe0f46442497bed358", size = 545148, upload-time = "2026-06-09T07:52:13.603Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/f203ff3a3ddfe19308efc83c5a3a29ed02bf786732ec35e68bf9162f3365/safetensors-0.8.0-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd6f3f93c9a0a7cc2788ee63fb763353d4bd2e89b0751bc78fcf7dda00bea774", size = 516040, upload-time = "2026-06-09T07:52:16.29Z" }, + { url = "https://files.pythonhosted.org/packages/46/fb/cdaed17ceb2948784fd9c36b6fd3e951b608547cea81a48e8ee6f8cfdfcb/safetensors-0.8.0-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:fcdd41ec4628fee5799f807c73c353629130fbd942aa23d83c623dd6c9d52d78", size = 513832, upload-time = "2026-06-09T07:52:12.37Z" }, + { url = "https://files.pythonhosted.org/packages/0d/49/1e15de264dcc3b77943d2d0c56a95809956883b1c2d6d585c792523f180b/safetensors-0.8.0-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8e9f537aa183a38ace122d27303dcd986b26bd2a7591f9181d7f0c396f4677ca", size = 559930, upload-time = "2026-06-09T07:52:14.743Z" }, + { url = "https://files.pythonhosted.org/packages/2a/43/bf38443278eab4b1be1fce2931e2b012ad9cb7df52ada751d0aab8f7659a/safetensors-0.8.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:87eec7ffed2b809f05a398a8becb7d013f19f7837cd15d9748580d6cf30dbaf4", size = 678670, upload-time = "2026-06-09T07:52:20.032Z" }, + { url = "https://files.pythonhosted.org/packages/72/e3/68cd3fa5b48488e84add63e04cb12f3bc28ae4638c06d4508c6e88823d0e/safetensors-0.8.0-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:4a95ae2b05d7726d751da4ebf626a2ca782b706e101bd894c95bc2450b1cffcc", size = 786679, upload-time = "2026-06-09T07:52:21.322Z" }, + { url = "https://files.pythonhosted.org/packages/29/4b/1c19c509d56e01f4fbb3d0a2e597450f6cc04d1d56cf52defb0a62dfd715/safetensors-0.8.0-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae091f16662658bdc019a4ff6cb4c085bb7d725eb5978b183ffd265863b6d2d", size = 765683, upload-time = "2026-06-09T07:52:22.594Z" }, + { url = "https://files.pythonhosted.org/packages/27/43/41c1621732edd934d868a00d1b891584c892a7b62a9aab82ea5a0a5623ee/safetensors-0.8.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8e080062fcde23be189565e1c3305d16751a218ecf9412c8601e64204eb6f846", size = 722361, upload-time = "2026-06-09T07:52:23.924Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3f/73ccf82579412b4a71c4ca673f10b5f1f888d7cf5af7fe24f27d30307be4/safetensors-0.8.0-cp310-abi3-win32.whl", hash = "sha256:2ddf52eac562eda224f99acfa7889d02968c1fd59a5b011ae7d8137c37e9c02d", size = 342401, upload-time = "2026-06-09T07:52:28.895Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6d/3fba214c1e5e0f69991677ec3bc17023f0421776975e1de0c682dca475e2/safetensors-0.8.0-cp310-abi3-win_amd64.whl", hash = "sha256:096ec1a98435df7beb08853bb5aa9081a84f23d0adc67ed1a0a10550f608373f", size = 355540, upload-time = "2026-06-09T07:52:27.832Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fc/7eedc3510d97878876e32774eebbeb61c43f148a96e915c84229a3e967aa/safetensors-0.8.0-cp310-abi3-win_arm64.whl", hash = "sha256:f7838e5135a406ad3e02efdcb8cf2e5397d368b0154537c4fec682dbc544d452", size = 340500, upload-time = "2026-06-09T07:52:26.745Z" }, +] + [[package]] name = "sail" version = "0.12.8" @@ -4984,28 +5010,36 @@ wheels = [ [[package]] name = "tokenizers" -version = "0.22.2" +version = "0.23.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "huggingface-hub" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, - { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, - { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, - { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, - { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, - { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, - { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, - { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, - { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, - { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, - { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, - { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, - { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, - { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e0/7c/2cabb2174e772636683008f2c5621949b645da7d303c596589e84516a184/tokenizers-0.23.3.tar.gz", hash = "sha256:cded33237c77caeef62944d32aa9a7ef42bdce2b3497e18d137e072a8c4be438", size = 385286, upload-time = "2026-10-09T10:16:55.759Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/2e/4ce5b9716f26e526eff6b0502ebed4ea8d7161f03b3c77617c9f25528e97/tokenizers-0.23.3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:9d2b5c97daf61688c2ad1803ca851800feaba50fb68d5821779e9ea5880d968c", size = 3148800, upload-time = "2026-10-09T10:00:51.457Z" }, + { url = "https://files.pythonhosted.org/packages/b2/72/01e49f032bb346e5aaf06c10c74fe8aeec847173adbadd66eb7c53054bf2/tokenizers-0.23.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:68649e97d5b43c44c031d8d848874a6eecae8f8fe40ea989aa777a5a83aca716", size = 3101381, upload-time = "2026-10-09T10:00:54.063Z" }, + { url = "https://files.pythonhosted.org/packages/15/fc/ae987741829b1cd547668c4c94be732ae3eefd1d74344e64c3d2ca714acd/tokenizers-0.23.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec82e80e65a862275b97c3d90b7a523df8d9519ee48aeb4e9625b2cc909274e0", size = 3519944, upload-time = "2026-10-09T10:00:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/1c/da/cc8f6c030afaf05fbddc608158fbb761dca46913cbeba6b112e59fc82e2a/tokenizers-0.23.3-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c64a0713180ff16829d4e7f39a658b77ea11443af4e1aa46523692943c9b1414", size = 3397695, upload-time = "2026-10-09T10:00:57.444Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f1/256f78d1365fa2cd3ea6db716883d74667c8cbb6a21f15fa5b89a773cdc2/tokenizers-0.23.3-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddedfd4b3b4be6be24ff6ca645c4a37fddfd305f6f3e354c54cf10b715c48215", size = 3753125, upload-time = "2026-10-09T10:01:00.165Z" }, + { url = "https://files.pythonhosted.org/packages/60/93/eee007ac2fcbf4ecfce7fbc354826cf3611f56bdb886f3e91b1f7dd06b8f/tokenizers-0.23.3-cp310-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2a89614730d7b80940a5d2ed9320e1ec8add5a745c6151d8d05071b7215505b6", size = 4018598, upload-time = "2026-10-09T10:01:02.05Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f9/0c96c4739461fce9d8d865b416728081bf6230022d7163bd6244f35f4b31/tokenizers-0.23.3-cp310-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e88646b8580c5ad7f4361477f1298e9cc01771a1ee9aecfe32c47b8ff614cc38", size = 3602442, upload-time = "2026-10-09T10:01:03.77Z" }, + { url = "https://files.pythonhosted.org/packages/3a/40/6706b82693715581457c6d5423eaa7faae576bb0526c5738a57085eb4449/tokenizers-0.23.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:376851d22bcf9d650a5c3090bb83e6cf9e895fbf0595369fa4cd43c1f69b5f87", size = 3396193, upload-time = "2026-10-09T10:01:05.48Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0c/85946de40e25b7364b8f1bcf56def129069acd5bb364b7c86a32919e1a23/tokenizers-0.23.3-cp310-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:bf501c40b72d2d5c8623620210430e9cac1ce47a46e45b34107b70a1557d46b0", size = 3553483, upload-time = "2026-10-09T10:01:07.387Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6b/8d615d92cad1d511ca5ab188d1c7c167f0b3d295cc0d96207f9f82d486d8/tokenizers-0.23.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:114e2b55ed177179d59f4ab98200a4471e11e78f9e4b5a922d146740f96fcf52", size = 9972248, upload-time = "2026-10-09T10:01:09.437Z" }, + { url = "https://files.pythonhosted.org/packages/c9/7d/a922e37ddd58d1b463bbc2ad08120c8f59c60b814cd353519a116b24f8ba/tokenizers-0.23.3-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:d3407fb7b9c4d75dd68850ffd7180bc0a5d2dbaf0762d888e612f31fec3f9c6b", size = 9802957, upload-time = "2026-10-09T10:01:11.869Z" }, + { url = "https://files.pythonhosted.org/packages/4b/06/5d3f506a86ae0699a0e4ea05c05978f9aee169ef2c1d844e68c971cf8194/tokenizers-0.23.3-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:84513ef0aeb8bf8f4ea11a2e8a7ac163ec5288aa115e649a59b470ac5c3107df", size = 10145487, upload-time = "2026-10-09T10:01:14.268Z" }, + { url = "https://files.pythonhosted.org/packages/26/e5/065625317690ea3548d834dad81f48ea1fd32e4964610e658e195d7fe28e/tokenizers-0.23.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e05ab7baf7f47b406a95fea6f3b0a484b2ddcd9e1d14b68844c457eb755085a3", size = 10266026, upload-time = "2026-10-09T10:16:33.054Z" }, + { url = "https://files.pythonhosted.org/packages/77/4e/babede85d0d19f5e3deeef0063e01848141329934d3d77c31b5cab5ac2b4/tokenizers-0.23.3-cp310-abi3-win32.whl", hash = "sha256:1ebf28794e7e4954e20a7f70fbea410b2d1f0418f7dbbca97ca384fcfef38c25", size = 2588086, upload-time = "2026-10-09T10:16:35.686Z" }, + { url = "https://files.pythonhosted.org/packages/d1/6c/24f074c9a0efb98e61b20aafe6b2641922d5db24e447d5d6daffd9e17555/tokenizers-0.23.3-cp310-abi3-win_amd64.whl", hash = "sha256:1f0823bb00c5fdc98e487354d54dd55a03848d61a1a0bf29a68c77f24f3b26c3", size = 2872101, upload-time = "2026-10-09T10:16:37.533Z" }, + { url = "https://files.pythonhosted.org/packages/53/77/a476b6f73a661c11d113a342d2326b91506cf2285f0995d1212a6bb2022d/tokenizers-0.23.3-cp310-abi3-win_arm64.whl", hash = "sha256:7e48734d2de9260d86f03ab056d2cfeeff3869f61dbd49aaa15a2793b5f3458b", size = 2742580, upload-time = "2026-10-09T10:16:39.244Z" }, + { url = "https://files.pythonhosted.org/packages/65/46/f66baaedd42414a3f583c47379dc350e3e1f858a690d2574fd85ae70681b/tokenizers-0.23.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:efa3d7318406b4d115dce61ad5061953f1f44b128e79c020ce4615d763e23b6e", size = 3154274, upload-time = "2026-10-09T10:16:40.876Z" }, + { url = "https://files.pythonhosted.org/packages/c6/41/8de8c63b2d935eee5a0f42011fb7b786ffafeab0b8eb6d17acb8af2293b7/tokenizers-0.23.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a4fbb3662f9f59d199d61338e54b4bcc11d07ebbb1aeb3540dacb2be9c521cb7", size = 3077805, upload-time = "2026-10-09T10:16:42.856Z" }, + { url = "https://files.pythonhosted.org/packages/e3/08/b1cbae8dc8fc7c91f992ac2d87a086e9b3f25a28814047ca16a82fe8c87b/tokenizers-0.23.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de536665495cb4b409d25bade41963f801aff4225c19a6b804b048f7d14e34c7", size = 3491678, upload-time = "2026-10-09T10:16:45.093Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0d/aac0cb2f3a1fdbef514145b4c5f2df4d05deeb1ee8f73ae641a1b4a62a85/tokenizers-0.23.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cc24bb457dd4a8af89c8fcb40074d570129ec473df2a866c276ee55db4749d7", size = 3367420, upload-time = "2026-10-09T10:16:47.112Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1d/41a697d0c193a320b243fbd68b2057b6eb2f01ecf80899e1a16e646ff699/tokenizers-0.23.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:acd5c57b4bd3e56e246e2731a3a3a6825a7a7d89b7e3b761ba80bc521710f04b", size = 9945973, upload-time = "2026-10-09T10:16:49.326Z" }, + { url = "https://files.pythonhosted.org/packages/37/e9/b56e619fcd583000a2b1254bb46af8dc6a174d3ba3329f454ad5a95a2be2/tokenizers-0.23.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82eb480f6f1c21cea3349dec32cf1a6384c6c1e775f00f83b0d51197bc013687", size = 10237491, upload-time = "2026-10-09T10:16:51.943Z" }, + { url = "https://files.pythonhosted.org/packages/6f/68/f58b3beb95f3b62816e91e5e768e684cd63e58f9cbece22036dae3b1c971/tokenizers-0.23.3-cp314-cp314t-win_amd64.whl", hash = "sha256:1554a6eed34d9d6a78d23360f4e06df8dffab1ae08c7e8488e0b3e3b36cc266f", size = 2847654, upload-time = "2026-10-09T10:16:54.166Z" }, ] [[package]] @@ -5119,6 +5153,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d8/8e/bb97bb0c71802080bfc8952937d174e49cfc50de5c951dd47b2496f0dcdb/tqdm-4.68.3-py3-none-any.whl", hash = "sha256:39832cc2def2789a6f29df83f172db7416cea70052c0907a57801c5f2fdccb03", size = 78337, upload-time = "2026-06-17T07:36:50.132Z" }, ] +[[package]] +name = "transformers" +version = "5.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/9d/59e6232df8761e2c82d52e0bef5382e830d1cd65f02f648712ccd7675e7b/transformers-5.21.0.tar.gz", hash = "sha256:43c971a3eb9ab68dc9dfff430f8ab8a0fba09af2f68b608fd70c6245d2ddb8e7", size = 10321727, upload-time = "2026-10-26T18:17:26.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/fa/fb638d54bffff44700c8d487608c1f2872823e606fbd043b3c29c93236cf/transformers-5.21.0-py3-none-any.whl", hash = "sha256:8803a8712932aa3441c1e8bb340e66d5e3eca079fe65d9b717ede2f3ead01c79", size = 12978463, upload-time = "2026-10-26T18:17:23.647Z" }, +] + [[package]] name = "truststore" version = "0.10.5" From 379edd87527c6598c19b79108a12db4af3a55982 Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Wed, 28 Oct 2026 09:24:15 -0700 Subject: [PATCH 6/9] docs(agents): name training/_tools.py Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index b8d90963b..15c227732 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -113,7 +113,7 @@ and deserialized through a registry. | `providers/` | `providers/sandbox_providers/` holds the sandbox providers `local`, `modal`, `modal_vm`, `e2b`, `sail_vm` (the `sail` extra), `smol_vm` (the `smol` extra), `tensorlake` (the `tensorlake` extra), `tenki` (the `tenki` extra), `vercel` (the `vercel` extra), `boxd` (the `boxd` extra), `daytona` (the `daytona` extra); `[sandbox] default` and `agent_default` accept a comma-separated fallback chain, whose deploys skip a provider that can't run them. A sandbox provider declares what it runs (`ON_THIS_MACHINE`, and for VMs on this machine the address their requests to it arrive at, `REACHES_HOST_AT`; `CONTAINER_ACCEPTS`, `PRIVATE_NETWORK`; `SUPPORTED_SANDBOX_MODES`, derived from whether it implements `create_sandbox` and `create_vm`; and `SANDBOX_ACCEPTS`, the images an agent's sandbox runs, which defaults to those a VM loads when it implements `create_vm` and to `CONTAINER_ACCEPTS` otherwise, so only a provider that implements `create_vm` yet gives an agent a container, like the local one, declares it), each sandbox carries the `platform` of the images it runs (linux/amd64 unless its provider sets another when it creates it; None, any, unchecked, for a local sandbox), and code outside `providers/sandbox_providers/` reads those, never which provider it has. `providers/env_providers/` holds the environment providers: `EnvironmentProvider` (an env's containers and state store), `EnvironmentGatewayProvider` (`gateway`, an env's default), which renders a docker-compose for the gateway and its MCP servers inside the sandbox, and `EnvironmentServerProvider` (`server`), one MCP server in its own container with no gateway in front and no state store; `providers/env_state/` holds env-state providers (`local_postgres` built in). | | `a2a_agent/` | The `A2AAgent` entity (`a2a_agent`), its stores and the validator steps. The protocol package provides the agent-side framework. | | `openenv/` | `build_app(bundle)`: the [OpenEnv](https://github.com/openenv/OpenEnv) server for a bundle's tasks, each session playing one episode through the Episode API, with the envs' MCP tools, an added `end_turn` tool and the task's verifier score as the reward. It prepares bundles as `agent-env run` does (`prepare_bundle(played=True)`, without the agent the client plays) and keeps namespace routing on for the app's life. `openenv` comes with the `openenv` extra, a floor with no cap; this package and `training/trl.py` are the only ones that import it. Each task's description in the Task API names its `toolset` (the played agent's envs, role, and whether it gets `bash`). | -| `training/` | One module per outside trainer, each behind its own extra. `trl.py` (`trl` extra): `AgentEnv(url)` over a server `openenv/` makes, with `dataset` (a row per task) and `factory` (TRL's `environment_factory`: a generated class per toolset whose methods are the tools, a background `reset` and an owned `get_reward`); `AgentEnv.local(bundle)` serves the bundle in process. | +| `training/` | One module per outside trainer, each behind its own extra, and `_tools.py`, which turns an env's tools into typed, documented Python methods from their JSON Schema, as transformers' `get_json_schema` reads them. `trl.py` (`trl` extra): `AgentEnv(url)` over a server `openenv/` makes, with `dataset` (a row per task) and `factory` (TRL's `environment_factory`: a generated class per toolset whose methods are the tools, a background `reset` and an owned `get_reward`); `AgentEnv.local(bundle)` serves the bundle in process. | | `runner/` | The `[runner]` seam: `Runner.submit()` returns a `RunHandle` (`run_id`, `instance_id`) before the run starts; `LocalRunner` is built in. `runner/store.py` is the run index every runner writes and the explorer reads. | | `bundle/` | A bundle is a folder of entities (`envs/`, `agents/`, `artifacts/`, `skills/`, `tasks/.json`, `evals/.toml`) that runs without a registry: `agent-env run` plans it, writes it into the `@local` namespace (reusing each version whose inputs haven't changed) and runs its evals or tasks; `agent-env bundle init` writes a stored task or eval as one, with every env, agent and artifact it uses in its own folder (each image as its build context, pruned to what its Dockerfile reads and pinned to the Python and npm versions of the stored image) so it runs with no store. | | `eval/` | `Eval` (`eval`), a versioned list of task references, and its store. | From 6b6cb5b30725a9568967964df8fe4961396b380d Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Wed, 28 Oct 2026 09:51:06 -0700 Subject: [PATCH 7/9] fix(training): a review's findings: tool deadlines held by the server, rows checked by task id, and more From a review on the pull request: 1. A tool's deadline travels in the call's action metadata, and the server ends the call at it. The model is told the tool didn't answer, the episode goes on, and the session is kept. Before, the client dropped the session while the server still held its slot, so the next reset could hit the server's limit and stop training. Only an environment's first session now raises when refused. A later one, opened in the background after a drop, is retried while the server is full, and failing that costs one rollout. 2. Rows carry a task_id, and a rollout is checked against the task it plays, not its prompt, which the user may change. 3. Toolsets include the env versions the task deploys, and a rollout whose tools aren't its class's makes get_reward raise instead of leaving tasks unscored without a sound. 4. The Task API gives each task's turn count, so building the factory lists every turn's tools and ends the probe unscored, without running the verifiers. 5. A test imports agent_env.openenv and agent_env.training.trl with nothing beyond their own extra installed. 6. The TRL integration tests are in the slow tier. 7. AgentEnv's docstring says that the model's reply reaches the task only through end_turn. 8. The loop helpers are private, the exit hook waits at most 30 s per stage, and the README section moves to the pull request as notes for the docs site. Co-Authored-By: Claude Opus 5.5 --- README.md | 12 -- src/agent_env/openenv/environment.py | 30 ++- src/agent_env/training/trl.py | 191 +++++++++++------- tst/integration/training/trl_test.py | 2 +- tst/unit/openenv/environment_test.py | 5 +- .../test_optional_extras_stay_optional.py | 19 +- tst/unit/training/trl_test.py | 58 ++++-- 7 files changed, 206 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index 7deab3b39..93e469d7b 100644 --- a/README.md +++ b/README.md @@ -52,18 +52,6 @@ A package of your own can add env types, providers, task steps and stores to age Every task in a bundle is an [OpenEnv](https://github.com/openenv/OpenEnv) env, with the `openenv` extra installed (`pip install 'agentenv-framework[openenv]'`). `agent_env.openenv.build_app(bundle)` returns the OpenEnv server for a bundle, a folder or an installed bundle's name, or for a mapping of split names to bundles. Any OpenEnv client resets it to a task, which deploys the task's envs and opens its agent's turn, acts through the envs' tools and an added `end_turn` tool, and gets the task's verifier score as its reward. -TRL's trainers train on such a server with the `trl` extra (`pip install 'agentenv-framework[trl]'`): `GRPOTrainer`, verified with TRL 1.16, and AsyncGRPO, which takes the same `environment_factory`. Either needs TRL 1.15 or later, for an environment that owns its reward. `AgentEnv(url)` from `agent_env.training.trl` gives `dataset`, one row per task, and `factory`, the `environment_factory` whose environments play the tasks' episodes and own their reward, so the trainer needs no `reward_funcs`. `AgentEnv.local(bundle, max_concurrent_envs=...)` serves the bundle in the training process instead. The server must allow a session for every rollout the trainer generates at once (`MAX_CONCURRENT_ENVS`). OpenEnv 0.10 holds the trainer to mcp 1, and so to vLLM 0.29 or older. - -```python -from trl import GRPOConfig, GRPOTrainer -from agent_env.training.trl import AgentEnv - -env = AgentEnv("http://localhost:8000") -trainer = GRPOTrainer(model="Qwen/Qwen3-4B", args=GRPOConfig(max_tool_calling_iterations=12), - train_dataset=env.dataset, environment_factory=env.factory) -trainer.train() -``` - ## Time zones Servers and the env clock work in UTC. To run an agent in a user's time zone, the task sets it on the agent's `deploy_agent` step as `env_vars: {"TZ": "America/New_York"}`, which reaches the agent's sandbox, and names the zone in the agent's prompt. While the env clock is armed, the gateway's `get_time` tool takes that IANA name as `time_zone` and adds the `local_time` in that zone, and a trigger executor's working `hours` can take a `time_zone` instead of a fixed `utc_offset`, so they keep their local hours across daylight-saving changes. diff --git a/src/agent_env/openenv/environment.py b/src/agent_env/openenv/environment.py index eef3788c7..3e65ef590 100644 --- a/src/agent_env/openenv/environment.py +++ b/src/agent_env/openenv/environment.py @@ -36,6 +36,9 @@ input_schema={"type": "object", "properties": {"response": {"type": "string", "description": "Your reply."}}}, ) ENDED = "the episode has ended; call reset" +#: The key of a tool call's deadline in seconds in its action's metadata, which a client sets to have the server end a +#: call that runs past it. +TIMEOUT_METADATA = "timeout_s" NOT_RESET = "no episode has started; call reset before stepping" @@ -43,6 +46,7 @@ class Toolset(TypedDict): """Where a task's played agent gets its tools, as the Task API names it in each task's ``toolset``.""" envs: list[str] + versions: dict[str, int | None] role: str | None access: list[dict[str, Any]] bash: bool @@ -59,13 +63,15 @@ class ServedTask: @property def toolset(self) -> Toolset: - """Where the played agent's tools come from: the envs it works in, its role in them, the changes the task makes - to that role's tool access, and whether its turns offer ``bash`` in its sandbox. Tasks that agree on it offer - the same tools, so a trainer can group them.""" + """Where the played agent's tools come from: the envs it works in and the versions the task deploys, its role + in them, the changes the task makes to that role's tool access, and whether its turns offer ``bash`` in its + sandbox. Tasks that agree on it offer the same tools, so a trainer can group them.""" agent = self.spec.trainer_agents[0] [deploy] = [step for step in self.task.steps if step.type in DEPLOYS and step.agent_name == agent] - role = getattr(deploy, "role", None) - return Toolset(envs=list(getattr(deploy, "env_ids", None) or []), role=role, + role, envs = getattr(deploy, "role", None), list(getattr(deploy, "env_ids", None) or []) + return Toolset(envs=envs, role=role, + versions={step.env_id: step.env_version for step in self.task.steps + if hasattr(step, "env_version") and step.env_id in envs}, access=[{"env": step.env_id, "action": step.action, "tools": step.tools} for step in self.task.steps if step.type == "modify_env_tool_access" and step.role == role], @@ -185,7 +191,7 @@ async def _step(self, action: Any, timeout_s: float | None) -> Observation: self._state.step_count += 1 if action.tool_name == END_TURN.name: return await self._end_turn(action.tool_name, action.arguments.get("response") or "") - return await self._call(action, timeout_s) + return await self._call(action, timeout_s if timeout_s is not None else _deadline_of(action)) def step(self, action: Any, timeout_s: float | None = None, **kwargs: Any) -> Observation: """OpenEnv's web playground steps from a worker thread; the episode lives on the loop its reset ran on.""" @@ -217,8 +223,8 @@ async def _call(self, action: CallToolAction, timeout_s: float | None) -> CallTo result = await asyncio.wait_for(self._tools.call(name, action.arguments), timeout_s) except EpisodeError as e: return _tool_error(name, ToolErrorType.TOOL_NOT_FOUND, e) - except TimeoutError as e: - return _tool_error(name, ToolErrorType.TIMEOUT, e) + except TimeoutError: + return _tool_error(name, ToolErrorType.TIMEOUT, f"{name} didn't answer within {timeout_s:g}s") except httpx.HTTPError as e: return _tool_error(name, ToolErrorType.TRANSPORT_ERROR, e) if result.isError: @@ -325,7 +331,8 @@ def _describe(self, split: str, index: int) -> dict[str, Any]: served = self._task_at(split, index) prompt, system_prompt = served.spec.prompts[0] return {"id": served.task.id, "name": served.name, "index": index, "prompt": prompt, - "system_prompt": system_prompt, "agent": served.spec.trainer_agents[0], "toolset": served.toolset} + "system_prompt": system_prompt, "agent": served.spec.trainer_agents[0], "toolset": served.toolset, + "turns": len(served.spec.prompts)} def _task_metadata(self) -> dict[str, Any] | None: if self._served_task is None: @@ -334,6 +341,11 @@ def _task_metadata(self) -> dict[str, Any] | None: "index": self._state.index} +def _deadline_of(action: CallToolAction) -> float | None: + deadline = action.metadata.get(TIMEOUT_METADATA) + return float(deadline) if isinstance(deadline, (int, float)) and deadline > 0 else None + + def _tool(tool: Any) -> Tool: return Tool(name=tool.name, description=tool.description or "", input_schema=tool.inputSchema or {"type": "object"}) diff --git a/src/agent_env/training/trl.py b/src/agent_env/training/trl.py index cd888ab80..bf386b124 100644 --- a/src/agent_env/training/trl.py +++ b/src/agent_env/training/trl.py @@ -34,7 +34,7 @@ from agent_env.openenv import build_app from agent_env.openenv.environment import END_TURN as END_TURN_TOOL -from agent_env.openenv.environment import Toolset +from agent_env.openenv.environment import TIMEOUT_METADATA, Toolset from agent_env.training._tools import tool_method logger = logging.getLogger(__name__) @@ -44,6 +44,13 @@ END_TURN = END_TURN_TOOL.name CONNECT_SECONDS = 60.0 CLOSE_SECONDS = 300.0 +#: How much longer than a tool's deadline the client waits for the server to end the call, before dropping the session. +GRACE_SECONDS = 30.0 +#: How long the exit hook waits for the sessions to close, and then for each server to stop. +EXIT_SECONDS = 30.0 +#: How often, and how far apart in seconds, an environment tries to open a session in place of a dropped one while the +#: server is at its limit, which it stays at until it has seen the dropped one close. +REOPEN_ATTEMPTS, REOPEN_SECONDS = 5, 1.0 _CAPACITY = re.compile(r"Server at capacity: \d+/(\d+)") @@ -57,14 +64,17 @@ class AgentEnv: """A server's tasks for TRL. ``dataset`` holds one row per task of ``split``, and ``factory`` is the ``environment_factory`` whose environments play them: each holds one session on the server, ``reset`` starts the row's task, its methods call the turn's tools, ``end_turn`` hands over the reply and answers with the next turn's - prompt, and ``get_reward`` is the episode's reward, ``None`` when it ended in an error. A task's turn not ended by - the model ends with an empty reply. Tasks whose agents get different tools take TRL's multi-environment mode: - ``factory`` is then a dict, and each row names its ``environment``. + prompt, and ``get_reward`` is the episode's reward, ``None`` when it ended in an error. TRL hands an environment + no completion, so the model's reply reaches the task only through ``end_turn(response=...)``: a turn the model + leaves open ends with an empty reply, which a verifier judging the reply scores as such. Tasks whose agents get + different tools take TRL's multi-environment mode: ``factory`` is then a dict, and each row names its + ``environment``. Args: url: The server's base URL. split: The split ``dataset`` holds. - tool_timeout: One tool call's deadline in seconds. A call that misses it ends its episode unscored. + tool_timeout: One tool call's deadline in seconds, which the server ends the call at: the model is told the + tool didn't answer, and the episode goes on. reset_timeout: How long an episode may take to deploy and open its first turn, and each turn to be ended, in seconds. @@ -112,21 +122,24 @@ def dataset(self) -> Dataset: return self.dataset_for(self.split) def dataset_for(self, split: str) -> Dataset: - """``split``'s tasks as TRL's rows: the ``prompt`` as chat messages, and the ``split`` and ``index`` that - ``reset`` plays, with the ``environment`` in multi-environment mode.""" + """``split``'s tasks as TRL's rows: the ``prompt`` as chat messages, the ``split`` and ``index`` that + ``reset`` plays, the ``task_id`` it checks the episode against, and the ``environment`` in multi-environment + mode. The prompt is the model's to change.""" several = len(self._toolsets) > 1 return Dataset.from_list([{ "prompt": _messages(task["system_prompt"], task["prompt"]), "split": split, "index": task["index"], + "task_id": task["id"], **({"environment": self._toolsets[_key(task)].name} if several else {}), } for task in self._split(split)]) @cached_property def factory(self) -> type[_Environment] | dict[str, type[_Environment]]: """TRL's ``environment_factory``: an environment class per toolset of the server's tasks, or the one class when - every task has the same. Building it plays one task of each toolset through, in turn, ending each of its turns - empty, since TRL reads the tools from the class: its methods are every tool a turn offers, and one a turn - doesn't offer answers so. Each call on a probe's session has the reset deadline.""" - classes = on_loop(self._environments(), None) + every task has the same. Building it walks one task of each toolset through its turns, in turn, ending each but + the last empty and listing its tools, then ends the episode unscored, since TRL reads the tools from the + class: its methods are every tool a turn offers, and one a turn doesn't offer answers so. Each call on a + probe's session has the reset deadline.""" + classes = _on_loop(self._environments(), None) return classes[0] if len(classes) == 1 else {each.__name__: each for each in classes} def close(self) -> None: @@ -134,7 +147,7 @@ def close(self) -> None: started.""" mine = [environment for environment in list(_environments) if environment._shared in self._shared] try: - on_loop(_gathered(environment._close() for environment in mine), CLOSE_SECONDS) + _on_loop(_gathered(environment._close() for environment in mine), CLOSE_SECONDS) finally: if self._server is not None: self._server.stop() @@ -143,22 +156,25 @@ async def _environments(self) -> list[type[_Environment]]: return [await self._environment(toolset) for toolset in self._toolsets.values()] async def _environment(self, toolset: _Toolset) -> type[_Environment]: - split, index = toolset.first + split, index, turns = toolset.first client = await _opened(self.url, self._reset_timeout) first, tools = None, {} try: turn = await client.reset(split=split, index=index) - while not turn.done: + for number in range(turns): + if turn.done: + break listed = await _list_tools(client) if listed.done: turn = listed break first = first or frozenset(tool["name"] for tool in listed.observation["tools"]) tools |= {tool["name"]: tool for tool in listed.observation["tools"] if tool["name"] not in tools} - turn = await _end_turn(client, "") - if error := turn.observation.get("error"): - raise RuntimeError(f"task {index} of split {split!r} refused an empty {END_TURN}: " - f"{error.get('message')}") + if number + 1 < turns: + turn = await _end_turn(client, "") + if error := turn.observation.get("error"): + raise RuntimeError(f"task {index} of split {split!r} refused an empty {END_TURN}: " + f"{error.get('message')}") finally: with contextlib.suppress(Exception): await client.close() @@ -183,7 +199,7 @@ def _split(self, split: str) -> list[dict[str, Any]]: @dataclass(frozen=True) class _Toolset: name: str - first: tuple[str, int] + first: tuple[str, int, int] # the split, index and turn count of its first task @dataclass(frozen=True, eq=False) @@ -201,6 +217,19 @@ class _Failed(Exception): pass +class _AtCapacity(RuntimeError): + def __init__(self, url: str, limit: str): + super().__init__(f"the server at {url} is at its limit of {limit} sessions at once, and the trainer holds one " + "per rollout it generates at once: serve the bundle with MAX_CONCURRENT_ENVS (or " + "max_concurrent_envs=) at least that many, or generate fewer rollouts at once") + self.limit = limit + + +class _Misgrouped(_Failed): + """An episode offering other tools than its class has: a toolset that misses what tells tasks apart, which no + rollout fixes.""" + + @dataclass class _Rollout: started: asyncio.Task[None] = field(init=False) @@ -208,6 +237,7 @@ class _Rollout: done: bool = False reward: float | None = None failure: str | None = None + misgrouped: bool = False class _Environment: @@ -219,59 +249,68 @@ class _Environment: def __init__(self) -> None: self._client: GenericEnvClient | None = None + self._opened = False self._rollout: _Rollout | None = None self._lock = asyncio.Lock() _environments.add(self) - def reset(self, *, split: str, index: int, prompt: list[dict[str, Any]] | None = None, **_: Any) -> None: + def reset(self, *, split: str, index: int, task_id: str | None = None, **_: Any) -> None: """Starts the row's episode and returns before it is deployed, since ``GRPOTrainer`` resets its environments - one at a time: the first tool call or ``get_reward`` waits for it. Raises when the server refuses the session, - as one at capacity does.""" - on_loop(self._reset(split, index, _prompt_of(prompt)), CONNECT_SECONDS + CLOSE_SECONDS) + one at a time: the first tool call or ``get_reward`` waits for it. Raises when the server refuses this + environment's first session, as one at capacity does; a later session it refuses, after one was dropped, + leaves only that rollout unscored.""" + _on_loop(self._reset(split, index, task_id), CONNECT_SECONDS + CLOSE_SECONDS) def get_reward(self) -> float | None: + """The episode's reward, or ``None`` when it ended in an error. Raises when the episode offered other tools + than this class has, which no rollout fixes.""" rollout, self._rollout = self._rollout, None if rollout is None: return None try: - on_loop(self._finish(rollout), None) + _on_loop(self._finish(rollout), None) except Exception as e: rollout.failure = rollout.failure or _why(e) + if rollout.misgrouped: + raise RuntimeError(f"{rollout.failure}. The tasks of one toolset must offer the same tools, since TRL shows " + "the model a class's: their toolset misses what tells them apart") if rollout.failure is not None: logger.warning("rollout left unscored: %s", rollout.failure) return None return rollout.reward def _call(self, name: str, arguments: dict[str, Any]) -> str: - return on_loop(self._step(name, arguments), - self._shared.reset_timeout + self._shared.tool_timeout + CLOSE_SECONDS) + return _on_loop(self._step(name, arguments), + self._shared.reset_timeout + self._shared.tool_timeout + GRACE_SECONDS + CLOSE_SECONDS) - async def _reset(self, split: str, index: int, expected: tuple[str | None, str] | None) -> None: + async def _reset(self, split: str, index: int, expected: str | None) -> None: async with self._lock: if (rollout := self._rollout) is not None: rollout.started.cancel() await asyncio.wait([rollout.started]) self._rollout = None - if self._client is None: - self._client = await _opened(self._shared.url, self._shared.reset_timeout) + if not self._opened: + self._client, self._opened = await _opened(self._shared.url, self._shared.reset_timeout), True rollout = _Rollout() rollout.started = asyncio.ensure_future(self._begin(rollout, split, index, expected)) self._rollout = rollout - async def _begin(self, rollout: _Rollout, split: str, index: int, - expected: tuple[str | None, str] | None) -> None: + async def _begin(self, rollout: _Rollout, split: str, index: int, expected: str | None) -> None: + if self._client is None: + self._client = await _reopened(self._shared.url, self._shared.reset_timeout) reset = await self._client.reset(split=split, index=index) if reset.done: raise _Failed(_error_of(reset.observation)) metadata = reset.observation.get("metadata") or {} - rollout.system = metadata.get("system_prompt") or None - if expected is not None and (rollout.system, metadata.get("prompt")) != expected: - raise _Failed("the turn's prompt differs from the dataset row's") + rollout.system, played = metadata.get("system_prompt") or None, (metadata.get("task") or {}).get("id") + if expected is not None and played != expected: + raise _Failed(f"the episode plays task {played!r}, not the row's {expected!r}") listed = await _list_tools(self._client) if listed.done: raise _Failed(_error_of(listed.observation)) if (offered := {tool["name"] for tool in listed.observation["tools"]}) != self._shared.offered: - raise _Failed(f"the episode offers the tools {sorted(offered)}, not {sorted(self._shared.offered)}") + raise _Misgrouped(f"task {played!r} offers the tools {sorted(offered)}, not the " + f"{sorted(self._shared.offered)} its toolset's first task does") async def _step(self, name: str, arguments: dict[str, Any]) -> str: async with self._lock: @@ -282,26 +321,26 @@ async def _step(self, name: str, arguments: dict[str, Any]) -> str: return ENDED try: await rollout.started - result = await asyncio.wait_for(_call_tool(self._client, name, arguments), self._deadline(name)) + deadline = None if name == END_TURN else self._shared.tool_timeout + result = await asyncio.wait_for(_call_tool(self._client, name, arguments, deadline), + None if deadline is None else deadline + GRACE_SECONDS) except _Failed as e: + rollout.misgrouped = isinstance(e, _Misgrouped) await self._fail(rollout, str(e)) return FAILED except TimeoutError: - await self._fail(rollout, f"{name} didn't answer within {self._shared.tool_timeout:g}s") + await self._fail(rollout, f"{name} didn't answer within {self._shared.tool_timeout:g}s, and the " + f"server didn't end the call in the {GRACE_SECONDS:g}s after") return FAILED except Exception as e: await self._fail(rollout, f"{name}: {_why(e)}") return FAILED return self._answer(rollout, name, result) - def _deadline(self, name: str) -> float | None: - """A tool's deadline; ``end_turn`` has the session's, the reset deadline, since the server answers it once the - task's next turn opens or its verifiers have scored the episode.""" - return None if name == END_TURN else self._shared.tool_timeout - async def _finish(self, rollout: _Rollout) -> None: """Ends the turns the model left open with an empty reply, until the episode is scored; each call on the - session has the reset's deadline.""" + session has the reset's deadline, as ``end_turn`` does, since the server answers it once the next turn opens + or the verifiers have scored the episode.""" async with self._lock: try: await rollout.started @@ -311,6 +350,7 @@ async def _finish(self, rollout: _Rollout) -> None: rollout.failure = f"{END_TURN}: {error.get('message')}" self._answer(rollout, END_TURN, result) except Exception as e: + rollout.misgrouped = isinstance(e, _Misgrouped) await self._fail(rollout, str(e) if isinstance(e, _Failed) else _why(e)) def _answer(self, rollout: _Rollout, name: str, result: Any) -> str: @@ -338,7 +378,7 @@ def _answer(self, rollout: _Rollout, name: str, result: Any) -> str: async def _fail(self, rollout: _Rollout, why: str) -> None: """Records why the rollout won't be scored and drops the session, which may be out of step with the server; the - server ends a dropped session's episode, and the next reset opens another.""" + server ends a dropped session's episode, and the next rollout opens another.""" rollout.failure = rollout.failure or why client, self._client = self._client, None if client is not None: @@ -369,13 +409,13 @@ def __init__(self, app: Any): self.url = f"http://127.0.0.1:{self._uvicorn.servers[0].sockets[0].getsockname()[1]}" _servers.add(self) - def stop(self) -> None: + def stop(self, timeout: float = CLOSE_SECONDS) -> None: _servers.discard(self) self._uvicorn.should_exit = True - self._thread.join(CLOSE_SECONDS) + self._thread.join(timeout) -def episode_loop() -> asyncio.AbstractEventLoop: +def _episode_loop() -> asyncio.AbstractEventLoop: global _loop with _loop_lock: if _loop is None: @@ -384,8 +424,8 @@ def episode_loop() -> asyncio.AbstractEventLoop: return _loop -def on_loop(coroutine: Coroutine[Any, Any, Any], timeout: float | None) -> Any: - future = asyncio.run_coroutine_threadsafe(coroutine, episode_loop()) +def _on_loop(coroutine: Coroutine[Any, Any, Any], timeout: float | None) -> Any: + future = asyncio.run_coroutine_threadsafe(coroutine, _episode_loop()) try: return future.result(timeout) except TimeoutError: @@ -399,14 +439,14 @@ async def _gathered(coroutines: Any) -> list[Any]: def _close_all() -> None: """A trainer that stops or crashes ends the episodes it left running, since TRL never closes its environments, - then stops the servers ``AgentEnv.local`` started. It runs before threading's shutdown, while the thread pools an - episode's teardown uses still take work.""" + then stops the servers ``AgentEnv.local`` started, waiting at most ``EXIT_SECONDS`` for each. It runs before + threading's shutdown, while the thread pools an episode's teardown uses still take work.""" try: - on_loop(_gathered(environment._close() for environment in list(_environments)), CLOSE_SECONDS) + _on_loop(_gathered(environment._close() for environment in list(_environments)), EXIT_SECONDS) except Exception as e: logger.warning("couldn't end the episodes left running: %s", _why(e)) for server in list(_servers): - server.stop() + server.stop(EXIT_SECONDS) threading._register_atexit(_close_all) @@ -422,20 +462,35 @@ async def _opened(url: str, timeout: float) -> GenericEnvClient: with contextlib.suppress(Exception): await client.close() if match := _CAPACITY.search(str(e)): - raise RuntimeError( - f"the server at {url} is at its limit of {match[1]} sessions at once, and the trainer holds one per " - "rollout it generates at once: serve the bundle with MAX_CONCURRENT_ENVS (or max_concurrent_envs=) " - "at least that many, or generate fewer rollouts at once") from e + raise _AtCapacity(url, match[1]) from e raise ConnectionError(f"couldn't open a session on the server at {url}: {_why(e)}") from e return client +async def _reopened(url: str, timeout: float) -> GenericEnvClient: + """A session in place of a dropped one, tried again while the server is at its limit; raises _Failed, which leaves + the rollout unscored, when it can't open one.""" + for attempt in range(REOPEN_ATTEMPTS): + try: + return await _opened(url, timeout) + except _AtCapacity as e: + if attempt + 1 == REOPEN_ATTEMPTS: + raise _Failed(f"its session was dropped, and the server stayed at its limit of {e.limit} sessions for " + f"{(REOPEN_ATTEMPTS - 1) * REOPEN_SECONDS:g}s after") from e + await asyncio.sleep(REOPEN_SECONDS) + except ConnectionError as e: + raise _Failed(f"its session was dropped, and {e}") from e + + async def _list_tools(client: GenericEnvClient) -> Any: return await client.step({"type": "list_tools"}) -async def _call_tool(client: GenericEnvClient, name: str, arguments: dict[str, Any]) -> Any: - return await client.step({"type": "call_tool", "tool_name": name, "arguments": arguments}) +async def _call_tool(client: GenericEnvClient, name: str, arguments: dict[str, Any], + deadline: float | None = None) -> Any: + """The call, with its ``deadline`` in seconds for the server to end it at.""" + return await client.step({"type": "call_tool", "tool_name": name, "arguments": arguments, + **({"metadata": {TIMEOUT_METADATA: deadline}} if deadline is not None else {})}) async def _end_turn(client: GenericEnvClient, response: str) -> Any: @@ -453,19 +508,19 @@ def _read_tasks(url: str) -> tuple[str, dict[str, list[dict[str, Any]]]]: splits = [split["name"] for split in http.get(f"/{name}/splits").raise_for_status().json()] tasks = {split: http.post(f"/{name}/tasks", json={"split": split}).raise_for_status().json()["tasks"] for split in splits} - if not all("toolset" in task for described in tasks.values() for task in described): - raise ValueError(f"the server at {url} doesn't describe its tasks' toolsets, so it isn't a bundle served by " - "agent_env.openenv.build_app from this version of agentenv-framework or later") + if not all("toolset" in task and "turns" in task for described in tasks.values() for task in described): + raise ValueError(f"the server at {url} doesn't describe its tasks' toolsets and turns, so it isn't a bundle " + "served by agent_env.openenv.build_app from this version of agentenv-framework or later") return name, tasks def _toolsets(name: str, tasks: dict[str, list[dict[str, Any]]]) -> dict[str, _Toolset]: """Each toolset the tasks have, with the first task that has it, named after the server when it's the only one and otherwise after its envs, and ``bash`` when its turns offer it.""" - firsts: dict[str, tuple[str, int]] = {} + firsts: dict[str, tuple[str, int, int]] = {} for split, described in tasks.items(): for task in described: - firsts.setdefault(_key(task), (split, task["index"])) + firsts.setdefault(_key(task), (split, task["index"], task["turns"])) if len(firsts) == 1: return {key: _Toolset(name, first) for key, first in firsts.items()} named: dict[str, _Toolset] = {} @@ -490,14 +545,6 @@ def _messages(system: str | None, prompt: str) -> list[dict[str, str]]: return [*([{"role": "system", "content": system}] if system else []), {"role": "user", "content": prompt}] -def _prompt_of(messages: list[dict[str, Any]] | None) -> tuple[str | None, str] | None: - """The system prompt and prompt a row of ``dataset_for`` carries.""" - if not messages: - return None - system = next((message["content"] for message in messages if message["role"] == "system"), None) - return system or None, messages[-1]["content"] - - def _error_of(observation: Mapping[str, Any]) -> str: return (observation.get("metadata") or {}).get("error") or "the episode ended in an error" diff --git a/tst/integration/training/trl_test.py b/tst/integration/training/trl_test.py index 26f47bec1..ee42bdca8 100644 --- a/tst/integration/training/trl_test.py +++ b/tst/integration/training/trl_test.py @@ -14,7 +14,7 @@ from agent_env.training.trl import ENDED, AgentEnv from tst.integration.openenv.build_app_test import _left -pytestmark = pytest.mark.integration +pytestmark = [pytest.mark.integration, pytest.mark.int_test_slow] def _tools(environment): diff --git a/tst/unit/openenv/environment_test.py b/tst/unit/openenv/environment_test.py index d03a8d254..ffe1abb5f 100644 --- a/tst/unit/openenv/environment_test.py +++ b/tst/unit/openenv/environment_test.py @@ -89,8 +89,9 @@ async def test_the_task_api_lists_each_split_with_each_tasks_prompt_and_agent(te assert (env.num_tasks("smoke"), env.num_tasks("all")) == (1, 3) assert env.get_task("all", 2) == {"id": "dock-2", "name": "dock-2", "index": 2, "prompt": "Add crate 2.", "system_prompt": "You keep the items.", "agent": "alice", - "toolset": {"envs": ["items"], "role": "alice@example.com", "access": [], - "bash": False}} + "toolset": {"envs": ["items"], "versions": {}, "role": "alice@example.com", + "access": [], "bash": False}, + "turns": 1} hide = {"id": "hide", "type": "modify_env_tool_access", "env_id": "items", "action": "disable", "role": "alice@example.com", "tools": ["add_item"]} hidden = _task(ITEMS, ALICE, hide, {**hide, "id": "other", "role": "bob@example.com"}, TURN, REPLIES) diff --git a/tst/unit/test_optional_extras_stay_optional.py b/tst/unit/test_optional_extras_stay_optional.py index 6407b1f82..717fdc1db 100644 --- a/tst/unit/test_optional_extras_stay_optional.py +++ b/tst/unit/test_optional_extras_stay_optional.py @@ -143,6 +143,14 @@ def _only_in(extra: str) -> list[str]: return sorted(set().union(*(_module_paths(d) for d in extra_only)) - provided_by_core) +def _outside(extra: str) -> list[str]: + """Module paths some extra installs and neither a core install nor ``extra``, with the extras it includes, does.""" + installed = _closure(frozenset({extra})) + provided = set().union(*(_module_paths(d) for d in installed)) + others = set().union(*(_closure(frozenset({other})) for other in _optional_requirements())) - installed + return sorted(set().union(*(_module_paths(d) for d in others)) - provided) + + def _not_in_core() -> list[str]: """Module paths some extra installs and a core install does not, including those two extras share.""" core = _closure(frozenset()) @@ -258,12 +266,21 @@ def test_the_explorer_imports_without_any_extra(): assert _run(_IMPORT, _not_in_core(), "agent_env.explorer.app") == "" -@pytest.mark.parametrize(("module", "extra"), [("agent_env.openenv", "openenv"), ("agent_env.training.trl", "trl")]) +_EXTRA_MODULES = [("agent_env.openenv", "openenv"), ("agent_env.training.trl", "trl")] + + +@pytest.mark.parametrize(("module", "extra"), _EXTRA_MODULES) def test_the_openenv_server_and_the_trl_helper_without_their_extra_name_the_extra_to_install(module, extra): message = _run(_IMPORT, _only_in(extra), module) assert message.endswith(f"pip install 'agentenv-framework[{extra}]'"), message +@pytest.mark.parametrize(("module", "extra"), _EXTRA_MODULES) +def test_the_openenv_server_and_the_trl_helper_import_with_their_extra_alone(module, extra): + """What `pip install 'agentenv-framework[]'` gives them is enough: no module only another extra installs.""" + assert _run(_IMPORT, _outside(extra), module) == "" + + @pytest.mark.parametrize("package", _STORE_PACKAGES) def test_a_backend_imported_on_first_use_is_visible_to_type_checkers(package): """A name only __getattr__ serves is Any to a type checker, which a strict consumer cannot subclass, so each one diff --git a/tst/unit/training/trl_test.py b/tst/unit/training/trl_test.py index 6a195eb71..781f7692a 100644 --- a/tst/unit/training/trl_test.py +++ b/tst/unit/training/trl_test.py @@ -59,10 +59,10 @@ def test_the_dataset_holds_each_tasks_prompt_and_the_factory_offers_its_tools(se env = served({"a": PLAYED, "b": [ITEMS, ALICE, {**TURN, "prompt": "Add a barrel."}, REPLIES]}, {"smoke": ["b"]}) assert env.dataset.to_list() == [ - {"prompt": [SYSTEM, {"role": "user", "content": "Add a crate."}], "split": "all", "index": 0}, - {"prompt": [SYSTEM, {"role": "user", "content": "Add a barrel."}], "split": "all", "index": 1}] + {"prompt": [SYSTEM, {"role": "user", "content": "Add a crate."}], "split": "all", "index": 0, "task_id": "a"}, + {"prompt": [SYSTEM, {"role": "user", "content": "Add a barrel."}], "split": "all", "index": 1, "task_id": "b"}] assert env.dataset_for("smoke").to_list() == [ - {"prompt": [SYSTEM, {"role": "user", "content": "Add a barrel."}], "split": "smoke", "index": 0}] + {"prompt": [SYSTEM, {"role": "user", "content": "Add a barrel."}], "split": "smoke", "index": 0, "task_id": "b"}] assert (env.factory.__name__, _tools(env.factory)) == ("dock", ["add_item", "end_turn", "get_reward", "reset"]) assert inspect.signature(env.factory.end_turn).parameters["response"].annotation == str | None with pytest.raises(ValueError, match="has no split 'train'; it serves all, smoke"): @@ -98,7 +98,7 @@ def test_end_turn_answers_with_the_next_turns_prompt_and_a_turn_left_open_ends_e assert environment.get_reward() == 1.0 -def test_an_episode_that_fails_or_isnt_its_rows_is_left_unscored_and_the_environment_plays_on(served, caplog): +def test_an_episode_that_fails_or_isnt_its_rows_is_left_unscored_and_one_with_its_prompt_changed_plays(served, caplog): env = served({"a": PLAYED, "broken": [{**ITEMS, "fail": True}, ALICE, TURN, REPLIES]}) played, broken = env.dataset.to_list() environment = env.factory() @@ -107,15 +107,15 @@ def test_an_episode_that_fails_or_isnt_its_rows_is_left_unscored_and_the_environ environment.reset(**broken) assert environment.add_item(item="crate") == FAILED assert environment.get_reward() is None - environment.reset(**{**played, "prompt": [{"role": "user", "content": "Add a barrel."}]}) + environment.reset(**{**played, "task_id": "broken"}) assert environment.get_reward() is None - environment.reset(**played) + environment.reset(**{**played, "prompt": [{"role": "system", "content": "Think first."}, *played["prompt"]]}) environment.end_turn(response="Added a crate.") assert environment.get_reward() == 1.0 assert [record.getMessage() for record in caplog.records if record.name == trl.__name__] == [ "rollout left unscored: RuntimeError: the env didn't come up", - "rollout left unscored: the turn's prompt differs from the dataset row's"] + "rollout left unscored: the episode plays task 'a', not the row's 'broken'"] class _SecondTurnTools: @@ -180,21 +180,51 @@ async def close(self): return _Session() -def test_a_tool_that_misses_its_deadline_leaves_the_rollout_unscored_and_the_environment_plays_on( - served, monkeypatch, caplog): +def test_a_tool_that_misses_its_deadline_is_ended_by_the_server_and_the_episode_and_its_session_go_on( + served, monkeypatch): monkeypatch.setattr(proxy_module, "ToolSession", _Hanging()) - env = served({"a": PLAYED}, tool_timeout=0.5) + env = served({"a": PLAYED}, tool_timeout=0.5, max_concurrent_envs=1) environment = env.factory() environment.reset(**env.dataset[0]) - with caplog.at_level(logging.WARNING, logger=trl.__name__): - assert environment.wait() == FAILED - assert environment.get_reward() is None + assert environment.wait() == "wait didn't answer within 0.5s" + environment.end_turn(response="Added a crate.") + assert environment.get_reward() == 1.0 environment.reset(**env.dataset[0]) environment.end_turn(response="Added a crate.") assert environment.get_reward() == 1.0 - assert "rollout left unscored: wait didn't answer within 0.5s" in caplog.text + + +def test_a_session_refused_after_one_was_dropped_leaves_only_that_rollout_unscored(served, caplog): + env = served({"a": PLAYED}, max_concurrent_envs=1) + first, second = env.factory(), env.factory() + first.reset(**env.dataset[0]) + first.end_turn(response="Added a crate.") + assert first.get_reward() == 1.0 + trl._on_loop(first._close(), 30) + + second.reset(**env.dataset[0]) + with caplog.at_level(logging.WARNING, logger=trl.__name__): + first.reset(**env.dataset[0]) + assert first.get_reward() is None + trl._on_loop(second._close(), 30) + first.reset(**env.dataset[0]) + first.end_turn(response="Added a crate.") + + assert first.get_reward() == 1.0 + assert "its session was dropped, and the server stayed at its limit of 1 sessions for 4s after" in caplog.text + + +def test_a_rollout_whose_tools_arent_its_classs_stops_training(served, monkeypatch): + monkeypatch.setattr(proxy_module, "ToolSession", _SecondTurnTools()) + env = served({"a": PLAYED}) + environment = env.factory() + + environment.reset(**env.dataset[0]) + with pytest.raises(RuntimeError, match=r"task 'a' offers the tools \['add_item', 'end_turn', 'take_item'\], not " + r"the \['add_item', 'end_turn'\] its toolset's first task does"): + environment.get_reward() class _Slow(TaskStep): From 4170bf85f3b4b8c01dba522433c58168d0455033 Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Wed, 28 Oct 2026 10:25:27 -0700 Subject: [PATCH 8/9] fix(training): end the episode when a tool misses its deadline, and wait out teardown at exit Cancelling a timed-out tool call doesn't stop what the call does, so the server now ends the episode and the rollout is left unscored; the session is kept for the next one. The exit hook waits up to 180 s for each local server to tear down its episodes, past a sandbox's two-minute teardown, and warns when one is still tearing down. Co-Authored-By: Claude Opus 5.5 --- src/agent_env/openenv/environment.py | 8 +++++--- src/agent_env/training/trl.py | 19 ++++++++++++------- tst/unit/training/trl_test.py | 25 +++++++++++++++++++------ 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/agent_env/openenv/environment.py b/src/agent_env/openenv/environment.py index 3e65ef590..2d0a09290 100644 --- a/src/agent_env/openenv/environment.py +++ b/src/agent_env/openenv/environment.py @@ -36,8 +36,8 @@ input_schema={"type": "object", "properties": {"response": {"type": "string", "description": "Your reply."}}}, ) ENDED = "the episode has ended; call reset" -#: The key of a tool call's deadline in seconds in its action's metadata, which a client sets to have the server end a -#: call that runs past it. +#: The key of a tool call's deadline in seconds in its action's metadata. A call that runs past it ends the episode, since +#: cancelling the call doesn't stop what it does, and the session goes on. TIMEOUT_METADATA = "timeout_s" NOT_RESET = "no episode has started; call reset before stepping" @@ -224,7 +224,9 @@ async def _call(self, action: CallToolAction, timeout_s: float | None) -> CallTo except EpisodeError as e: return _tool_error(name, ToolErrorType.TOOL_NOT_FOUND, e) except TimeoutError: - return _tool_error(name, ToolErrorType.TIMEOUT, f"{name} didn't answer within {timeout_s:g}s") + ended = await self._end_with_error(f"{name} didn't answer within {timeout_s:g}s, and the episode ended, " + "since what the call does may still be under way") + return CallToolObservation(tool_name=name, done=True, reward=None, metadata=ended.metadata) except httpx.HTTPError as e: return _tool_error(name, ToolErrorType.TRANSPORT_ERROR, e) if result.isError: diff --git a/src/agent_env/training/trl.py b/src/agent_env/training/trl.py index bf386b124..dfee6eddd 100644 --- a/src/agent_env/training/trl.py +++ b/src/agent_env/training/trl.py @@ -46,8 +46,9 @@ CLOSE_SECONDS = 300.0 #: How much longer than a tool's deadline the client waits for the server to end the call, before dropping the session. GRACE_SECONDS = 30.0 -#: How long the exit hook waits for the sessions to close, and then for each server to stop. -EXIT_SECONDS = 30.0 +#: How long the exit hook waits for the sessions to close, and then for each server ``AgentEnv.local`` started to tear +#: down its episodes and stop: a sandbox's teardown may take two minutes, and they run at once. +EXIT_SECONDS, EXIT_SERVER_SECONDS = 30.0, 180.0 #: How often, and how far apart in seconds, an environment tries to open a session in place of a dropped one while the #: server is at its limit, which it stays at until it has seen the dropped one close. REOPEN_ATTEMPTS, REOPEN_SECONDS = 5, 1.0 @@ -73,8 +74,8 @@ class AgentEnv: Args: url: The server's base URL. split: The split ``dataset`` holds. - tool_timeout: One tool call's deadline in seconds, which the server ends the call at: the model is told the - tool didn't answer, and the episode goes on. + tool_timeout: One tool call's deadline in seconds. The server ends a call that misses it, and the episode with + it, since what the call does may still be under way: the rollout is left unscored, and the session kept. reset_timeout: How long an episode may take to deploy and open its first turn, and each turn to be ended, in seconds. @@ -413,6 +414,9 @@ def stop(self, timeout: float = CLOSE_SECONDS) -> None: _servers.discard(self) self._uvicorn.should_exit = True self._thread.join(timeout) + if self._thread.is_alive(): + logger.warning("the bundle's server at %s was still tearing down its episodes after %gs; their sandboxes " + "may be left running", self.url, timeout) def _episode_loop() -> asyncio.AbstractEventLoop: @@ -439,14 +443,15 @@ async def _gathered(coroutines: Any) -> list[Any]: def _close_all() -> None: """A trainer that stops or crashes ends the episodes it left running, since TRL never closes its environments, - then stops the servers ``AgentEnv.local`` started, waiting at most ``EXIT_SECONDS`` for each. It runs before - threading's shutdown, while the thread pools an episode's teardown uses still take work.""" + then stops the servers ``AgentEnv.local`` started, which tear down their episodes, waiting at most + ``EXIT_SECONDS`` and ``EXIT_SERVER_SECONDS``. It runs before threading's shutdown, while the thread pools an + episode's teardown uses still take work.""" try: _on_loop(_gathered(environment._close() for environment in list(_environments)), EXIT_SECONDS) except Exception as e: logger.warning("couldn't end the episodes left running: %s", _why(e)) for server in list(_servers): - server.stop(EXIT_SECONDS) + server.stop(EXIT_SERVER_SECONDS) threading._register_atexit(_close_all) diff --git a/tst/unit/training/trl_test.py b/tst/unit/training/trl_test.py index 781f7692a..09290aad7 100644 --- a/tst/unit/training/trl_test.py +++ b/tst/unit/training/trl_test.py @@ -180,20 +180,21 @@ async def close(self): return _Session() -def test_a_tool_that_misses_its_deadline_is_ended_by_the_server_and_the_episode_and_its_session_go_on( - served, monkeypatch): +def test_a_tool_that_misses_its_deadline_ends_the_episode_unscored_and_its_session_goes_on( + served, monkeypatch, caplog): monkeypatch.setattr(proxy_module, "ToolSession", _Hanging()) env = served({"a": PLAYED}, tool_timeout=0.5, max_concurrent_envs=1) environment = env.factory() environment.reset(**env.dataset[0]) - assert environment.wait() == "wait didn't answer within 0.5s" - environment.end_turn(response="Added a crate.") - assert environment.get_reward() == 1.0 + with caplog.at_level(logging.WARNING, logger=trl.__name__): + assert environment.wait() == FAILED + assert environment.get_reward() is None environment.reset(**env.dataset[0]) environment.end_turn(response="Added a crate.") assert environment.get_reward() == 1.0 + assert "rollout left unscored: wait didn't answer within 0.5s, and the episode ended" in caplog.text def test_a_session_refused_after_one_was_dropped_leaves_only_that_rollout_unscored(served, caplog): @@ -294,10 +295,22 @@ def test_a_tool_trl_cant_call_is_refused_with_how_to_fix_it(served, tools): env.factory # noqa: B018 +class _SlowTeardown: + """A teardown hold that lets each teardown finish after half a second.""" + + async def wait(self): + await asyncio.sleep(0.5) + + def set(self): + pass + + @pytest.mark.parametrize("end", ["close", "exit"]) -def test_a_bundle_served_by_local_plays_and_its_episodes_left_running_end_at_close_and_at_exit( +def test_a_bundle_served_by_local_plays_and_its_episodes_left_running_end_at_close_and_at_exit_waiting_out_a_slow_teardown( tmp_path, monkeypatch, teardowns, end): monkeypatch.setenv("HOME", str(tmp_path)) + monkeypatch.setattr(trl, "EXIT_SECONDS", 0.1) + teardowns.hold = _SlowTeardown() unbound = {key: value for key, value in ALICE.items() if key not in ("env_ids", "role")} env = AgentEnv.local(layout(tmp_path / "dock", {"tasks/a.json": json.dumps([ITEMS, unbound, TURN, REPLIES])})) environment = env.factory() From 2010c68516ae2e15220ac30701737b93e86cfb18 Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Wed, 28 Oct 2026 11:20:16 -0700 Subject: [PATCH 9/9] fix(training): give a local server a teardown's worst case to stop An episode's teardown saves the agents' changelogs, ends open recordings and terminates the sandboxes, each part within TERMINATE_TIMEOUT_SECONDS, so a server AgentEnv.local started now gets three of those plus a minute to stop, at close and at exit. Co-Authored-By: Claude Opus 5.5 --- src/agent_env/training/trl.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/agent_env/training/trl.py b/src/agent_env/training/trl.py index dfee6eddd..9f4123a63 100644 --- a/src/agent_env/training/trl.py +++ b/src/agent_env/training/trl.py @@ -35,6 +35,7 @@ from agent_env.openenv import build_app from agent_env.openenv.environment import END_TURN as END_TURN_TOOL from agent_env.openenv.environment import TIMEOUT_METADATA, Toolset +from agent_env.task.teardown import TERMINATE_TIMEOUT_SECONDS from agent_env.training._tools import tool_method logger = logging.getLogger(__name__) @@ -46,9 +47,12 @@ CLOSE_SECONDS = 300.0 #: How much longer than a tool's deadline the client waits for the server to end the call, before dropping the session. GRACE_SECONDS = 30.0 -#: How long the exit hook waits for the sessions to close, and then for each server ``AgentEnv.local`` started to tear -#: down its episodes and stop: a sandbox's teardown may take two minutes, and they run at once. -EXIT_SECONDS, EXIT_SERVER_SECONDS = 30.0, 180.0 +#: How long a server ``AgentEnv.local`` started is given to stop, at ``close`` and at exit: its episodes' teardowns run +#: at once, and each saves the agents' changelogs, ends open recordings and terminates the sandboxes, every part within +#: ``TERMINATE_TIMEOUT_SECONDS``. +STOP_SECONDS = 3 * TERMINATE_TIMEOUT_SECONDS + 60.0 +#: How long the exit hook waits for the sessions to close. +EXIT_SECONDS = 30.0 #: How often, and how far apart in seconds, an environment tries to open a session in place of a dropped one while the #: server is at its limit, which it stays at until it has seen the dropped one close. REOPEN_ATTEMPTS, REOPEN_SECONDS = 5, 1.0 @@ -410,7 +414,7 @@ def __init__(self, app: Any): self.url = f"http://127.0.0.1:{self._uvicorn.servers[0].sockets[0].getsockname()[1]}" _servers.add(self) - def stop(self, timeout: float = CLOSE_SECONDS) -> None: + def stop(self, timeout: float = STOP_SECONDS) -> None: _servers.discard(self) self._uvicorn.should_exit = True self._thread.join(timeout) @@ -444,14 +448,14 @@ async def _gathered(coroutines: Any) -> list[Any]: def _close_all() -> None: """A trainer that stops or crashes ends the episodes it left running, since TRL never closes its environments, then stops the servers ``AgentEnv.local`` started, which tear down their episodes, waiting at most - ``EXIT_SECONDS`` and ``EXIT_SERVER_SECONDS``. It runs before threading's shutdown, while the thread pools an - episode's teardown uses still take work.""" + ``EXIT_SECONDS`` for the sessions and ``STOP_SECONDS`` for each server. It runs before threading's shutdown, while + the thread pools an episode's teardown uses still take work.""" try: _on_loop(_gathered(environment._close() for environment in list(_environments)), EXIT_SECONDS) except Exception as e: logger.warning("couldn't end the episodes left running: %s", _why(e)) for server in list(_servers): - server.stop(EXIT_SERVER_SECONDS) + server.stop() threading._register_atexit(_close_all)