Repository navigation
[api][plan][java] Validate router candidates and default model at declaration - #1151
Conversation
7ae85e3 to
4af5405
Compare
4af5405 to
3ac4048
Compare
|
Two corrections to the description and one small commit. The old failure happened when the router was resolved on the TaskManager, before the durable routing call, not inside it. And only four of the five new |
a2a7f8b to
a76df74
Compare
…laration Generated-by: Claude Code 2.1.280 (Claude Fable 5.1)
…r constructor Generated-by: Claude Code 2.1.280 (Claude Fable 5.1)
a76df74 to
3e4d231
Compare
wenjin272
left a comment
There was a problem hiding this comment.
Thanks for taking this on. LGTM
Linked issue: #1150
Purpose of change
User-visible outcome
ModelRouter.of(...).build()andnew AgentPlan(...)now reject a router declaration with a duplicate or empty candidate, no candidates, or a default model that is not a candidate, with anIllegalArgumentExceptionthat names the offending value. Previously such a declaration passed both and failed only when the router was resolved on the TaskManager, once per routed request, before the durable routing call.Intent
These are static declaration errors, like a rule key that names no candidate or a judge model that is not registered. #1042 moved those two checks to plan construction; this change gives the candidate list and the default model the same treatment.
Runtime flow
Unchanged for a valid declaration. For an invalid one the exception surfaces at
build()(fluent path) or atnew AgentPlan(...)(descriptor path) instead of atModelRoutingResolver#resolve->ctx.getResourceon the TaskManager. The constructor keeps the check for a descriptor that reaches it without passing through either.Key decisions
RoutingCandidateValidator#validate(subject, candidates, defaultModel), called by the constructor, the builder andAgentPlan, so the three paths share the check and the message without exception wrapping. It is a separate util class inapi, likeToolParameterInjectionValidator(also called fromAgentPlan), rather than a method onModelRouter, and it is marked@InternallikeSkillUrlUtils.List<?>andObject, because a deserialized descriptor may carry non-String elements; those are rejected with the candidate-name message rather than aClassCastException.ModelRouter.DEFAULT_MODEL_KEYreplaces the literal"default_model"in the constructor, the builder andAgentPlan, asCANDIDATES_KEYalready does.Behavioral Semantics
Interaction decisions
build()runs the candidate check before the existing rule-key check, so a declaration with several problems reports the first one in this order: candidates, default model, rule keys.AgentPlan#validateRoutingStrategiesruns it after thecandidatesshape guard, for every strategy type.Behavioral contracts
IllegalArgumentException("requires at least one candidate").StringraisesIllegalArgumentExceptionnaming the value.IllegalArgumentExceptionnaming the candidate.IllegalArgumentExceptionnaming the model and the candidates.build(), at plan construction, and in the constructor. The constructor's messages for 3 and 4 are unchanged; plan construction prefixes them withModel router '<name>'like the other plan-time router diagnostics. Adefault_modelthat is not aStringis rejected the same way in the constructor (second commit).Failure behavior
An invalid declaration fails the registration call or plan construction and never reaches the TaskManager. Previously, under the default
FAILpolicy the job failed on the first routed record, and underIGNOREevery routed record was dropped while the job stayed RUNNING.Tests
Contracts to tests
RoutingTest.builderRequiresAtLeastOneCandidate,AgentPlanLlmJudgeValidationTest.emptyCandidateListFailsAtPlanConstructionRoutingTest.builderRejectsBlankCandidateName,RoutingTest.routerConstructionRejectsBlankCandidateNameRoutingTest.builderRejectsDuplicateCandidate,AgentPlanLlmJudgeValidationTest.duplicateCandidateFailsAtPlanConstructionRoutingTest.builderRejectsDefaultModelThatIsNotACandidate,AgentPlanLlmJudgeValidationTest.defaultModelNamingNonCandidateFailsAtPlanConstruction,RoutingTest.routerConstructionRejectsNonStringDefaultModelrouterConstructionRejectsBlankCandidateNameand the pre-existingdefaultModelMustBeCandidatecover the constructorCoverage and what was not verified
mvn -pl api,plan test: api 460, plan 408, 0 failures.mvn -pl runtime test: 980, 0 failures.spotless:checkand./tools/check-license.shpass. Without theapichange four of the five newRoutingTestcases fail (routerConstructionRejectsBlankCandidateNamealready passed, becauseRoutingCandidaterejected a blank name); without the constructor change in the second commitrouterConstructionRejectsNonStringDefaultModelfails withClassCastException; without theAgentPlanchange the three new plan cases fail; nothing else changes.Not verified: the per-record failure on the TaskManager described above is read from the call path and from the #1042 discussion, not observed on a running job. The Python lane does not apply: routers cannot be registered from Python yet.
Implementation invariants and supporting evidence
candidateslist is no longer deferred to the constructor at plan construction, so thecandidates == nullearly return invalidateRuleKeysbecame dead and is removed.AgentPlanRoutingBackstopTest's minimal router fixture gains one candidate, as it gained a strategy when strategy validation was added.of(...)andbuild().API
Compatibility impact
ModelRouter.DEFAULT_MODEL_KEYis added.RoutingCandidateValidatoris added as@Internal; it is a contract shared with the plan module, not a stable public API. No existing signature changes. An empty-string candidate name, previously accepted bybuild(), is now rejected. A plan that previously constructed with an empty or missingcandidateslist, or a non-candidatedefault_model, now fails at plan construction; such a plan could never route a request.Documentation
doc-neededdoc-not-neededdoc-includedWas this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code 2.1.280 (Claude Fable 5.1)