[ApexAgents] Coerce stringified MCP tool arguments and surface validation errors - #2868
Merged
arti4nvj merged 3 commits intoAug 31, 2026
Conversation
…tion errors Signed-off-by: Serge Panev <spanev@nvidia.com>
Signed-off-by: Serge Panev <spanev@nvidia.com>
arti4nvj
requested changes
Aug 31, 2026
| resolved = resolve(definition_tables[ref], expanding | {ref}) | ||
| siblings = {key: resolve(value, expanding) for key, value in node.items() if key != "$ref"} | ||
| if isinstance(resolved, dict): | ||
| return {**resolved, **siblings} |
Contributor
There was a problem hiding this comment.
One concern here is that sibling constraints are applied in addition to the reference schema rather than overwriting it. So if def has maxlen=1 and $ref site as maxlen=3, the merge would set maxlen=3 overwriting and accepting the value that the og schema rejects
could we preserve the $ref and $defs and copy the resolved type onto the reference site instead?
Contributor
Author
There was a problem hiding this comment.
Good catch, fixed in the latest commit. Implemented what you suggested: keep $ref/$defs, copy only the resolved type onto the ref site.
Signed-off-by: Serge Panev <spanev@nvidia.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.
Problem
GLM 5.2 rollouts fail 100% of tool calls to Archipelago MCP tools whose schema wraps parameters in a bare-
$refproperty with no inlinetype(e.g.{"properties": {"request": {"$ref": "#/$defs/DynamicModel"}}}). vLLM's glm47 tool-call parser reconstructs argument types fromproperties[key].type; with no inline type the object value arrives as a JSON-encoded string ({"request": "{\"code\": ...}"}), Stirrup's pydantic validation rejects it with the bareTool arguments are not valid, and the model retries the same shape indefinitely. Observed in the kimi-vs-glm52 trajectory comparison: 12/12code_code_execfailures, 3/3mail_search_mail, 113 trajectory items and 3 context compactions vs 13 items for Kimi-K3 on the same task. Flat inline-typed schemas (todo_write,mail_read_mail) are unaffected.The typing bug is serving-side (same family as vLLM PR #41801 for
deepseek_v4), so the model cannot self-correct no matter how the error is phrased — the harness has to handle it.Changes
Repair the symptom (coercion safety net):
stirrup_runtime.py: addcoerce_tool_arguments, a validation-gated repair that (a) unwraps top-level string values that JSON-decode to an object/array where the field annotation expects structured data, and (b) wraps flat arguments into the model's single required nested-model field. A candidate is accepted only if it validates, and dict payloads may only use keys the nested model knows (MCP-generated models default toextra="ignore", so validation alone would silently drop unknown keys and run the tool with defaults).Agent.run_toolviainstall_tool_argument_coercion, installed insiderun_stirrup_rollout(the code must be self-contained in this file — it is the only one staged into the sandbox). Coercion usesmodel_copyso trajectory history keeps the model's raw emission. Finish tools are exempt:Agent.step()re-validates the original tool_call outside any try/except (stirrup 0.1.12agent.py:1261).stirrup_agent/nemo_client.py).Remove the trigger (type annotation at the client boundary):
annotate_schema_ref_types: copies only the resolved definition'stypeonto typeless$refsites —$refand$defsstay intact. Atypesibling equal to the definition's type is a conjunctive no-op under JSON Schema 2020-12 (and ignored under draft-07), so schema semantics are provably unchanged, while the parser now findsproperties[key].typeand stops string-encoding. Full inlining was considered and rejected: merging sibling keys alters 2020-12 conjunctive constraint semantics (e.g. a sitemaxLengthoverriding a stricter definitionmaxLength). Data-position values (const/enum/default/examples) are never annotated, and definition names are RFC 6901-escaped during resolution.install_tool_schema_type_annotationwraps stirrup'sto_openai_toolsand rebinds it instirrup.clients.utilsand the from-import namespaces (chat_completions_client,litellm_client) — the client calls the copy in its own namespace.inspect_tooloutput shows the same annotated shape the wire carries.Testing
pytest responses_api_agents/apex_agent/tests/— 62 passed (41 pre-existing, no regressions).ruff check/ruff format --checkclean; module-level imports ofstirrup_runtime.pyverified stdlib-only (barepython3import without stirrup installed).json_schema_to_pydantic-generated model thatMCPToolProviderbuilds: all three observed GLM shapes repair; garbage and already-valid args are left alone; the patchedto_openai_toolsin theChatCompletionsClientnamespace emits{"$ref": ..., "type": "object"}at the property site with$defspreserved; raw-vs-annotated schemas agree on ~10k adversarial + fuzzed instances under jsonschema Draft 2020-12 and Draft-07, including the conflicting-maxLengthconjunction case.