Skip to content

Commit 86df7e5

Browse files
MorabbinCopilot
andauthored
SDK: add optional memory configuration to session create and resume (#1617)
* Add optional memory configuration to session create and resume Add a `MemoryConfiguration` type and a `memory` option on `SessionConfig` and `ResumeSessionConfig`, with `with_memory` builders. The configuration serializes as an optional `memory` object on the `session.create` / `session.resume` requests and is omitted when unset, so the runtime applies its own default for the memory feature. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Trim memory runtime-default note to the type doc Keep the runtime-default note only on the MemoryConfiguration type comment; drop it from the field and builder docs. In the README, drop the runtime-default note (no sibling feature section mentions runtime defaults), fix the trailing colon so the code block reads as a usage example, and remove the extensibility aside. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Drop usage example from with_memory doc comment No other SessionConfig / ResumeSessionConfig builder method carries a usage-example doctest; keep with_memory consistent with its siblings. The README retains a usage example. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Remove Default derive from MemoryConfiguration MemoryConfiguration exposes explicit enabled()/disabled() constructors, so a Default impl is redundant. SessionConfig and ResumeSessionConfig hold memory as Option<MemoryConfiguration>, which defaults to None without requiring MemoryConfiguration: Default. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add memory configuration API to Go, Node, Python, .NET, and Java SDKs Mirror the Rust session memory surface across the remaining SDK languages: session create and resume accept an optional memory configuration carrying a required `enabled` flag, omitted from the wire when unset. Adds the type, the create/resume wiring, clone and serialization coverage, and README docs per language. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Default session memory to disabled in empty client mode Apply the empty-mode SDK default for the `memory` session option across all language SDKs: in `empty` client mode `memory` defaults to disabled unless the app supplies a value, while in the default `copilot-cli` mode `memory` is left unset so the runtime applies its own default. Caller-supplied configuration always wins. Adds unit tests (Rust, Go, Node, Python) and a README note per language. Also break a module-level cyclic import in the Python client by importing `MemoryConfiguration` under `TYPE_CHECKING` (it is only referenced in annotations, which are lazy via `from __future__ import annotations`). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Test: avoid deprecated buildCreateRequest signature in java tests * Fix python cyclic imports for CodeQL * Address review comments on SDK PR 1617 - Fix Python payload keys for ModelCapabilitiesOverride serialization to match wire format (camelCase instead of snake_case) - Update Rust README example to include SessionConfig import * Re-trigger CI for flaky Windows model-switch e2e The Node.js SDK Tests (windows-latest) leg failed on an unrelated, flaky end-to-end test (test/e2e/rpc_session_state.e2e.test.ts, model switchTo: expected claude-sonnet-4.5, received gpt-4.1). This PR only changes the memory configuration RPC surface and does not touch model switching or session-state code. The same base commit (Update @github/copilot to 1.0.62) passes this leg on main. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Tighten _memory_default typing to MemoryConfiguration Type _memory_default's supplied parameter and return value as MemoryConfiguration | None instead of Any, matching the type specificity of the other mode-default helpers and recovering the benefit of the MemoryConfiguration TypedDict. The type is imported under TYPE_CHECKING (with the module's existing 'from __future__ import annotations') so no runtime import edge is added between _mode and session. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8e2d6f8 commit 86df7e5

37 files changed

Lines changed: 1064 additions & 74 deletions

dotnet/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,20 @@ When enabled, sessions emit compaction events:
410410
- `SessionCompactionStartEvent` - Background compaction started
411411
- `SessionCompactionCompleteEvent` - Compaction finished (includes token counts)
412412

413+
## Memory
414+
415+
Sessions can opt into persistent memory, allowing the agent to read and write memory across turns. Memory is configured per session and applies to both `CreateSessionAsync` and `ResumeSessionAsync`.
416+
417+
```csharp
418+
var session = await client.CreateSessionAsync(new SessionConfig
419+
{
420+
Model = "gpt-5",
421+
Memory = new MemoryConfiguration { Enabled = true }
422+
});
423+
```
424+
425+
When `Memory` is left unset, no memory configuration is sent and the runtime default applies. In the default `CopilotClientMode.CopilotCli` the SDK leaves `Memory` unset so the runtime applies its own default, while `CopilotClientMode.Empty` defaults `Memory` to disabled unless you set it explicitly.
426+
413427
## Advanced Usage
414428

