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
AHP has no mechanism for the copilot-sdkSystemMessageTransform family: a server-initiated, batched, request/response RPC that hands an active client the rendered content of one or more system-prompt sections and receives rewritten content back, used by the agent host when assembling the model's system prompt. This proposal adds:
A new server → client request method systemMessageTransform, registered in ServerCommandMap. (resourceRequest is the only existing entry there — it lives in both CommandMap and ServerCommandMap because it is genuinely symmetric; this method is not.)
An opt-in field on SessionActiveClient enumerating the section IDs the client wants to handle.
A discovery field on AgentInfo that lets hosts publish the catalogue of transformable sections (and which are policy-restricted), so clients aren't forced to hardcode provider-internal names.
Together, these close the gap so a downstream AHP host (e.g. github/copilot-host) can bridge an AHP active client to the SDK's SystemMessageTransform callback without inventing a side channel.
Motivation
This came up auditing PR github/copilot-host #143, which lists SystemMessageTransform under "What we deliberately did NOT pull in":
SystemMessageTransform / TraceContextProvider: No AHP surface for prompt customization or distributed-trace context yet.
fn section_ids(&self) -> Vec<String> — declares which sections this transform handles.
async fn transform_section(section_id, content, ctx) -> Option<String> — receives the host-rendered content for a section and returns a rewritten version (or None to pass through).
Plumbing on SessionConfig (rust/src/types.rs:1236): the SDK injects action: "transform" overrides into SystemMessageConfig.sections for each ID the transform declares (preserving any override the caller already supplied for that ID), forces mode: "customize", and routes incoming systemMessage.transform JSON-RPC requests to the transform (rust/src/session.rs:2021–2079).
The runtime side (copilot-agent-runtime, the agent the SDK speaks to):
Dispatches a batched JSON-RPC request systemMessage.transform carrying every transformable section's current rendered content: src/core/server.ts:3873.
The runtime fires sectionTransformFnevery time it assembles a system prompt, both at session init (src/core/session.ts:11260) and on every turn inside the build-system-message step (src/core/session.ts:13245). The lifecycle is multi-shot per render, not one-shot at session create.
The provider-internal catalogue of section IDs lives in SystemPromptSection: identity, tone, tool_efficiency, environment_context, code_change_rules, guidelines, safety, tool_instructions, custom_instructions, runtime_instructions, last_instructions. The wire uses string so unknown IDs degrade gracefully.
Use cases (already exercised by SDK consumers):
Append behavioral guardrails to specific sections (e.g. "and never run destructive shell commands without confirmation" on safety).
Strip / redact identifying content from environment_context before it reaches the model.
Find-and-replace pass over tool_instructions to align with house style.
Logging / inspection of rendered prompt material in test harnesses.
None of these is expressible through AHP today.
Today: where the gap lives
Searched at HEAD 0871c60:
types/common/messages.ts::ServerCommandMap has exactly one entry: 'resourceRequest'. No server → client request method exists for "host renders content, asks the client to rewrite it." Comment at L165: "Bidirectional commands (currently only resourceRequest) appear in both CommandMap and ServerCommandMap with identical params/result shapes."
types/channels-session/{commands,actions,state,reducer}.ts — zero references to systemMessage, system_message, prompt-section, transform, or anything in the same neighborhood.
types/channels-session/state.ts::SessionActiveClient (L150–166) carries tools, customizations, and identity — no prompt-shaping surface.
docs/guide/customizations.md exposes PromptCustomization / RuleCustomization as discoverable Open Plugins children, not as slots in the rendered system prompt. The doc itself draws the line: "The protocol intentionally omits host-internal execution details… Those stay on the agent host." These are useful for static prompt content but cannot inspect or rewrite host-rendered sections.
CreateSessionParams.config: Record<string, unknown> (types/channels-session/commands.ts:89) is opaque provider-specific config validated against SessionConfigSchema. A host could carry a static prompt override here as a provider extension, but there is no AHP-level shape for it and no callback semantics regardless.
State-based elicitation (session/inputRequested, docs/guide/elicitation.md) is user-facing — it represents a question for a human and surfaces in SessionStatus.InputNeeded. Not appropriate for an automatic, opaque, model-internal flow that fires on every prompt render.
So the gap is real, the closest existing pattern (resourceRequest) doesn't fit semantically, and there is no work in flight upstream.
Proposed change
1. New server → client request method systemMessageTransform
Add a new entry to ServerCommandMap only — this method is not symmetric, so it does not appear in CommandMap:
// types/channels-session/commands.ts/** A single system-prompt section in the transform request/response. */exportinterfaceSystemMessageTransformSection{/** * Current rendered content of the section, as the host would otherwise * send to the model. Receivers MUST NOT rely on stable formatting * between renders and SHOULD treat the string as opaque. */content: string;}/** Server → client request: ask the active client to rewrite system-prompt sections. */exportinterfaceSystemMessageTransformParamsextendsBaseParams{/** Session URI this transform applies to. */channel: URI;/** * Sections the host would like the client to rewrite, keyed by an * opaque, provider-defined section ID. The host MUST only include IDs * the active client declared in * `SessionActiveClient.systemMessageTransform.sections`. */sections: Record<string,SystemMessageTransformSection>;}/** Client → server response to `systemMessageTransform`. */exportinterfaceSystemMessageTransformResult{/** * Rewritten sections, keyed by the same section IDs as the request. * * - Sections present REPLACE the original content for this render. * - Sections omitted from the response pass through unchanged. * - Receivers MUST NOT introduce section IDs that were not in the * request; hosts MAY ignore any such extras. * * A response whose payload does not deserialize as `SystemMessageTransformResult` * (e.g. missing `sections`, or any section with a non-string `content`) * is treated by the host as an RPC failure — see the fallback rule * in the lifecycle section below. */sections: Record<string,SystemMessageTransformSection>;}
CommandMap is not touched: there is no client → server meaning for this method. (resourceRequest is duplicated across both maps because permission grants are genuinely symmetric; rewriting host-rendered prompt sections is not.)
Naming follows the existing AHP command convention (camelCase, no slash prefix — fetchTurns, completions, resourceRequest, etc.). The method is session-scoped only by virtue of carrying channel: URI pointing at an ahp-session:/... URI in params, matching the convention for other session-scoped commands like fetchTurns.
2. Opt-in field on SessionActiveClient
Add to types/channels-session/state.ts::SessionActiveClient:
exportinterfaceSessionActiveClient{clientId: string;displayName?: string;tools: ToolDefinition[];customizations?: ClientPluginCustomization[];/** * Optional opt-in declaring which system-prompt sections this client * wants to transform. When set with a non-empty `sections` array, the * host MAY invoke `systemMessageTransform` against this client * before assembling the system prompt for the model. * * Section IDs are opaque and provider-defined. Hosts SHOULD publish * the available IDs and any policy restrictions via * `AgentInfo.systemMessageSections`. Section IDs the host does not * recognise SHOULD be silently dropped from the request; the client * just won't be invoked for them. */systemMessageTransform?: SessionActiveClientSystemMessageTransform;}exportinterfaceSessionActiveClientSystemMessageTransform{/** Section IDs this active client wants to transform. */sections: string[];}
Why on the active client (and not on CreateSessionParams directly):
The active client is the role that already mediates tool execution and contributes plugin customizations; system-prompt shaping is in the same trust class.
Multi-shot semantics need a single owner per render; the active-client snapshot at render-start gives the host a deterministic target.
It composes with SessionActiveClientChangedAction: when the active client changes, the host re-reads the opt-in and routes future transforms to the new owner (or to no one, if the new owner has no opt-in).
Initial declaration at session create rides the existing CreateSessionParams.activeClient slot, which already carries SessionActiveClient-shaped data. No new field on CreateSessionParams
is needed.
3. Section discovery on AgentInfo
Add to types/channels-root/state.ts::AgentInfo:
exportinterfaceAgentInfo{// ...existing fields.../** * Section catalogue this agent exposes to `systemMessageTransform`. * Clients SHOULD use this list to populate * `SessionActiveClient.systemMessageTransform.sections` rather than * hardcoding provider-internal IDs. May be omitted by agents that do * not support `systemMessageTransform` at all. */systemMessageSections?: SystemMessageSection[];}exportinterfaceSystemMessageSection{/** Provider-defined opaque ID; appears in transform requests as the key. */id: string;/** Human-readable label for UI surfaces. */label: string;/** Optional explanatory description. */description?: string;/** * When `true`, the host policy refuses transforms for this section * even if the active client opts in. Clients SHOULD treat the ID as * advertise-only (e.g. for UI showing which sections exist) and * SHOULD NOT include it in `SessionActiveClient.systemMessageTransform.sections`. * Hosts MUST still enforce the refusal — a misbehaving client cannot * bypass it by lying about its opt-in. */restricted?: boolean;}
The catalogue is advertise-only; the wire format does not require clients to limit themselves to IDs the host advertised. Hosts SHOULD silently ignore unknown IDs in the opt-in (no-op, no error). This keeps the contract additive across new sections that future provider versions introduce.
4. Negotiation
PROTOCOL_VERSION is currently '0.2.0' (types/version/registry.ts:18). The proposed change is additive — old peers see new optional fields they ignore — but the new wire method needs a feature gate:
Bump PROTOCOL_VERSION to '0.3.0' (MINOR — additive RPC + state field).
types/version/registry.ts today only carries ACTION_INTRODUCED_IN and NOTIFICATION_INTRODUCED_IN; there is no command/server-request version map. Two options for the follow-up PR:
Add a new SERVER_COMMAND_INTRODUCED_IN map (parallel to the action/notification ones) and gate the new method on it. Symmetric with how existing additions are gated.
Skip per-method gating and rely solely on the PROTOCOL_VERSION bump + types/version/message-checks.ts keeping ServerCommandMap and command sources in sync. Simpler; matches today's de facto pattern for server commands (only resourceRequest exists, and it isn't gated by a per-method map).
Maintainer call which they prefer; the proposal is correct either way.
No new capability mechanism in initialize is needed.
Functional degradation across the version line: a 0.3.0 client offering opt-in talking to a <0.3.0 host sees no transforms fire (the host has no idea what the opt-in field means). A <0.3.0 client talking to a 0.3.0 host never declares opt-in, so the host never issues the request. Neither side breaks.
Lifecycle & semantics
The host's algorithm for each system-prompt render is:
Snapshot state.activeClient at the start of the render. If absent, render with no transforms.
Intersect activeClient.systemMessageTransform.sections with the sections the current render actually contains, and filter against the host's policy (drop any restricted: true IDs).
If the resulting set is empty, render with no transforms.
Otherwise issue onesystemMessageTransform request batching every matching section, scoped to the session URI, targeted at the snapshotted active client.
On success: substitute the returned content per section (omitted sections pass through; sections never requested are unchanged).
On RPC failure — including timeout (host SHOULD apply a bounded timeout; ~5 s is what github/copilot-host already uses for client-tool calls), client error, malformed response, or active-client disconnect — every section passes through unchanged. Host SHOULD log at debug.
If the active client changes mid-flight (e.g. it releases or another client takes over): the host MUST NOT redirect the in-flight request to the new client. The original request resolves against the snapshotted target, and the next render uses the new active client's opt-in.
Normative rules to add to the spec:
Hosts MUST NOT issue systemMessageTransform against a client that has not opted in (i.e. SessionActiveClient.systemMessageTransform is absent or sections is empty).
Hosts SHOULD treat a failed / timed-out / disconnected transform as a non-fatal fallback to the un-transformed section. The same fallback applies when the response payload fails to deserialize as SystemMessageTransformResult.
Hosts MUST batch all sections for a given render into a single request.
Hosts MUST limit included sections to (activeClient.systemMessageTransform.sections ∩ {sections in current render} ∩ {ids not marked restricted}).
Hosts MAY ignore opt-in entries for section IDs whose registration with the underlying agent backend can only be configured at session creation. The SDK / runtime baseline today binds the action: "transform" overrides at session.create time; hosts wrapping that baseline cannot expand the set mid-session without a session restart, and the proposal intentionally does not require them to. See the "Deferred" section for the mid-session-mutation story.
Clients MUST NOT introduce section IDs in the response that were not in the request.
Security & policy
Read-side risk. Transforms expose rendered system-prompt section content to the active client. This is model-prompt material, not user-message content. The active client is already trusted to register tools the agent will call (and receives tool.call payloads), so it is already inside the "what the agent does on the user's behalf" trust boundary; adding "what the agent sees as instructions" to that same boundary is consistent. Worth calling out explicitly in docs/specification/session-channel.md.
Write-side risk. A transform can weaken safety-critical sections — safety, identity, tool_instructions, code_change_rules in the Copilot catalogue. Mitigations baked into the wire:
The restricted flag on SystemMessageSection lets hosts publish the no-go list.
The host MUST enforce the refusal, not just publish it.
Hosts SHOULD additionally cap the number of sections per request and the total response size to avoid pathological clients flooding the prompt.
Audit. Hosts SHOULD log every transform invocation (session URI, active clientId, section IDs, before / after content hashes) at debug level. The content itself SHOULD NOT be logged at info or higher because it is model-prompt material.
Privacy on observed state. The opt-in field on SessionActiveClient is just a list of section IDs — no content — so it is safe to broadcast to other session subscribers as normal state. The transform request and response are RPC-only and never enter session state.
Spec & schema deliverables for the follow-up PR
A follow-up PR realising this proposal should land:
types/common/messages.ts — register 'systemMessageTransform' in ServerCommandMap (and not in CommandMap).
types/version/registry.ts — bump PROTOCOL_VERSION to '0.3.0'; per §4 (Negotiation), either add a new SERVER_COMMAND_INTRODUCED_IN map or rely on the global version bump.
types/version/message-checks.ts — ensure the new method is covered by the existing exhaustiveness check.
Regenerated JSON schemas under schema/ (the files carry $comment: "Generated from types/... — do not edit"; the PR should update source types/*.ts and rerun scripts/generate-json-schema.ts, not hand-edit schemas).
docs/specification/session-channel.md — describe the server → client request, the active-client snapshot rule, the timeout/error fallback, and the section-allowlist normative text.
docs/guide/customizations.md — short cross-reference distinguishing this from PromptCustomization (declarative discoverable prompts) so reviewers know the two concepts coexist.
docs/guide/state-model.md — extend the active-client section.
Reducer test cases under types/test-cases/reducers/ covering:
Active client claims with systemMessageTransform.sections set / unset.
Active client released / replaced: opt-in clears with the rest of activeClient.
SessionActiveClientChangedAction echoes the opt-in through unchanged.
Cross-client SDK updates under clients/{rust,kotlin,swift,typescript}:
Server-side handler interface mirroring resourceRequest's server-handler trait (a delegate that receives SystemMessageTransformParams and returns a SystemMessageTransformResult).
Generator updates so the new types regenerate cleanly in all four clients (scripts/generate-{rust,kotlin,swift,typescript}.ts).
Rust regen: clients/rust/crates/ahp-types and the dispatch in clients/rust/crates/ahp.
Alternatives considered
Alternative
Why it doesn't fit
PromptCustomization / RuleCustomization
Static plugin children. They can inject instructions but cannot inspect or rewrite host-rendered section content. Useful for the append-only use case, not for SystemMessageTransform.
CreateSessionParams.config as the carrier
Opaque, provider-specific, and validated against SessionConfigSchema — could plausibly carry a staticreplace / append SDK SystemMessageConfig as a Copilot-specific config field, but has no callback semantics and would not standardise across providers.
Overload resourceRequest with ahp-prompt:/section/... URIs
Hack. resourceRequest's result is empty (it grants access; the caller then issues resourceRead / resourceWrite). Repurposing it for a content-bearing RPC would corrupt the access-permission semantics and confuse implementations.
State-based elicitation (session/inputRequested)
User-facing: pushes the session into SessionStatus.InputNeeded and surfaces a question for a human. SystemMessageTransform is automatic, opaque, and fires on every prompt render — wrong shape.
Read-only SessionState.systemMessage for observation
Too late in the pipeline: observation cannot rewrite anything. Useful as a debug surface but doesn't address the use cases.
One-shot at session create (not multi-shot)
The runtime fires sectionTransformFn on every render (init + every turn — see copilot-agent-runtime/src/core/session.ts:11260, 13245). A one-shot wire would force the host to materialise the union of all possible renders, breaking the dynamic sections the SDK already supports (e.g. runtime_instructions).
Backward compatibility
Purely additive. Old peers see new optional fields they ignore:
A <0.3.0 host receiving a CreateSessionParams.activeClient.systemMessageTransform payload from a newer client treats the unknown field as data, ignores it, and never issues the new RPC. Functional degradation: the model sees the un-transformed prompt — quality regression for slash-style customisation clients, not a wire break.
A <0.3.0 client subscribing to a session on a 0.3.0 host never declares opt-in, never receives the RPC.
The new RPC method itself is gated by the negotiated protocolVersion, so hosts MUST NOT call it against clients whose negotiated version is <0.3.0.
SessionActiveClient does not currently set additionalProperties: false, and AgentInfo likewise tolerates additive fields, so the schema changes are non-breaking against existing implementations.
Mid-session opt-in mutation — if a new active client takes over and wants to opt into sections the original active client did not, the host's underlying SDK / runtime would need to update its SystemMessageConfig.sections mid-session. The SDK has no such update path today; a v1 host can either reject the new opt-in or tear down and recreate the session. Out of scope here.
Per-section sensitivity hints richer than a single restricted flag (e.g. "append-only", "redact regex on response", per-client allow-lists).
Static SDK SystemMessageConfig overrides (mode: "replace" | "append" | "customize"withoutaction: "transform") — could ride the same opt-in shape but is separable and easier to standardise; happy to spin off a sibling proposal if maintainers want to bundle.
References
Parent gap: github/copilot-host PR #143 ("Expose more copilot-sdk surface: AgentMode plumbing + AHP/SDK pin bumps") — see "What we deliberately did NOT pull in".
Parallel proposal from the same PR's deferral list: AHP issue #161 (UserMessage.modelText). Cross-reference for negotiation pattern and v1 scoping style.
AHP precedent for entries in ServerCommandMap: resourceRequest at types/common/commands.ts:544 and types/common/messages.ts:172. Note that resourceRequest is the only entry today and is duplicated in CommandMap because it is symmetric; systemMessageTransform is server → client only and would not be duplicated.
Happy to follow up with a draft PR against this spec / schema / docs / generator set once the design lands; or to iterate further on opt-in shape, negotiation, or normative wording first.
Summary
AHP has no mechanism for the
copilot-sdkSystemMessageTransformfamily: a server-initiated, batched, request/response RPC that hands an active client the rendered content of one or more system-prompt sections and receives rewritten content back, used by the agent host when assembling the model's system prompt. This proposal adds:systemMessageTransform, registered inServerCommandMap. (resourceRequestis the only existing entry there — it lives in bothCommandMapandServerCommandMapbecause it is genuinely symmetric; this method is not.)SessionActiveClientenumerating the section IDs the client wants to handle.AgentInfothat lets hosts publish the catalogue of transformable sections (and which are policy-restricted), so clients aren't forced to hardcode provider-internal names.Together, these close the gap so a downstream AHP host (e.g.
github/copilot-host) can bridge an AHP active client to the SDK'sSystemMessageTransformcallback without inventing a side channel.Motivation
This came up auditing PR
github/copilot-host#143, which listsSystemMessageTransformunder "What we deliberately did NOT pull in":The SDK side is already on
main:copilot-sdk/rust/src/transforms.rsfn section_ids(&self) -> Vec<String>— declares which sections this transform handles.async fn transform_section(section_id, content, ctx) -> Option<String>— receives the host-rendered content for a section and returns a rewritten version (orNoneto pass through).SessionConfig(rust/src/types.rs:1236): the SDK injectsaction: "transform"overrides intoSystemMessageConfig.sectionsfor each ID the transform declares (preserving any override the caller already supplied for that ID), forcesmode: "customize", and routes incomingsystemMessage.transformJSON-RPC requests to the transform (rust/src/session.rs:2021–2079).copilot-agent-runtime, the agent the SDK speaks to):systemMessage.transformcarrying every transformable section's current rendered content:src/core/server.ts:3873.SystemMessageTransformRequest/SystemMessageTransformResponseatsrc/core/protocol/types.ts:179.sectionTransformFnevery time it assembles a system prompt, both at session init (src/core/session.ts:11260) and on every turn inside the build-system-message step (src/core/session.ts:13245). The lifecycle is multi-shot per render, not one-shot at session create.SystemPromptSection:identity,tone,tool_efficiency,environment_context,code_change_rules,guidelines,safety,tool_instructions,custom_instructions,runtime_instructions,last_instructions. The wire usesstringso unknown IDs degrade gracefully.Use cases (already exercised by SDK consumers):
safety).environment_contextbefore it reaches the model.tool_instructionsto align with house style.None of these is expressible through AHP today.
Today: where the gap lives
Searched at HEAD
0871c60:types/common/messages.ts::ServerCommandMaphas exactly one entry:'resourceRequest'. No server → client request method exists for "host renders content, asks the client to rewrite it." Comment at L165: "Bidirectional commands (currently onlyresourceRequest) appear in bothCommandMapandServerCommandMapwith identical params/result shapes."types/channels-session/{commands,actions,state,reducer}.ts— zero references tosystemMessage,system_message,prompt-section,transform, or anything in the same neighborhood.types/channels-session/state.ts::SessionActiveClient(L150–166) carriestools,customizations, and identity — no prompt-shaping surface.docs/guide/customizations.mdexposesPromptCustomization/RuleCustomizationas discoverable Open Plugins children, not as slots in the rendered system prompt. The doc itself draws the line: "The protocol intentionally omits host-internal execution details… Those stay on the agent host." These are useful for static prompt content but cannot inspect or rewrite host-rendered sections.CreateSessionParams.config: Record<string, unknown>(types/channels-session/commands.ts:89) is opaque provider-specific config validated againstSessionConfigSchema. A host could carry a static prompt override here as a provider extension, but there is no AHP-level shape for it and no callback semantics regardless.session/inputRequested,docs/guide/elicitation.md) is user-facing — it represents a question for a human and surfaces inSessionStatus.InputNeeded. Not appropriate for an automatic, opaque, model-internal flow that fires on every prompt render.So the gap is real, the closest existing pattern (
resourceRequest) doesn't fit semantically, and there is no work in flight upstream.Proposed change
1. New server → client request method
systemMessageTransformAdd a new entry to
ServerCommandMaponly — this method is not symmetric, so it does not appear inCommandMap:Register in
types/common/messages.ts:CommandMapis not touched: there is no client → server meaning for this method. (resourceRequestis duplicated across both maps because permission grants are genuinely symmetric; rewriting host-rendered prompt sections is not.)Naming follows the existing AHP command convention (camelCase, no slash prefix —
fetchTurns,completions,resourceRequest, etc.). The method is session-scoped only by virtue of carryingchannel: URIpointing at anahp-session:/...URI inparams, matching the convention for other session-scoped commands likefetchTurns.2. Opt-in field on
SessionActiveClientAdd to
types/channels-session/state.ts::SessionActiveClient:Why on the active client (and not on
CreateSessionParamsdirectly):SessionActiveClientChangedAction: when the active client changes, the host re-reads the opt-in and routes future transforms to the new owner (or to no one, if the new owner has no opt-in).Initial declaration at session create rides the existing
CreateSessionParams.activeClientslot, which already carriesSessionActiveClient-shaped data. No new field onCreateSessionParamsis needed.
3. Section discovery on
AgentInfoAdd to
types/channels-root/state.ts::AgentInfo:The catalogue is advertise-only; the wire format does not require clients to limit themselves to IDs the host advertised. Hosts SHOULD silently ignore unknown IDs in the opt-in (no-op, no error). This keeps the contract additive across new sections that future provider versions introduce.
4. Negotiation
PROTOCOL_VERSIONis currently'0.2.0'(types/version/registry.ts:18). The proposed change is additive — old peers see new optional fields they ignore — but the new wire method needs a feature gate:PROTOCOL_VERSIONto'0.3.0'(MINOR — additive RPC + state field).types/version/registry.tstoday only carriesACTION_INTRODUCED_INandNOTIFICATION_INTRODUCED_IN; there is no command/server-request version map. Two options for the follow-up PR:SERVER_COMMAND_INTRODUCED_INmap (parallel to the action/notification ones) and gate the new method on it. Symmetric with how existing additions are gated.PROTOCOL_VERSIONbump +types/version/message-checks.tskeepingServerCommandMapand command sources in sync. Simpler; matches today's de facto pattern for server commands (onlyresourceRequestexists, and it isn't gated by a per-method map).Maintainer call which they prefer; the proposal is correct either way.
initializeis needed.Functional degradation across the version line: a
0.3.0client offering opt-in talking to a<0.3.0host sees no transforms fire (the host has no idea what the opt-in field means). A<0.3.0client talking to a0.3.0host never declares opt-in, so the host never issues the request. Neither side breaks.Lifecycle & semantics
The host's algorithm for each system-prompt render is:
state.activeClientat the start of the render. If absent, render with no transforms.activeClient.systemMessageTransform.sectionswith the sections the current render actually contains, and filter against the host's policy (drop anyrestricted: trueIDs).systemMessageTransformrequest batching every matching section, scoped to the session URI, targeted at the snapshotted active client.github/copilot-hostalready uses for client-tool calls), client error, malformed response, or active-client disconnect — every section passes through unchanged. Host SHOULD log at debug.Normative rules to add to the spec:
systemMessageTransformagainst a client that has not opted in (i.e.SessionActiveClient.systemMessageTransformis absent orsectionsis empty).SystemMessageTransformResult.activeClient.systemMessageTransform.sections∩{sections in current render}∩{ids not marked restricted}).action: "transform"overrides atsession.createtime; hosts wrapping that baseline cannot expand the set mid-session without a session restart, and the proposal intentionally does not require them to. See the "Deferred" section for the mid-session-mutation story.Security & policy
Read-side risk. Transforms expose rendered system-prompt section content to the active client. This is model-prompt material, not user-message content. The active client is already trusted to register tools the agent will call (and receives
tool.callpayloads), so it is already inside the "what the agent does on the user's behalf" trust boundary; adding "what the agent sees as instructions" to that same boundary is consistent. Worth calling out explicitly indocs/specification/session-channel.md.Write-side risk. A transform can weaken safety-critical sections —
safety,identity,tool_instructions,code_change_rulesin the Copilot catalogue. Mitigations baked into the wire:restrictedflag onSystemMessageSectionlets hosts publish the no-go list.Audit. Hosts SHOULD log every transform invocation (session URI, active
clientId, section IDs, before / after content hashes) at debug level. The content itself SHOULD NOT be logged at info or higher because it is model-prompt material.Privacy on observed state. The opt-in field on
SessionActiveClientis just a list of section IDs — no content — so it is safe to broadcast to other session subscribers as normal state. The transform request and response are RPC-only and never enter session state.Spec & schema deliverables for the follow-up PR
A follow-up PR realising this proposal should land:
types/channels-session/commands.ts—SystemMessageTransformSection,SystemMessageTransformParams,SystemMessageTransformResult.types/channels-session/state.ts—SessionActiveClient.systemMessageTransform,SessionActiveClientSystemMessageTransform.types/channels-root/state.ts—AgentInfo.systemMessageSections,SystemMessageSection.types/common/messages.ts— register'systemMessageTransform'inServerCommandMap(and not inCommandMap).types/version/registry.ts— bumpPROTOCOL_VERSIONto'0.3.0'; per §4 (Negotiation), either add a newSERVER_COMMAND_INTRODUCED_INmap or rely on the global version bump.types/version/message-checks.ts— ensure the new method is covered by the existing exhaustiveness check.schema/(the files carry$comment: "Generated from types/... — do not edit"; the PR should update sourcetypes/*.tsand rerunscripts/generate-json-schema.ts, not hand-edit schemas).docs/specification/session-channel.md— describe the server → client request, the active-client snapshot rule, the timeout/error fallback, and the section-allowlist normative text.docs/guide/customizations.md— short cross-reference distinguishing this fromPromptCustomization(declarative discoverable prompts) so reviewers know the two concepts coexist.docs/guide/state-model.md— extend the active-client section.types/test-cases/reducers/covering:systemMessageTransform.sectionsset / unset.activeClient.SessionActiveClientChangedActionechoes the opt-in through unchanged.clients/{rust,kotlin,swift,typescript}:resourceRequest's server-handler trait (a delegate that receivesSystemMessageTransformParamsand returns aSystemMessageTransformResult).scripts/generate-{rust,kotlin,swift,typescript}.ts).clients/rust/crates/ahp-typesand the dispatch inclients/rust/crates/ahp.Alternatives considered
PromptCustomization/RuleCustomizationSystemMessageTransform.CreateSessionParams.configas the carrierSessionConfigSchema— could plausibly carry a staticreplace/appendSDKSystemMessageConfigas a Copilot-specific config field, but has no callback semantics and would not standardise across providers.resourceRequestwithahp-prompt:/section/...URIsresourceRequest's result is empty (it grants access; the caller then issuesresourceRead/resourceWrite). Repurposing it for a content-bearing RPC would corrupt the access-permission semantics and confuse implementations.session/inputRequested)SessionStatus.InputNeededand surfaces a question for a human. SystemMessageTransform is automatic, opaque, and fires on every prompt render — wrong shape.SessionState.systemMessagefor observationsectionTransformFnon every render (init + every turn — seecopilot-agent-runtime/src/core/session.ts:11260, 13245). A one-shot wire would force the host to materialise the union of all possible renders, breaking the dynamic sections the SDK already supports (e.g.runtime_instructions).Backward compatibility
Purely additive. Old peers see new optional fields they ignore:
<0.3.0host receiving aCreateSessionParams.activeClient.systemMessageTransformpayload from a newer client treats the unknown field as data, ignores it, and never issues the new RPC. Functional degradation: the model sees the un-transformed prompt — quality regression for slash-style customisation clients, not a wire break.<0.3.0client subscribing to a session on a0.3.0host never declares opt-in, never receives the RPC.protocolVersion, so hosts MUST NOT call it against clients whose negotiated version is<0.3.0.SessionActiveClientdoes not currently setadditionalProperties: false, andAgentInfolikewise tolerates additive fields, so the schema changes are non-breaking against existing implementations.Deferred
TraceContextProvider— the cousin from PR Improvements to AHP to support a better UX for managing customizations #143's deferral list; same shape of problem (callback-style augmentation that AHP has no surface for) but a different domain (OTLP / trace propagation). The OTLP channel landed in PR Add ahp-otlp: telemetry channel for OpenTelemetry pass-through #140 — there may now be a natural home for trace-context propagation there, separate from this proposal.SystemMessageConfig.sectionsmid-session. The SDK has no such update path today; a v1 host can either reject the new opt-in or tear down and recreate the session. Out of scope here.restrictedflag (e.g. "append-only", "redact regex on response", per-client allow-lists).SystemMessageConfigoverrides (mode: "replace" | "append" | "customize"withoutaction: "transform") — could ride the same opt-in shape but is separable and easier to standardise; happy to spin off a sibling proposal if maintainers want to bundle.References
github/copilot-hostPR #143 ("Expose more copilot-sdk surface: AgentMode plumbing + AHP/SDK pin bumps") — see "What we deliberately did NOT pull in".UserMessage.modelText). Cross-reference for negotiation pattern and v1 scoping style.copilot-sdk/rust/src/transforms.rs.copilot-sdk/rust/src/session.rs:2021–2079(request handler),rust/src/types.rs:1236(SessionConfig.system_message_transform).copilot-agent-runtime/src/core/server.ts:3873(attachTransformCallback),src/core/session.ts:11260, 13245(per-render invocation).copilot-agent-runtime/src/core/protocol/types.ts:70(SystemPromptSectioncatalogue),:179(SystemMessageTransformRequest/Response).ServerCommandMap:resourceRequestattypes/common/commands.ts:544andtypes/common/messages.ts:172. Note thatresourceRequestis the only entry today and is duplicated inCommandMapbecause it is symmetric;systemMessageTransformis server → client only and would not be duplicated.Happy to follow up with a draft PR against this spec / schema / docs / generator set once the design lands; or to iterate further on opt-in shape, negotiation, or normative wording first.