Skip to content

Complete the child-customization model (per-child enable/disable + invocation matrix)#288

Open
colbylwilliams wants to merge 4 commits into
mainfrom
colby-child-customization-model
Open

Complete the child-customization model (per-child enable/disable + invocation matrix)#288
colbylwilliams wants to merge 4 commits into
mainfrom
colby-child-customization-model

Conversation

@colbylwilliams

Copy link
Copy Markdown
Member

Closes #285.

What & why

The customization model could express container-level enable/disable but not per-child state, and its agent/skill invocation gating was asymmetric. This completes both halves.

A. Per-child enable/disable + child-addressable toggling

  • New non-exported ChildCustomizationBase (extends CustomizationBase) adds an optional enabled flag, shared by the five leaf children — AgentCustomization, SkillCustomization, PromptCustomization, RuleCustomization, HookCustomization. Absent ⇒ enabled.
  • session/customizationToggled (and every client reducer) now matches id against top-level containers first, then the children inside every container, and sets that entry's enabled.
  • Documented precedence: a child's effective state is container.enabled && (child.enabled ?? true), so disabling a container still disables all its children.

B. Symmetric user/model invocation matrix

  • SkillCustomization gains disableUserInvocation (it already had disableModelInvocation).
  • AgentCustomization gains both disableModelInvocation and disableUserInvocation.

All fields are optional and additive — backward compatible, so no protocol-version bump.

Key design decision

The issue suggested putting enabled? on the shared base "next to id/uri/name". I initially did exactly that (on CustomizationBase) and validated the result — but the client generators (Rust/Go/Kotlin/Swift) inherit the base's optional declaration and ignore the required redeclaration on McpServerCustomization/containers, flipping their enabled to optional. The JSON-schema generator, by contrast, resolves the override correctly and keeps them required — so the clients and schema would disagree on the wire contract for containers and MCP servers.

A dedicated ChildCustomizationBase that only the five leaf children extend (McpServer and the containers keep their own required enabled) sidesteps the conflict entirely: every generator agrees, and this matches the issue's "shared child base" intent.

Changed surface

  • Canonical types: types/channels-session/{state,actions,reducer}.ts.
  • Conformance fixtures: 225 (toggle a child by id; also exercises the top-level-McpServer and skip-McpServer-container branches) and 226 (no-op for unknown child id). Reducer branch coverage stays at 100%.
  • Generated mirrors: schema + all five client type mirrors (regenerated; idempotent).
  • Hand-written client reducers: Rust / Go / Kotlin / Swift each gain a set*ChildEnabled helper and a child-search fallback in the toggle path.
  • Docs: docs/guide/customizations.md (Children, metadata table, Toggling) and docs/guide/actions.md; reference docs regenerated.
  • CHANGELOGs: root + all five clients under ## [Unreleased].

Verification

npm run generate (idempotent) and npm run test (typecheck, lint, verify:release-metadata, verify:changelog, 268 reducer tests @ 100% branch coverage) pass. The shared fixtures 225/226 were confirmed running and green in all five implementations: TypeScript, Rust (cargo test), Go (go test ./...), Kotlin (./gradlew test), and Swift (swift test).

Add per-child enable/disable plus a symmetric agent/skill invocation
matrix, completing the customization model.

- Introduce `ChildCustomizationBase` with an optional `enabled` flag,
  shared by the five leaf child customizations (Agent/Skill/Prompt/Rule/
  Hook); absent means enabled. McpServer and the container types keep
  their own required `enabled`. (Putting `enabled?` directly on
  `CustomizationBase` makes the client generators inherit the optional
  declaration and flip McpServer/containers to optional, disagreeing with
  the schema — a dedicated child base avoids that.)
- Extend `session/customizationToggled` and every client reducer to
  toggle a top-level container OR an individual child by id. A child's
  effective state is `container.enabled && (child.enabled ?? true)`.
- Add `disableUserInvocation` to `SkillCustomization`, and
  `disableModelInvocation` + `disableUserInvocation` to
  `AgentCustomization`.

Regenerate the schema and all five client mirrors, update the
customizations/actions guides and reference docs, refresh all six
CHANGELOGs, and add reducer conformance fixtures (225/226). Verified
across TypeScript, Rust, Go, Kotlin, and Swift.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR completes the session customization model by adding per-child enable/disable state and by making the skill/agent invocation gating symmetric between user-driven and model-driven invocation. It updates the canonical TypeScript protocol types/reducer, propagates the change through schema + generated client mirrors, and updates docs, changelogs, and conformance fixtures.

Changes:

  • Add an optional enabled?: boolean to leaf child customization types and extend session/customizationToggled to target either a container or a child by id.
  • Add disableUserInvocation to skills, and add both disableModelInvocation + disableUserInvocation to agents to complete the user/model invocation matrix.
  • Update conformance reducer fixtures, docs, schemas, and all client implementations (reducers + generated types) accordingly.
