Skip to content

Expose exp_assignments injection on session create/resume across all SDKs#1750

Open
ellismg wants to merge 4 commits into
mainfrom
ellismg-expose-exp-assignments
Open

Expose exp_assignments injection on session create/resume across all SDKs#1750
ellismg wants to merge 4 commits into
mainfrom
ellismg-expose-exp-assignments

Conversation

@ellismg

@ellismg ellismg commented Jun 22, 2026

Copy link
Copy Markdown

What

Adds an internal/trusted-integrator option to inject ExP ("flight") assignment data into session create and resume, forwarded to the wire key expAssignments and omitted entirely when unset. The payload is opaque JSON in the exact shape of CopilotExpAssignmentResponse (matching the runtime contract and each SDK's already-generated SessionOpenOptions.expAssignments field — no typed schema struct introduced).

Implemented consistently across all six SDKs. Per-language surface:

  • Rust#[doc(hidden)] pub fn with_exp_assignments(serde_json::Value) -> Self + #[doc(hidden)] pub exp_assignments: Option<serde_json::Value> on SessionConfig / ResumeSessionConfig; forwarded through the hand-written SessionCreateWire / SessionResumeWire (skip_serializing_if = "Option::is_none").
  • Node@internal expAssignments?: Record<string, unknown> on SessionConfigBase; forwarded in the inline session.create / session.resume payloads in client.ts.
  • Pythonexp_assignments: dict[str, Any] | None = None kwarg on create_session / resume_session; mapped to payload["expAssignments"] when set.
  • GoExpAssignments any (documented Internal:) on SessionConfig / ResumeSessionConfig; forwarded into the create/resume wire structs with json:"expAssignments,omitempty".
  • .NET[EditorBrowsable(Never)] JsonElement? ExpAssignments on SessionConfigBase; wired through the internal CreateSessionRequest / ResumeSessionRequest records ([JsonPropertyName("expAssignments")], null auto-omitted).
  • JavaJsonNode expAssignments field + fluent setExpAssignments / getExpAssignments on SessionConfig / ResumeSessionConfig; mapped through CreateSessionRequest / ResumeSessionRequest in SessionRequestBuilder (@JsonProperty("expAssignments"), @JsonInclude(NON_NULL)).

Why

Lets out-of-process integrators — the GitHub Copilot desktop app — inject ExP flight data over JSON-RPC, mirroring the runtime contract added in github/copilot-agent-runtime#9955. The runtime already feeds supplied expAssignments through the same FeatureFlagService.setExpAssignments() path as CLI-fetched flights, stamps exp_assignment_context on telemetry, sets the CAPI header X-Copilot-Exp-Assignment-Context, and is non-blocking / fail-open when absent. Injection is supported on both create and resume.

Each SDK's generated SessionOpenOptions already reflects the field, but the create/resume paths serialize hand-written config + wire structs, so an out-of-process consumer had no way to send it. The desktop app is the first out-of-process consumer (starting with the Rust SDK); in-process callers already had the capability. Adding it to every SDK keeps the surface consistent per review feedback. Part of the github/github-app ExP onboarding epic #7452.

Internal posture

The field/builder are marked internal in each language's idiom (Rust #[doc(hidden)], Node @internal, Go Internal: doc, .NET [EditorBrowsable(Never)], Java doc note; Python documents it as a trusted-integrator option) to mirror upstream's .asInternal() posture — a trusted-integrator API, not broadly advertised public surface.

Validation

All six SDKs add create + resume serialization tests asserting expAssignments is emitted on both paths and omitted when unset.

  • Rustcargo +nightly-2026-04-14 fmt --check clean; cargo clippy --all-features --all-targets -- -D warnings clean; cargo test --all-features --lib → 166 passed.
  • Nodenpm run typecheck clean; new unit tests pass.
  • Python — new tests pass; ruff check / ruff format --check clean.
  • Gogofmt clean; go build ./... + go test ./... pass.
  • .NET — 37 serialization tests pass (incl. 2 new).
  • Java — 82 SessionRequestBuilderTest tests pass (incl. 2 new); mvn spotless:apply applied.

Also verified the github-app consumer compiles against the synced Rust shape (cargo check -p copilot-tauri green); with_exp_assignments is reachable from the inject call site.

Notes

Opening as a draft pending the paired github-app consumer work. The vendored sync into github-app will follow via the auto-sync agent after merge.

cc @criemen — he asked to be kept in the loop on this.

Expose ExP assignment ("flight") data on the SDK's session-open and
session-resume paths so an out-of-process integrator can inject the same
CopilotExpAssignmentResponse payload the CLI fetches itself. The runtime
already accepts expAssignments on the wire, but the hand-written
SessionCreateWire / SessionResumeWire structs (and their public configs)
did not carry it.

- SessionConfig / ResumeSessionConfig: add doc-hidden exp_assignments
  field (serde_json::Value) plus a doc-hidden with_exp_assignments builder
- SessionCreateWire / SessionResumeWire: add exp_assignments, serialized
  as camelCase expAssignments and omitted when None
- Forward the field through both into_wire paths
- Unit tests asserting expAssignments is emitted on create and resume and
  omitted when unset

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

Copy link
Copy Markdown
Collaborator

Even if the main known consumer at present is the GH Copilot App, I'd like to see us add whatever we need here consistently across all 6 languages. I'd like to avoid having to keep track of differences in exposed functionality by language. And presumably other languages will end up needing this, too.

…SDKs

Mirror the Rust SDK change in the remaining five SDKs so out-of-process
integrators can inject ExP ("flight") assignment data into session create
and resume. Adds an internal/trusted-integrator config field that forwards
to the wire key `expAssignments` (omitted when unset), in the opaque JSON
shape of `CopilotExpAssignmentResponse`:

- Node: `expAssignments?: Record<string, unknown>` on `SessionConfigBase`
  (`@internal`), forwarded in the inline session.create/session.resume
  payloads in client.ts.
- Python: `exp_assignments: dict[str, Any] | None = None` kwarg on
  `create_session`/`resume_session`, mapped to `payload["expAssignments"]`.
- Go: `ExpAssignments any` on `SessionConfig`/`ResumeSessionConfig`
  (documented Internal:), forwarded into the create/resume wire structs
  with `json:"expAssignments,omitempty"`.
- .NET: `JsonElement? ExpAssignments` on `SessionConfigBase`
  (`[EditorBrowsable(Never)]`), wired through the internal
  CreateSessionRequest/ResumeSessionRequest records.
- Java: `JsonNode expAssignments` field + fluent setter/getter on
  SessionConfig/ResumeSessionConfig, mapped through
  CreateSessionRequest/ResumeSessionRequest in SessionRequestBuilder.

Each language gains create+resume serialization tests asserting the field
serializes to `expAssignments` when set and is omitted when unset.

Part of github/github-app epic #7452; mirrors the runtime contract added
in github/copilot-agent-runtime#9955.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@ellismg ellismg changed the title Expose exp_assignments injection on SessionConfig / ResumeSessionConfig Expose exp_assignments injection on session create/resume across all SDKs Jun 22, 2026
…signments

# Conflicts:
#	nodejs/test/client.test.ts
@github-actions

This comment has been minimized.

@ellismg ellismg marked this pull request as ready for review June 23, 2026 16:24
Copilot AI review requested due to automatic review settings June 23, 2026 16:24
@ellismg ellismg requested a review from a team as a code owner June 23, 2026 16:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds a trusted-integrator/internal option to inject ExP (“flight”) assignment payloads into session.create and session.resume requests, forwarding to the wire key expAssignments and omitting it when unset, implemented consistently across all SDKs.

Changes:

  • Adds expAssignments to create/resume wire payload construction in Rust/Node/Python/Go/.NET/Java.
  • Introduces internal-facing config surfaces (builder/field/kwarg/property) for supplying opaque ExP assignment JSON.
  • Adds/updates serialization tests in each language to assert presence when set and omission when unset.
Show a summary per file
File Description
rust/src/wire.rs Adds exp_assignments to SessionCreateWire/SessionResumeWire with skip_serializing_if.
rust/src/types.rs Adds internal config fields + builder methods and serialization tests for expAssignments.
python/copilot/client.py Adds exp_assignments kwargs to create_session/resume_session and maps to payload["expAssignments"].
python/test_client.py Adds tests asserting expAssignments forwarding and omission in create/resume calls.
nodejs/src/types.ts Adds @internal expAssignments?: Record<string, unknown> to SessionConfigBase.
nodejs/src/client.ts Forwards config.expAssignments into session.create/session.resume request payloads.
nodejs/test/client.test.ts Adds tests asserting forwarding and unset behavior for expAssignments.
go/types.go Adds ExpAssignments any to session configs and wires it into request structs (json:"expAssignments,omitempty").
go/client.go Copies config.ExpAssignments into create/resume request payloads.
go/client_test.go Adds JSON marshal tests for expAssignments presence/omission on create/resume wire structs.
dotnet/src/Types.cs Adds [EditorBrowsable(Never)] JsonElement? ExpAssignments to SessionConfigBase.
dotnet/src/Client.cs Wires ExpAssignments through internal create/resume request records with JsonPropertyName("expAssignments").
dotnet/test/Unit/SerializationTests.cs Adds tests asserting expAssignments is serialized when set and omitted when unset.
java/src/main/java/com/github/copilot/SessionRequestBuilder.java Propagates expAssignments from configs into create/resume request objects.
java/src/main/java/com/github/copilot/rpc/SessionConfig.java Adds JsonNode expAssignments with getter/setter and clones it.
java/src/main/java/com/github/copilot/rpc/ResumeSessionConfig.java Adds JsonNode expAssignments setter and clones it.
java/src/main/java/com/github/copilot/rpc/CreateSessionRequest.java Adds @JsonProperty("expAssignments") JsonNode expAssignments to create request.
java/src/main/java/com/github/copilot/rpc/ResumeSessionRequest.java Adds @JsonProperty("expAssignments") JsonNode expAssignments to resume request.
java/src/test/java/com/github/copilot/SessionRequestBuilderTest.java Adds tests for propagation + JSON omission when expAssignments is null.

Copilot's findings

  • Files reviewed: 19/19 changed files
  • Comments generated: 1

Comment thread dotnet/src/Types.cs
Comment on lines +3060 to +3061
[EditorBrowsable(EditorBrowsableState.Never)]
public JsonElement? ExpAssignments { get; set; }

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch — fixed in e3cbdaa. SessionConfigBase's copy constructor now copies ExpAssignments (right after RemoteSession), so both SessionConfig.Clone() and ResumeSessionConfig.Clone() preserve it.

I audited every config copy/clone/normalize path across all 6 SDKs, since the original tests only checked serialization from a freshly-built config and never exercised a copy path:

  • .NET — had the gap (this fix). Grepped the other copy sites (SessionConfig/ResumeSessionConfig derived copy ctors only forward their own fields and chain : base(other)), so the base ctor was the one place to fix.
  • Java — already safe: SessionConfig.clone() and ResumeSessionConfig.clone() both assign expAssignments.
  • Rust — already safe: both configs #[derive(Clone)] with no hand-written Clone impl, so all fields (incl. exp_assignments) are copied.
  • Node — already safe: createSession/resumeSession normalize via { ...configDefaultsForMode(), ...config }, and object spread carries all own properties including expAssignments.
  • Go — already safe: create/resume assign req.ExpAssignments = config.ExpAssignments directly off the config pointer (no struct-copy in between).
  • Python — already safe: exp_assignments is an explicit kwarg mapped straight into payload["expAssignments"]; no intermediate dict that could drop it.

Regression coverage added for the three SDKs with explicit Clone()/clone() methods — each sets expAssignments, clones, and asserts it survives and is forwarded on the resulting create/resume request:

  • .NET: SessionConfigClone_PreservesExpAssignments, ResumeSessionConfigClone_PreservesExpAssignments — these fail against the pre-fix copy constructor (verified by reverting the one-line fix: both red, then green again).
  • Java: testClonePreservesAndForwardsExpAssignments.
  • Rust: session_config_clone_preserves_exp_assignments, resume_session_config_clone_preserves_exp_assignments.
  • Node's existing forward test already drives the spread-normalization copy path via createSession.

Re: the two CodeQL findings on this file — they flagged my new tests calling the deprecated single-arg buildCreateRequest(SessionConfig). Switched both to the current buildCreateRequest(SessionConfig, String) overload, so no new CodeQL debt. (The deprecated single-arg form is a pre-existing pattern used ~30 times elsewhere in this test file; I only touched the lines I added.)

Full 6-language validation green post-fix: Rust fmt/clippy + exp tests, Node typecheck + tests, Python tests + ruff, Go gofmt/build/tests, .NET 42 serialization tests, Java 83 SessionRequestBuilderTest + spotless.

The SessionConfigBase copy constructor (used by SessionConfig.Clone() and
ResumeSessionConfig.Clone()) did not copy the newly added ExpAssignments
property, so cloning a config silently dropped it and it would not be
forwarded on create/resume. Copy it alongside the other base properties.

Cross-language audit of every config copy/clone path:
- .NET: had the gap (fixed here).
- Java: SessionConfig/ResumeSessionConfig clone() already copy expAssignments (safe).
- Rust: SessionConfig/ResumeSessionConfig derive Clone, no manual impl (safe).
- Node: createSession/resumeSession normalize via { ...defaults, ...config }
  spread, which preserves all own properties (safe).
- Go: create/resume requests assign req.ExpAssignments = config.ExpAssignments
  directly from the config pointer (safe).
- Python: exp_assignments is an explicit kwarg mapped straight into the
  payload, no intermediate dict copy (safe).

Add clone regression tests for .NET, Java, and Rust (the three with explicit
Clone()/clone() methods) that set expAssignments, clone, and assert it
survives and is forwarded on the resulting create/resume request. The .NET
tests fail against the pre-fix copy constructor. Node's existing forward test
already exercises the spread-normalization copy path.

Switch the two new Java expAssignments tests off the deprecated
buildCreateRequest(SessionConfig) overload to the current
buildCreateRequest(SessionConfig, String) form to avoid new CodeQL debt.

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

Copy link
Copy Markdown
Contributor

Cross-SDK Consistency Review ✅

Reviewed the expAssignments feature across all six SDKs. The implementation is consistent and well-coordinated.

Coverage

All six SDKs implement the feature on both session.create and session.resume paths:

SDK Config field/param Wire key Omit when absent Tests
TypeScript expAssignments?: Record<string, unknown> on SessionConfigBase expAssignments ✅ (undefined stripped by JSON.stringify)
Python exp_assignments: dict[str, Any] | None = None param expAssignments ✅ (explicit if ... is not None guard)
Go ExpAssignments any on SessionConfig/ResumeSessionConfig expAssignments ✅ (omitempty)
.NET JsonElement? ExpAssignments on SessionConfigBase expAssignments ✅ (serializer null-ignore config)
Java JsonNode expAssignments on SessionConfig/ResumeSessionConfig expAssignments ✅ (class-level @JsonInclude(NON_NULL))
Rust Option<Value> on SessionConfig/ResumeSessionConfig expAssignments ✅ (skip_serializing_if = "Option::is_none")

Consistency Observations

  • Wire key: All SDKs use expAssignments consistently ✅
  • Naming conventions: Each SDK follows its language idiom (expAssignments, exp_assignments, ExpAssignments) ✅
  • Null omission: All SDKs correctly omit the field from the wire payload when unset ✅
  • "Internal" marking: Each SDK uses the appropriate language-level mechanism (@internal, [EditorBrowsable(Never)], #[doc(hidden)], // Internal: doc, doc note) — the Java and Python approaches rely on documentation rather than compiler/IDE enforcement, which is expected given language limitations ✅
  • Clone/copy handling: All SDKs with config objects that support cloning (SessionConfig.clone(), ResumeSessionConfig.clone(), .NET copy constructor) properly propagate expAssignments
  • Test coverage: Every SDK adds serialization tests asserting the field is emitted when set and omitted when unset, for both create and resume paths ✅

No cross-SDK consistency issues found. The implementation achieves feature parity across all six SDKs as described in the PR.

Generated by SDK Consistency Review Agent for issue #1750 · sonnet46 1.5M ·

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.

4 participants