415429
### Manual Server Control

dotnet/src/Client.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,7 @@ private void ApplyConfigDefaultsForMode(SessionConfigBase config)
672672
config.EnableHostGitOperations ??= false;
673673
config.EnableSessionStore ??= false;
674674
config.EnableSkills ??= false;
675+
config.Memory ??= new MemoryConfiguration { Enabled = false };
675676
config.McpOAuthTokenStorage ??= McpOAuthTokenStorageMode.InMemory;
676677
}
677678
}
@@ -935,6 +936,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
935936
InstructionDirectories: config.InstructionDirectories,
936937
PluginDirectories: config.PluginDirectories,
937938
LargeOutput: config.LargeOutput,
939+
Memory: config.Memory,
938940
Canvases: config.Canvases,
939941
RequestCanvasRenderer: config.RequestCanvasRenderer,
940942
RequestExtensions: config.RequestExtensions,
@@ -1131,6 +1133,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
11311133
InstructionDirectories: config.InstructionDirectories,
11321134
PluginDirectories: config.PluginDirectories,
11331135
LargeOutput: config.LargeOutput,
1136+
Memory: config.Memory,
11341137
Canvases: config.Canvases,
11351138
RequestCanvasRenderer: config.RequestCanvasRenderer,
11361139
RequestExtensions: config.RequestExtensions,
@@ -2322,6 +2325,7 @@ internal record CreateSessionRequest(
23222325
IList<string>? InstructionDirectories = null,
23232326
IList<string>? PluginDirectories = null,
23242327
LargeToolOutputConfig? LargeOutput = null,
2328+
MemoryConfiguration? Memory = null,
23252329
#pragma warning disable GHCP001
23262330
IList<CanvasDeclaration>? Canvases = null,
23272331
bool? RequestCanvasRenderer = null,
@@ -2412,6 +2416,7 @@ internal record ResumeSessionRequest(
24122416
IList<string>? InstructionDirectories = null,
24132417
IList<string>? PluginDirectories = null,
24142418
LargeToolOutputConfig? LargeOutput = null,
2419+
MemoryConfiguration? Memory = null,
24152420
#pragma warning disable GHCP001
24162421
IList<CanvasDeclaration>? Canvases = null,
24172422
bool? RequestCanvasRenderer = null,

dotnet/src/Types.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2364,6 +2364,18 @@ public sealed class LargeToolOutputConfig
23642364
public string? OutputDirectory { get; set; }
23652365
}
23662366

2367+
/// <summary>
2368+
/// Configuration for session memory.
2369+
/// </summary>
2370+
public sealed class MemoryConfiguration
2371+
{
2372+
/// <summary>
2373+
/// Whether memory is enabled for the session.
2374+
/// </summary>
2375+
[JsonPropertyName("enabled")]
2376+
public bool Enabled { get; set; }
2377+
}
2378+
23672379
/// <summary>
23682380
/// GitHub repository metadata to associate with a cloud session.
23692381
/// </summary>
@@ -2458,6 +2470,7 @@ protected SessionConfigBase(SessionConfigBase? other)
24582470
Hooks = other.Hooks;
24592471
InfiniteSessions = other.InfiniteSessions;
24602472
LargeOutput = other.LargeOutput;
2473+
Memory = other.Memory;
24612474
McpServers = other.McpServers is not null
24622475
? (other.McpServers is Dictionary<string, McpServerConfig> dict
24632476
? new Dictionary<string, McpServerConfig>(dict, dict.Comparer)
@@ -2805,6 +2818,12 @@ protected SessionConfigBase(SessionConfigBase? other)
28052818
/// </summary>
28062819
public LargeToolOutputConfig? LargeOutput { get; set; }
28072820

2821+
/// <summary>
2822+
/// Configuration for session memory. When set, controls whether the
2823+
/// session can read and write persistent memory.
2824+
/// </summary>
2825+
public MemoryConfiguration? Memory { get; set; }
2826+
28082827
/// <summary>
28092828
/// Optional event handler registered on the session before the session.create / session.resume
28102829
/// RPC is issued, ensuring early events are delivered.

dotnet/test/Unit/CloneTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
9797
DisabledSkills = ["skill1"],
9898
PluginDirectories = ["/plugins"],
9999
LargeOutput = new LargeToolOutputConfig { Enabled = true, MaxSizeBytes = 2048, OutputDirectory = "/tmp/out" },
100+
Memory = new MemoryConfiguration { Enabled = true },
100101
OnExitPlanModeRequest = static (_, _) => Task.FromResult(new ExitPlanModeResult()),
101102
OnAutoModeSwitchRequest = static (_, _) => Task.FromResult(AutoModeSwitchResponse.No),
102103
};
@@ -129,6 +130,7 @@ public void SessionConfig_Clone_CopiesAllProperties()
129130
Assert.Equal(original.DisabledSkills, clone.DisabledSkills);
130131
Assert.Equal(original.PluginDirectories, clone.PluginDirectories);
131132
Assert.Same(original.LargeOutput, clone.LargeOutput);
133+
Assert.Same(original.Memory, clone.Memory);
132134
Assert.Same(original.OnExitPlanModeRequest, clone.OnExitPlanModeRequest);
133135
Assert.Same(original.OnAutoModeSwitchRequest, clone.OnAutoModeSwitchRequest);
134136
}
@@ -402,12 +404,14 @@ public void ResumeSessionConfig_Clone_CopiesPluginDirectoriesAndLargeOutput()
402404
{
403405
PluginDirectories = ["/resume/plugins"],
404406
LargeOutput = largeOutput,
407+
Memory = new MemoryConfiguration { Enabled = true },
405408
};
406409

