Repository navigation
fix(openenv): a scored episode answers at once, and a role-less agent's toolset keys the default role's access - #393
Merged
earakely-scale merged 3 commits intoOct 29, 2026
Conversation
…'s toolset keys the default role's access A rollout's reward came back about 2.4 s after its last end_turn on a GridSim bundle, and TRL collects rewards one at a time, so that was about 45 s of every 32-rollout GRPO step: - prompt_agent noticed the turn had ended only at its next poll of the proxy (2 s by default). poll_a2a_task now takes a wake from whoever ends the task in this process (wake_poll), and an episode's turn wakes it as it ends; a woken poll asks again every 50 ms for up to 2 s, since the proxy shows the task as ended a moment after it accepts the reply. - The closing end_turn answered only after the episode had torn down. Episode.result(teardown=False) returns once the run's steps have finished, next_turn() returns None then too, and the OpenEnv env answers with the scores and lets the teardown finish behind, as a call past its deadline already did; the next reset or close waits for it. ServedTask.toolset kept a modify_env_tool_access only when its role equalled the played agent's, but an agent deployed with no role gets the gateway's "default", so a change on "default" fell out of its key and tasks that differ only by it shared an environment class. On the GridSim bundle, get_reward() now takes 0.03-0.04 s, from 2.45-2.62 s on 0.9.1380. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A turn ended in the moment between the proxy opening it and prompt_agent starting its poll dropped its wake, so that poll waited out a full interval. wake_poll now leaves the event set when no poll is registered, and poll_a2a_task takes it up as it begins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| def wake_poll(task_id: str) -> None: | ||
| """Have this process's poll of ``task_id`` ask for its state now, or as it begins: for whoever just ended the task | ||
| here, as an episode ending a turn its proxy holds does, so the step waiting on it goes on without a poll interval.""" | ||
| _polls.setdefault(task_id, asyncio.Event()).set() |
There was a problem hiding this comment.
wake_poll() now keeps an event even when nobody will poll that task again. If a caller ends a timed-out turn before the episode notices it has closed, ProxyClient.reply() returns False, but Turn.end() still calls wake_poll(). The finished poll has already removed its entry, so this creates an entry that nothing removes.
Repeated late replies grow _polls for the life of the server. Bound how long pending wakes live, or remove them when their episode closes, while preserving wakes that arrive before polling starts.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agent_env/a2a_agent/protocol.py
Line: 178
Comment:
**Late replies retain memory**
`wake_poll()` now keeps an event even when nobody will poll that task again. If a caller ends a timed-out turn before the episode notices it has closed, `ProxyClient.reply()` returns `False`, but `Turn.end()` still calls `wake_poll()`. The finished poll has already removed its entry, so this creates an entry that nothing removes.
Repeated late replies grow `_polls` for the life of the server. Bound how long pending wakes live, or remove them when their episode closes, while preserving wakes that arrive before polling starts.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.…s left A scored episode now answers before its teardown, so two sessions that just ended may still be tearing down when the test looks; it waits for them, as it does further on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
earakely-scale
deleted the
edgararakelyan/fd-4628-trl-rewards-no-2-s-poll-or-teardown-before-a-scored-episode
branch
October 29, 2026 20:32
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two fixes in the episode path that
agent_env.training.trltrains through. Both showed up training Qwen3-4B on the GridSim training bundle (the run behind the docs' TRL page).1. A scored episode answers at once. A rollout's reward came back about 2.4 s after its last
end_turn. TRL 1.16'sGRPOTrainercallsget_reward()on each environment in turn, so on that run it was about 45 s of every 190 s step (32 rollouts). Two waits made it up:prompt_agentnotices the turn has endedpoll_interval_seconds(2 s by default)poll_a2a_tasktakes a wake from whoever ends the task in this process (wake_poll), and an episode'sTurn.end()/_fail()wakes it. A woken poll asks again every 50 ms for up to 2 s, since the proxy shows the task as ended a moment (~20 ms) after it accepts the reply; a wake that comes before the poll begins is kept, and the poll takes it up as it starts.end_turnanswersEpisode.result()waits for the whole run)Episode.result(teardown=False)returns once the run's steps have finished,next_turn()returnsNonethen too, and the OpenEnv env answers with the scores and lets the teardown finish behind (_abandon(..., wait=False)), as a call past its deadline already did. The next reset or close of the session, and the server's shutdown, wait for it.2. A role-less agent's toolset keys the default role's access.
ServedTask.toolsetkept amodify_env_tool_accessonly when itsroleequalled the played agent's. An agent deployed with no role gets the gateway'sdefault, so a change ondefaultfell out of its key: tasks that differ only by it shared an environment class, and the first rollout that saw other tools than the class's would stop training. The GridSim bundle's Task API showed"access": []for tasks that all disable two tools ondefault.Measured
get_reward()on the GridSim bundle served byAgentEnv.localon local Docker (Mac), per rollout:Played the way
GRPOTrainerplays them (each environment reset in turn, a scripted tool call each, thenget_reward()each in turn), three rounds:close()Between rounds, 1–2 teardowns are still finishing on this branch (0 on 0.9.1380), so they don't pile up: each session's next reset waits for its own. At 32, Docker Desktop's 4 CPUs on the Mac can't start 32 GridSim envs at once, and both versions leave rollouts unscored at
apply_server_config(ReadTimeout): 64 of 96 on this branch, 75 of 96 on 0.9.1380. A native-Docker box with 4 vCPUs ran 32 at once without that.Behaviour changes
Release note:
env.factorycan become a dict for some bundles. A bundle whose played agent has no role, and whose tasks differ in amodify_env_tool_accessondefault, now serves a toolset per variant, soenv.factoryis a dict of classes (TRL's multi-environment mode) where it was one class. Before, those tasks shared a class whose tools didn't match some of them.A teardown that raises after the episode is scored no longer turns the closing
end_turninto an error observation; the reward stands and the session is freed.teardown_runnever raises (it is best-effort: failures are logged and reported asstill_up), so this changes only what a fake that raises produces; the test that pinned it is renamed and asserts the new outcome.Episode.result(teardown=...)is new and keyword-only, defaulting to the old behaviour;next_turn()now returnsNoneonce the steps have finished rather than once the teardown has.Tests
protocol_test: a woken poll asks at once and again until the task shows as ended, and leaves nothing registered; the fake clock stands in for_nap.run_test: a turn with a 30 s poll interval carries on within 2 s of ending; during a held teardownresult(teardown=False)returns the scores and the episode isn't finished.environment_test: with the teardown held,end_turnanswers with the reward and the teardown finishes after; a role-less agent's toolset carries the access change ondefault; a teardown that raises after scoring leaves the reward.trl_test: counts the teardowns afterclose(), which waits for them.make unit-test: 11,724 passed; twopackages/agentenv-protocoltests failed under-n auto(test_a2a_proxy::…stops_what_it_left,test_computer_queue::…release_timer) and pass alone on this branch and on main.run_test::test_a_turn_not_taken_by_its_deadline_fails_the_run…is flaky on main as well (the in-process proxy takes ~1.7 s to start against a 5 s wait); left as is.🤖 Generated with Claude Code


Confidence Score: 5/5
The PR appears safe to merge, with the previously reported non-blocking memory leak still open.Fix with agent prompt
Summary
Episodes return scores before teardown finishes, and role-less agents include
defaultaccess changes in their toolset keys.Diagram
sequenceDiagram participant Client participant Episode participant Poll participant Cleanup Client->>Episode: End turn Episode->>Poll: Wake poll Poll-->>Episode: Task completed Episode->>Cleanup: Start teardown Episode-->>Client: Return scores Client->>Episode: Reset or close Episode->>Cleanup: Wait for teardownReviews (3) · Last reviewed commit: "test(openenv): wait for the sessions' te..." · Reviewed by Greptile