Expose exp_assignments injection on session create/resume across all SDKs#1750
Expose exp_assignments injection on session create/resume across all SDKs#1750ellismg wants to merge 4 commits into
Conversation
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>
|
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>
…signments # Conflicts: # nodejs/test/client.test.ts
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
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
expAssignmentsto 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
| [EditorBrowsable(EditorBrowsableState.Never)] | ||
| public JsonElement? ExpAssignments { get; set; } |
There was a problem hiding this comment.
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/ResumeSessionConfigderived 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()andResumeSessionConfig.clone()both assignexpAssignments. - Rust — already safe: both configs
#[derive(Clone)]with no hand-writtenCloneimpl, so all fields (incl.exp_assignments) are copied. - Node — already safe:
createSession/resumeSessionnormalize via{ ...configDefaultsForMode(), ...config }, and object spread carries all own properties includingexpAssignments. - Go — already safe: create/resume assign
req.ExpAssignments = config.ExpAssignmentsdirectly off the config pointer (no struct-copy in between). - Python — already safe:
exp_assignmentsis an explicit kwarg mapped straight intopayload["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>
Cross-SDK Consistency Review ✅Reviewed the CoverageAll six SDKs implement the feature on both
Consistency Observations
No cross-SDK consistency issues found. The implementation achieves feature parity across all six SDKs as described in the PR.
|
What
Adds an internal/trusted-integrator option to inject ExP ("flight") assignment data into session create and resume, forwarded to the wire key
expAssignmentsand omitted entirely when unset. The payload is opaque JSON in the exact shape ofCopilotExpAssignmentResponse(matching the runtime contract and each SDK's already-generatedSessionOpenOptions.expAssignmentsfield — no typed schema struct introduced).Implemented consistently across all six SDKs. Per-language surface:
#[doc(hidden)] pub fn with_exp_assignments(serde_json::Value) -> Self+#[doc(hidden)] pub exp_assignments: Option<serde_json::Value>onSessionConfig/ResumeSessionConfig; forwarded through the hand-writtenSessionCreateWire/SessionResumeWire(skip_serializing_if = "Option::is_none").@internal expAssignments?: Record<string, unknown>onSessionConfigBase; forwarded in the inlinesession.create/session.resumepayloads inclient.ts.exp_assignments: dict[str, Any] | None = Nonekwarg oncreate_session/resume_session; mapped topayload["expAssignments"]when set.ExpAssignments any(documentedInternal:) onSessionConfig/ResumeSessionConfig; forwarded into the create/resume wire structs withjson:"expAssignments,omitempty".[EditorBrowsable(Never)] JsonElement? ExpAssignmentsonSessionConfigBase; wired through the internalCreateSessionRequest/ResumeSessionRequestrecords ([JsonPropertyName("expAssignments")], null auto-omitted).JsonNode expAssignmentsfield + fluentsetExpAssignments/getExpAssignmentsonSessionConfig/ResumeSessionConfig; mapped throughCreateSessionRequest/ResumeSessionRequestinSessionRequestBuilder(@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
expAssignmentsthrough the sameFeatureFlagService.setExpAssignments()path as CLI-fetched flights, stampsexp_assignment_contexton telemetry, sets the CAPI headerX-Copilot-Exp-Assignment-Context, and is non-blocking / fail-open when absent. Injection is supported on both create and resume.Each SDK's generated
SessionOpenOptionsalready 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, GoInternal: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
expAssignmentsis emitted on both paths and omitted when unset.cargo +nightly-2026-04-14 fmt --checkclean;cargo clippy --all-features --all-targets -- -D warningsclean;cargo test --all-features --lib→ 166 passed.npm run typecheckclean; new unit tests pass.ruff check/ruff format --checkclean.gofmtclean;go build ./...+go test ./...pass.SessionRequestBuilderTesttests pass (incl. 2 new);mvn spotless:applyapplied.Also verified the github-app consumer compiles against the synced Rust shape (
cargo check -p copilot-taurigreen);with_exp_assignmentsis 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.