407410
var clone = original.Clone();
408411

409412
Assert.Equal(original.PluginDirectories, clone.PluginDirectories);
410413
Assert.Same(original.LargeOutput, clone.LargeOutput);
414+
Assert.Same(original.Memory, clone.Memory);
411415
}
412416

413417
[Fact]

dotnet/test/Unit/SerializationTests.cs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,58 @@ public void SessionRequests_CanSerializePluginDirectoriesAndLargeOutput_WithSdkO
267267
Assert.Equal("/tmp/large-output", resumeLargeOutput.GetProperty("outputDir").GetString());
268268
}
269269

270+
[Fact]
271+
public void SessionRequests_CanSerializeMemory_WithSdkOptions()
272+
{
273+
var options = GetSerializerOptions();
274+
275+
var createRequestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest");
276+
var createRequest = CreateInternalRequest(
277+
createRequestType,
278+
("SessionId", "session-id"),
279+
("Memory", new MemoryConfiguration { Enabled = true }));
280+
281+
var createJson = JsonSerializer.Serialize(createRequest, createRequestType, options);
282+
using var createDocument = JsonDocument.Parse(createJson);
283+
var createRoot = createDocument.RootElement;
284+
Assert.True(createRoot.GetProperty("memory").GetProperty("enabled").GetBoolean());
285+
286+
var resumeRequestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest");
287+
var resumeRequest = CreateInternalRequest(
288+
resumeRequestType,
289+
("SessionId", "session-id"),
290+
("Memory", new MemoryConfiguration { Enabled = false }));
291+
292+
var resumeJson = JsonSerializer.Serialize(resumeRequest, resumeRequestType, options);
293+
using var resumeDocument = JsonDocument.Parse(resumeJson);
294+
var resumeRoot = resumeDocument.RootElement;
295+
Assert.False(resumeRoot.GetProperty("memory").GetProperty("enabled").GetBoolean());
296+
}
297+
298+
[Fact]
299+
public void SessionRequests_OmitMemory_WhenUnset()
300+
{
301+
var options = GetSerializerOptions();
302+
303+
var createRequestType = GetNestedType(typeof(CopilotClient), "CreateSessionRequest");
304+
var createRequest = CreateInternalRequest(
305+
createRequestType,
306+
("SessionId", "session-id"));
307+
308+
var createJson = JsonSerializer.Serialize(createRequest, createRequestType, options);
309+
using var createDocument = JsonDocument.Parse(createJson);
310+
Assert.False(createDocument.RootElement.TryGetProperty("memory", out _));
311+
312+
var resumeRequestType = GetNestedType(typeof(CopilotClient), "ResumeSessionRequest");
313+
var resumeRequest = CreateInternalRequest(
314+
resumeRequestType,
315+
("SessionId", "session-id"));
316+
317+
var resumeJson = JsonSerializer.Serialize(resumeRequest, resumeRequestType, options);
318+
using var resumeDocument = JsonDocument.Parse(resumeJson);
319+
Assert.False(resumeDocument.RootElement.TryGetProperty("memory", out _));
320+
}
321+
270322
[Fact]
271323
public void CreateSessionRequest_CanSerializeEnableSessionTelemetry_WithSdkOptions()
272324
{

go/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,32 @@ When enabled, sessions emit compaction events:
497497
- `session.compaction_start` - Background compaction started
498498
- `session.compaction_complete` - Compaction finished (includes token counts)
499499

500+
## Memory
501+
502+
Sessions can opt in to the memory feature, which lets the agent persist and recall
503+
information across turns. Provide a `MemoryConfiguration` on session create or resume;
504+
when omitted, the runtime default applies. In the default `ModeCopilotCli` client mode the
505+
SDK leaves `Memory` unset so the runtime applies its own default, while `ModeEmpty`
506+
defaults `Memory` to disabled unless you set it explicitly.
507+
508+
```go
509+
// Enable memory for a session
510+
session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
511+
Model: "gpt-5",
512+
Memory: &copilot.MemoryConfiguration{
513+
Enabled: true,
514+
},
515+
})
516+
517+
// Disable memory for a session
518+
session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
519+
Model: "gpt-5",
520+
Memory: &copilot.MemoryConfiguration{
521+
Enabled: false,
522+
},
523+
})
524+
```
525+
500526
## Custom Providers
501527

502528
The SDK supports custom OpenAI-compatible API providers (BYOK - Bring Your Own Key), including local providers like Ollama. When using a custom provider, you must specify the `Model` explicitly.

go/client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,6 +647,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
647647
req.DisabledSkills = config.DisabledSkills
648648
req.InfiniteSessions = config.InfiniteSessions
649649
req.LargeOutput = config.LargeOutput
650+
req.Memory = config.Memory
650651
req.GitHubToken = config.GitHubToken
651652
req.RemoteSession = config.RemoteSession
652653
req.Cloud = config.Cloud
@@ -983,6 +984,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
983984
req.DisabledSkills = config.DisabledSkills
984985
req.InfiniteSessions = config.InfiniteSessions
985986
req.LargeOutput = config.LargeOutput
987+
req.Memory = config.Memory
986988
req.GitHubToken = config.GitHubToken
987989
req.RemoteSession = config.RemoteSession
988990
req.Canvases = config.Canvases

go/client_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,6 +605,70 @@ func TestSessionRequests_PluginDirectoriesAndLargeOutput(t *testing.T) {
605605
})
606606
}
607607

