You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(config): canonical subagent model policy with a dedicated endpoint (#236)
## Related Issue
No issue. Fourth step of the subagent routing work. Stacked on #235
(merge #234 and #235 first; until then this diff includes their
commits).
## Problem
The subagent model configuration was validated only when a session
started, so any write could persist an unresolvable policy and the user
found out later. Several runtime readers consumed the legacy
`secondary_model` fields directly, and nothing let a client save the
policy without racing another writer.
## What changed
- `session/subagent/policy.ts`: `LegacySecondaryModelConfig` (disk /
legacy REST) is separated from `CanonicalSubagentModelPolicy` (`inherit
| default | pool | force`, zod discriminated union).
`normalizeLegacySecondaryModel` covers every legacy field combination
(16-row table test), persisted inherit is the absent section, canonical
values never carry legacy fields. `validateSubagentModelPolicy` is pure
and takes a `resolveModel` context; `prospectiveModelView` builds that
context from a previewed configuration. `subagentPolicyResourceVersion`
(strong hash over the canonical policy, `null` for the absent section),
`routingEnvironmentRevision` (ambient inputs only) and
`routeDecisionFingerprint` (request intent) are separate hashes.
- `ISubagentModelPolicyService` (App scope): `get()`, `getEffective()`
(effective policy is inherit while the feature is disabled, and its
source is then `default`), `set`/`clear` with an `expectedVersion` guard
(`config.version_conflict`), `prepareLegacyMutation(input, context?)`
for coordinators, `resolveRevision(caller)`.
- `IConfigService.previewReplaceSections(sections)`: the effective
configuration a replacement would yield (defaults, env bindings,
overlays such as the `PYTHINKER_MODEL_NAME` model, memory overrides)
with zero writes, zero events, zero registry mutation.
- `POST /config`: `secondary_model` goes through `prepareLegacyMutation`
against the prospective configuration of the same request, then one
`replaceSections()`. The B1 direct staging is gone. Provider discovery
routes its cascaded section through the same preparation.
- `GET/PUT/DELETE /config/subagent-model-policy` with a strong `ETag`
(`"subagent-policy-v1:<hash>"`, never weak) and `If-Match` → HTTP 412 +
`41201`. `DELETE` removes the section; legacy `POST /config` and
canonical `PUT` produce byte-identical files.
- Runtime readers in `configSection.ts` derive from the canonical
policy. An import-boundary test keeps legacy symbols inside the
adapter/policy service and asserts the section is written only by the
policy service or a prepared mutation.
- Existing gateway tests that persisted unknown model aliases now
configure those models first (validation at write time is the new
contract). API surface snapshot and config manifest updated.
Tests added: policy table + messages + round-trip + strict parsing;
validation matrix; prospective view; resource version stability (key
order, legacy spellings, absent section); revision/fingerprint
separation; service unit tests (set/clear/version
guard/effective/prepare/revision); `previewReplaceSections`
zero-side-effect test and a removal/swap prospective test; gateway
endpoint tests (ETag strong, 412 on PUT and DELETE, DELETE removes the
section, no-op PUT keeps the ETag, external file edit changes it, legacy
POST ≡ canonical PUT, feature-disabled effective inherit); `/config`
prospective-model and env-overlay tests.
## Checklist
- [x] I have read the
[CONTRIBUTING](https://github.com/PyModel/pythinker-code/blob/main/CONTRIBUTING.md)
document.
- [ ] I have linked a related issue (external PRs: the issue must have a
maintainer's `/approve`).
- [x] I have added tests that prove my feature works.
- [x] Ran `gen-changesets` skill, or this PR needs no changeset.
- [x] Ran `gen-docs` skill, or this PR needs no doc update.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
* **New Features**
* Added subagent model policy configuration with inherit, default, pool,
and force modes.
* Added API support to view, update, and clear policies, including
effective settings and version safeguards.
* Added detailed model validation, effort settings, and compatibility
with existing secondary-model configuration.
* **Bug Fixes**
* Prevented invalid model references from persisting during provider
refreshes.
* Removed unavailable models from secondary-model pools and cleared
dangling defaults.
* **Documentation**
* Added release metadata describing the new policy configuration
options.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
'[secondary_model].force cannot be combined with [secondary_model.models]: the pool table only exists to offer the main agent a choice, and force removes that choice';
'[secondary_model].default_model is required when [secondary_model.models] is configured';
122
-
123
-
exportconstSECONDARY_MODEL_PRIMARY_MODEL_RESERVED_MESSAGE=`[secondary_model.models] key "${PRIMARY_SUBAGENT_MODEL_CHOICE}" is reserved: it always binds the caller's own model. Rename the pool entry.`;
0 commit comments