Show a summary per file
File Description
types/test-cases/reducers/225-session-customizationtoggled-toggles-child-by-id.json Adds fixture coverage for toggling a child customization by id.
types/test-cases/reducers/226-session-customizationtoggled-is-no-op-for-unknown-child-id.json Adds fixture coverage for unknown child id no-op behavior.
types/channels-session/state.ts Introduces ChildCustomizationBase and adds enabled?: boolean + invocation gating fields to relevant child types.
types/channels-session/reducer.ts Extends session/customizationToggled reducer logic to match top-level first, then fall back to child search.
types/channels-session/actions.ts Updates action docs to describe container-or-child targeting and effective enabled precedence.
schema/state.schema.json Regenerates schema to include child enabled and new invocation gating fields.
schema/notifications.schema.json Regenerates schema mirrors for notification-side type refs.
schema/errors.schema.json Regenerates schema mirrors for error-side type refs.
schema/commands.schema.json Regenerates schema mirrors for command-side type refs.
schema/actions.schema.json Regenerates action schema + updates SessionCustomizationToggledAction docs.
docs/guide/customizations.md Documents per-child enable/disable, precedence, and the symmetric invocation matrix.
docs/guide/actions.md Updates action reference text for session/customizationToggled.
CHANGELOG.md Adds spec-level Unreleased entries for child enable/disable + invocation gating changes.
clients/typescript/CHANGELOG.md Adds TypeScript client Unreleased entries reflecting the new behavior/surface.
clients/swift/CHANGELOG.md Adds Swift client Unreleased entries reflecting the new behavior/surface.
clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/NativeReducer.swift Extends Swift reducer toggle implementation to support child toggles.
clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift Regenerated Swift types to include child enabled + invocation gating fields.
clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift Regenerated Swift actions to reflect updated toggle docs.
clients/rust/CHANGELOG.md Adds Rust client Unreleased entries reflecting the new behavior/surface.
clients/rust/crates/ahp/src/reducers.rs Extends Rust reducer toggle implementation to support child toggles.
clients/rust/crates/ahp-types/src/state.rs Regenerated Rust types to include child enabled + invocation gating fields.
clients/rust/crates/ahp-types/src/actions.rs Regenerated Rust actions docs for container-or-child toggle targeting.
clients/kotlin/CHANGELOG.md Adds Kotlin client Unreleased entries reflecting the new behavior/surface.
clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt Extends Kotlin reducer toggle implementation to support child toggles.
clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt Regenerated Kotlin types to include child enabled + invocation gating fields.
clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt Regenerated Kotlin actions docs for container-or-child toggle targeting.
clients/go/CHANGELOG.md Adds Go client Unreleased entries reflecting the new behavior/surface.
clients/go/ahptypes/state.generated.go Regenerated Go types to include child Enabled + invocation gating fields.
clients/go/ahptypes/actions.generated.go Regenerated Go actions docs for container-or-child toggle targeting.
clients/go/ahp/reducers.go Extends Go reducer toggle implementation to support child toggles.

Review details

  • Files reviewed: 26/30 changed files
  • Comments generated: 2
  • Review effort level: Low

Comment thread types/channels-session/reducer.ts Outdated
Comment thread clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt Outdated
The SessionCustomizationToggled child-search fallback in the canonical
TypeScript reducer and the hand-written Kotlin reducer built a full
copy of the customizations list via map() even when no child matched,
then discarded it. Replace the map()+changed-flag with an early-return
loop that scans in place and only copies the list (and the matched
container's children) when a child id actually matches, returning the
original state untouched on the no-op path.

No behavioral change: same outputs for every fixture (060/061/062/225/
226), fewer allocations on the no-match path. The Rust, Go, and Swift
reducers already used early-return loops and are unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

  • Files reviewed: 26/30 changed files
  • Comments generated: 2
  • Review effort level: Low

Comment thread types/channels-session/actions.ts Outdated
Comment on lines +277 to +283
* Targets either a top-level container (plugin or directory) or an
* individual child (a skill, agent, or other entry inside a container) by
* `id`, and sets that entry's `enabled` flag. Disabling a container still
* disables all of its children — the effective state of a child is
* `container.enabled && (child.enabled ?? true)` — so toggling a child
* only matters while its container is enabled. Is a no-op when no
* customization (container or child) has the given `id`.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 211ad0c. Reworded the JSDoc to say it matches id against every top-level customization first — a plugin or directory container, or a bare top-level MCP server — then against container children, and dropped the "(container or child)" qualifier on the no-op clause. Regenerated the Go/Rust type mirrors and JSON schema from the updated JSDoc.

Comment thread docs/guide/customizations.md Outdated
The SessionCustomizationToggled doc said the action targets a "top-level
container (plugin or directory)", but the reducer matches `id` against
every top-level customization first — including a bare top-level MCP
server — before searching container children. Reword the action JSDoc
and the customizations guide (intro, code comment, prose, and action
table) so they no longer imply top-level MCP servers can't be toggled by
id. Regenerated the Go/Rust type mirrors and JSON schema from the JSDoc.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@connor4312

Copy link
Copy Markdown
Member

I'm OK with this in general but I didn't implement it initially for because the Copilot SDK has no way to actually do this. The SDK just takes pluginDirectories but doesn't allow enabling/disabling the constintuent parts. (Of course we could copy and rewrite plugins before giving them to the SDK to remove parts that are disabled but.) Do you know if this is a use case the SDK plans to support?

…tion-model

# Conflicts:
#	CHANGELOG.md
#	clients/go/CHANGELOG.md
#	clients/go/ahptypes/state.generated.go
#	clients/kotlin/CHANGELOG.md
#	clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt
#	clients/rust/CHANGELOG.md
#	clients/rust/crates/ahp-types/src/state.rs
#	clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift
#	clients/swift/CHANGELOG.md
#	clients/typescript/CHANGELOG.md
#	docs/guide/customizations.md
#	schema/actions.schema.json
#	schema/commands.schema.json
#	schema/errors.schema.json
#	schema/notifications.schema.json
#	schema/state.schema.json
#	types/channels-session/state.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Complete the child-customization model: per-child enable/disable + a symmetric invocation matrix

3 participants