608+
func TestSessionRequests_Memory(t *testing.T) {
609+
t.Run("create includes memory in JSON when enabled", func(t *testing.T) {
610+
req := createSessionRequest{Memory: &MemoryConfiguration{Enabled: true}}
611+
data, err := json.Marshal(req)
612+
if err != nil {
613+
t.Fatalf("Failed to marshal: %v", err)
614+
}
615+
var m map[string]any
616+
if err := json.Unmarshal(data, &m); err != nil {
617+
t.Fatalf("Failed to unmarshal: %v", err)
618+
}
619+
expected := map[string]any{"enabled": true}
620+
if !reflect.DeepEqual(m["memory"], expected) {
621+
t.Errorf("Expected memory %v, got %v", expected, m["memory"])
622+
}
623+
})
624+
625+
t.Run("resume includes memory in JSON when disabled", func(t *testing.T) {
626+
req := resumeSessionRequest{SessionID: "s1", Memory: &MemoryConfiguration{Enabled: false}}
627+
data, err := json.Marshal(req)
628+
if err != nil {
629+
t.Fatalf("Failed to marshal: %v", err)
630+
}
631+
var m map[string]any
632+
if err := json.Unmarshal(data, &m); err != nil {
633+
t.Fatalf("Failed to unmarshal: %v", err)
634+
}
635+
expected := map[string]any{"enabled": false}
636+
if !reflect.DeepEqual(m["memory"], expected) {
637+
t.Errorf("Expected memory %v, got %v", expected, m["memory"])
638+
}
639+
})
640+
641+
t.Run("create omits memory when nil", func(t *testing.T) {
642+
req := createSessionRequest{}
643+
data, err := json.Marshal(req)
644+
if err != nil {
645+
t.Fatalf("Failed to marshal: %v", err)
646+
}
647+
var m map[string]any
648+
if err := json.Unmarshal(data, &m); err != nil {
649+
t.Fatalf("Failed to unmarshal: %v", err)
650+
}
651+
if _, ok := m["memory"]; ok {
652+
t.Errorf("Expected memory to be omitted")
653+
}
654+
})
655+
656+
t.Run("resume omits memory when nil", func(t *testing.T) {
657+
req := resumeSessionRequest{SessionID: "s1"}
658+
data, err := json.Marshal(req)
659+
if err != nil {
660+
t.Fatalf("Failed to marshal: %v", err)
661+
}
662+
var m map[string]any
663+
if err := json.Unmarshal(data, &m); err != nil {
664+
t.Fatalf("Failed to unmarshal: %v", err)
665+
}
666+
if _, ok := m["memory"]; ok {
667+
t.Errorf("Expected memory to be omitted")
668+
}
669+
})
670+
}
671+
608672
func TestCreateSessionRequest_Agent(t *testing.T) {
609673
t.Run("includes agent in JSON when set", func(t *testing.T) {
610674
req := createSessionRequest{Agent: "test-agent"}

go/mode_empty.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,9 @@ func (c *Client) applyConfigDefaultsForMode(config *SessionConfig) {
154154
f := false
155155
config.EnableSkills = &f
156156
}
157+
if config.Memory == nil {
158+
config.Memory = &MemoryConfiguration{Enabled: false}
159+
}
157160
if config.MCPOAuthTokenStorage == "" {
158161
config.MCPOAuthTokenStorage = "in-memory"
159162
}
@@ -195,6 +198,9 @@ func (c *Client) applyResumeDefaultsForMode(config *ResumeSessionConfig) {
195198
f := false
196199
config.EnableSkills = &f
197200
}
201+
if config.Memory == nil {
202+
config.Memory = &MemoryConfiguration{Enabled: false}
203+
}
198204
if config.MCPOAuthTokenStorage == "" {
199205
config.MCPOAuthTokenStorage = "in-memory"
200206
}

go/toolset_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,9 @@ func TestApplyConfigDefaultsForMode_emptyDefaultsGranularFlags(t *testing.T) {
273273
if cfg.EnableSkills == nil || *cfg.EnableSkills != false {
274274
t.Errorf("expected EnableSkills=false in empty mode, got %v", cfg.EnableSkills)
275275
}
276+
if cfg.Memory == nil || cfg.Memory.Enabled != false {
277+
t.Errorf("expected Memory.Enabled=false in empty mode, got %v", cfg.Memory)
278+
}
276279
}
277280

278281
func TestApplyConfigDefaultsForMode_emptyHonorsCallerGranularFlags(t *testing.T) {
@@ -287,6 +290,7 @@ func TestApplyConfigDefaultsForMode_emptyHonorsCallerGranularFlags(t *testing.T)
287290
EnableHostGitOperations: &trueVal,
288291
EnableSessionStore: &trueVal,
289292
EnableSkills: &trueVal,
293+
Memory: &MemoryConfiguration{Enabled: true},
290294
}
291295
c.applyConfigDefaultsForMode(cfg)
292296
if *cfg.SkipEmbeddingRetrieval != false {
@@ -310,6 +314,9 @@ func TestApplyConfigDefaultsForMode_emptyHonorsCallerGranularFlags(t *testing.T)
310314
if *cfg.EnableSkills != true {
311315
t.Errorf("caller-supplied EnableSkills must win")
312316
}
317+
if cfg.Memory == nil || cfg.Memory.Enabled != true {
318+
t.Errorf("caller-supplied Memory must win")
319+
}
313320
}
314321

315322
func TestApplyConfigDefaultsForMode_copilotCliLeavesGranularFlagsNil(t *testing.T) {
@@ -334,6 +341,9 @@ func TestApplyConfigDefaultsForMode_copilotCliLeavesGranularFlagsNil(t *testing.
334341
if cfg.EnableSkills != nil {
335342
t.Errorf("non-empty mode must not default EnableSkills")
336343
}
344+
if cfg.Memory != nil {
345+
t.Errorf("non-empty mode must not default Memory")
346+
}
337347
}
338348

339349
func TestApplyConfigDefaultsForMode_emptyDefaultsMCPOAuthTokenStorage(t *testing.T) {

0 commit comments

Comments
 (0)