Repository navigation
fix(training): TRL tools carry their MCP input schema, nested types included - #397
Merged
Merged
Conversation
…ncluded
TRL turns an environment_factory's methods into tool schemas with transformers' get_json_schema, which reads only
type hints and docstrings, so an argument like an array of objects reached the model as items: {type: object}, its
fields, required lists, enums and bounds only in prose. Each environment instance now sets a SchemaTool over each tool
method but end_turn: a dict holding the tool's published JSON Schema, which transformers renders as given, that TRL
calls by name. The factory checks at build time that transformers renders the tools as given, and transformers is
now declared by the trl extra, since the module imports it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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 changes
TRL turns an
environment_factory's methods into tool schemas with transformers'get_json_schema, which reads only a function's type hints and Google docstring. A type hint can't carry an object's properties, so a tool argument such as an array of objects reached the model as{"type": "array", "items": {"type": "object"}}, with its fields, required lists, enums and bounds only in prose. On the GridSim desk, untrained Qwen3-4B's main failure mode is malformed nested JSON for the offer tools, which take arguments like this.Neither TRL (1.16, 1.17, main) nor transformers (5.21, 5.22, main) has a supported way to attach a schema to a callable. TRL lists an environment's tools as functions on the class, fetches each from the instance with
getattr, and passes what it fetched straight toapply_chat_template(tools=...)andparse_response(tools=...). Transformers renders adicttool as given and converts a function or method withget_json_schema.SchemaTool(agent_env/training/_tools.py) is both things at once. It is adictholding{"type": "function", "function": {name, description, parameters: inputSchema}}, which transformers renders unchanged. It also has__name__and__call__, which is all TRL needs to call it.SchemaToolover every tool method exceptend_turn. The class methods stay as they are, because TRL finds the tools through them.end_turnkeeps its current schema.SchemaToolhas no docstring, soget_json_schemaon one raisesDocstringParsingExceptioninstead of building a wrong schema. On Python 3.14 it would otherwise silently produce a schema with no properties.SchemaToolas its own schema. Otherwise it raises aRuntimeErrornaming the tools.transformers>=5.2moves fromdevinto thetrlextra.trl.pynow imports transformers for that build-time check, andtest_a_module_using_an_extra_imports_only_what_one_extra_and_core_declarerequires a module's imports to be declared by its extra. TRL already requires transformers, so this adds nothing to a TRL install, and the extra still doesn't install TRL. 5.2 is the version TRL'senvironment_factoryneeds. The lockfile only moves the extra marker; resolution is unchanged.submit_dam_offerson the GridSim desk, before and afterThese come from
AgentEnv.localon the desk-v4 bench bundle, rendered by Qwen3-4B's chat template.Before,
curves(as_offersthe same way):{"type": "array", "items": {"type": "object"}, "nullable": true, "description": "curves Each entry: hours (string or array, required): operating hours: \"HE07-HE10\", \"HE07\", \"all\", or a list like [7, 8]; points (array): [[mw, price], ...] from max charge (negative MW) to max discharge; MW and price non-decreasing; 2-10 points."}as_offers[].productshowed as a bare string, and its enum was lost.After, exactly what the env publishes:
{"type": "array", "items": {"type": "object", "properties": { "hours": {"description": "operating hours: \"HE07-HE10\", \"HE07\", \"all\", or a list like [7, 8]", "anyOf": [{"type": "string"}, {"type": "array", "items": {"type": "integer"}}]}, "points": {"type": "array", "items": {"type": "array", "items": {"type": "number"}, "minItems": 2, "maxItems": 2}, "description": "[[mw, price], ...] from max charge (negative MW) to max discharge; MW and price non-decreasing; 2-10 points"}}, "required": ["hours"]}}as_offers[].productis now{"type": "string", "enum": ["regup", "regdn", "rrspfr", "rrsffr", "ecrs", "nspin"]}.Through the new factory, all 11 desk tools render as published and
end_turnkeeps its method schema.Token cost: the system prompt with the desk's 12 tools goes from 3,241 to 3,573 Qwen3-4B tokens (+332).
submit_dam_offersalone goes from 558 to 730.Versions checked
GRPOTrainerand AsyncGRPO's rollout worker find, render, parse and call tools the same way.chat_template_utilsis identical. The build-time check also passes on the 5.2.0 floor.tst/trlnow pins that a realGRPOTrainerstep shows the model the published nested schema andend_turn's current one. It passes locally with TRL 1.16.0 and 1.17.0, transformers 5.21.0, CPU torch 2.15.0 and Python 3.12. The tier's workflow runs the same matrix.Tests
tst/unit/training/tools_test.py:SchemaToolrenders throughapply_chat_templatewith Qwen3's chat template to exactly its schema;parse_response, with TRL's Qwen3 response template, types a call's arguments from that schema;get_json_schemarefuses the object.tst/data/training/qwen3_chat_template.jinja, since CI has no tokenizer cache. A word-level tokenizer built in memory carries it.tst/unit/training/trl_test.py:build_appand fetched as TRL fetches it;end_turnrenders to its current schema;tst/unit/openenv/environment_test.py: the fake tool session_Toolscan now offer wholeEnvironmentTools, not only names. That is how the trl tests serve a nestedinputSchemathrough the real server. Existing callers that pass names behave as before.make unit-test: 11,781 passed. Two tests fail on the Mac, and neither is affected by this change:hermes/test_hermes_bridge.py::test_the_event_pipe_is_not_left_open_in_the_serverreads/procand fails on unmodified main too.gateway/process_scope_socket_test.py::test_an_armed_time_trigger_keeps_firing_after_a_client_leavespasses when run alone.ruff checkis clean, anduv lock --checkpasses.Trainer scripts need no change to benefit:
GRPOTrainer(environment_factory=env.factory, ...)and AsyncGRPO pick up the schemas as they are.🤖 Generated with Claude Code


Confidence Score: 5/5
The PR appears safe to merge; no actionable issue was found.Summary
SchemaToollets TRL call each tool while giving transformers the full published input schema.earakely-scale explicitly documents the class-level method-schema fallback as intentional. Tests were inspected, not run during this review.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR A[Published tool schema] --> B[Class tool method] A --> C[SchemaTool dictionary] B --> D[Bound instance method] D --> C C --> E[Transformers renders full schema] C --> F[TRL calls tool] F --> D D --> G[Environment tool call]Reviews (1) · Last reviewed commit: "fix(training): TRL tools carry their MCP..." · Reviewed by Greptile