Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
100ca24
feat(nodejs): add infinite sessions configuration
jmoseley Jan 21, 2026
30b6a6f
feat(python): add infinite sessions configuration
jmoseley Jan 21, 2026
24fe4d7
feat(go): add infinite sessions configuration
jmoseley Jan 21, 2026
d10701b
feat(dotnet): add infinite sessions configuration
jmoseley Jan 21, 2026
8a85bc3
docs: document infinite sessions feature in all SDKs
jmoseley Jan 21, 2026
63e2f1a
docs: fix missing copilot. prefix in Go README example
jmoseley Jan 21, 2026
c3f3366
fix: wrap long line in Python docstring
jmoseley Jan 21, 2026
bfa9b72
chore: remove accidentally committed node_modules, add .gitignore
jmoseley Jan 21, 2026
128a95b
Merge main into jm/infinite
jmoseley Jan 23, 2026
f823071
test(nodejs): add e2e tests for compaction and usage events
jmoseley Jan 23, 2026
21dcf41
test: skip compaction trigger test in CI
jmoseley Jan 23, 2026
8d147eb
fix(dotnet): fix syntax errors from merge conflict resolution
jmoseley Jan 23, 2026
a75c1f4
test: remove usage event tests (not part of this PR)
jmoseley Jan 23, 2026
b6fa81c
test: enable compaction test in CI
jmoseley Jan 23, 2026
c334859
fix(dotnet): change records to internal for JSON serializer
jmoseley Jan 23, 2026
b49dcc6
test: skip compaction trigger test in CI until CLI is released
jmoseley Jan 23, 2026
a318f06
test: revert skip - will update after CLI release
jmoseley Jan 23, 2026
5694a85
test: add compaction e2e tests for all SDK flavors
jmoseley Jan 23, 2026
701e463
Merge origin/main into jm/infinite
jmoseley Jan 24, 2026
e1b198a
fix: use %v format for float64 tokensRemoved in Go test
jmoseley Jan 24, 2026
0910f53
chore: update @github/copilot CLI to 0.0.394
jmoseley Jan 24, 2026
501fe8d
style: format compaction tests for all SDKs
jmoseley Jan 24, 2026
d4da3dc
fix: use dict instead of Dict in Python SDK
jmoseley Jan 24, 2026
e924316
fix: correct SessionEventType import in Python compaction test
jmoseley Jan 24, 2026
b6a2edd
feat(go): add Float64 helper function for pointer values
jmoseley Jan 24, 2026
814a40f
test: skip .NET compaction tests due to Windows CI proxy timing issues
jmoseley Jan 24, 2026
bc261a3
test: remove .NET compaction tests causing Windows CI fixture issues
jmoseley Jan 24, 2026
c02ec51
Revert "test: remove .NET compaction tests causing Windows CI fixture…
jmoseley Jan 24, 2026
1650316
test: enable .NET compaction tests (remove Skip attributes)
jmoseley Jan 24, 2026
0771de0
fix: increase proxy timeout to 30s on Windows
jmoseley Jan 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 43 additions & 1 deletion dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,11 @@ Create a new conversation session.
- `ExcludedTools` - List of tool names to disable
- `Provider` - Custom API provider configuration (BYOK)
- `Streaming` - Enable streaming of response chunks (default: false)
- `InfiniteSessions` - Configure automatic context compaction (see below)

##### `ResumeSessionAsync(string sessionId, ResumeSessionConfig? config = null): Task<CopilotSession>`

Resume an existing session.
Resume an existing session. Returns the session with `WorkspacePath` populated if infinite sessions were enabled.

##### `PingAsync(string? message = null): Task<PingResponse>`

Expand All @@ -127,6 +128,7 @@ Represents a single conversation session.
#### Properties

- `SessionId` - The unique identifier for this session
- `WorkspacePath` - Path to the session workspace directory when infinite sessions are enabled. Contains `checkpoints/`, `plan.md`, and `files/` subdirectories. Null if infinite sessions are disabled.

#### Methods

Expand Down Expand Up @@ -281,6 +283,46 @@ When `Streaming = true`:

Note: `AssistantMessageEvent` and `AssistantReasoningEvent` (final events) are always sent regardless of streaming setting.

## Infinite Sessions

By default, sessions use **infinite sessions** which automatically manage context window limits through background compaction and persist state to a workspace directory.

```csharp
// Default: infinite sessions enabled with default thresholds
var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5"
});

// Access the workspace path for checkpoints and files
Console.WriteLine(session.WorkspacePath);
// => ~/.copilot/session-state/{sessionId}/

// Custom thresholds
var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5",
InfiniteSessions = new InfiniteSessionConfig
{
Enabled = true,
BackgroundCompactionThreshold = 0.80, // Start compacting at 80% context usage
BufferExhaustionThreshold = 0.95 // Block at 95% until compaction completes
}
});

// Disable infinite sessions
var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5",
InfiniteSessions = new InfiniteSessionConfig { Enabled = false }
});
```

When enabled, sessions emit compaction events:

- `SessionCompactionStartEvent` - Background compaction started
- `SessionCompactionCompleteEvent` - Compaction finished (includes token counts)

## Advanced Usage

### Manual Server Control
Expand Down
16 changes: 10 additions & 6 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -344,12 +344,13 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig? config = nul
config?.CustomAgents,
config?.ConfigDir,
config?.SkillDirectories,
config?.DisabledSkills);
config?.DisabledSkills,
config?.InfiniteSessions);

var response = await connection.Rpc.InvokeWithCancellationAsync<CreateSessionResponse>(
"session.create", [request], cancellationToken);

var session = new CopilotSession(response.SessionId, connection.Rpc);
var session = new CopilotSession(response.SessionId, connection.Rpc, response.WorkspacePath);
session.RegisterTools(config?.Tools ?? []);
if (config?.OnPermissionRequest != null)
{
Expand Down Expand Up @@ -406,7 +407,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
var response = await connection.Rpc.InvokeWithCancellationAsync<ResumeSessionResponse>(
"session.resume", [request], cancellationToken);

var session = new CopilotSession(response.SessionId, connection.Rpc);
var session = new CopilotSession(response.SessionId, connection.Rpc, response.WorkspacePath);
session.RegisterTools(config?.Tools ?? []);
if (config?.OnPermissionRequest != null)
{
Expand Down Expand Up @@ -991,7 +992,8 @@ internal record CreateSessionRequest(
List<CustomAgentConfig>? CustomAgents,
string? ConfigDir,
List<string>? SkillDirectories,
List<string>? DisabledSkills);
List<string>? DisabledSkills,
InfiniteSessionConfig? InfiniteSessions);

internal record ToolDefinition(
string Name,
Expand All @@ -1003,7 +1005,8 @@ public static ToolDefinition FromAIFunction(AIFunction function)
}

internal record CreateSessionResponse(
string SessionId);
string SessionId,
string? WorkspacePath);

internal record ResumeSessionRequest(
string SessionId,
Expand All @@ -1017,7 +1020,8 @@ internal record ResumeSessionRequest(
List<string>? DisabledSkills);

internal record ResumeSessionResponse(
string SessionId);
string SessionId,
string? WorkspacePath);

internal record GetLastSessionIdResponse(
string? SessionId);
Expand Down
13 changes: 12 additions & 1 deletion dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,29 @@ public partial class CopilotSession : IAsyncDisposable
/// <value>A string that uniquely identifies this session.</value>
public string SessionId { get; }

/// <summary>
/// Gets the path to the session workspace directory when infinite sessions are enabled.
/// </summary>
/// <value>
/// The path to the workspace containing checkpoints/, plan.md, and files/ subdirectories,
/// or null if infinite sessions are disabled.
/// </value>
public string? WorkspacePath { get; }

/// <summary>
/// Initializes a new instance of the <see cref="CopilotSession"/> class.
/// </summary>
/// <param name="sessionId">The unique identifier for this session.</param>
/// <param name="rpc">The JSON-RPC connection to the Copilot CLI.</param>
/// <param name="workspacePath">The workspace path if infinite sessions are enabled.</param>
/// <remarks>
/// This constructor is internal. Use <see cref="CopilotClient.CreateSessionAsync"/> to create sessions.
/// </remarks>
internal CopilotSession(string sessionId, JsonRpc rpc)
internal CopilotSession(string sessionId, JsonRpc rpc, string? workspacePath = null)
{
SessionId = sessionId;
_rpc = rpc;
WorkspacePath = workspacePath;
}

/// <summary>
Expand Down
36 changes: 36 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,36 @@ public class CustomAgentConfig
public bool? Infer { get; set; }
}

/// <summary>
/// Configuration for infinite sessions with automatic context compaction and workspace persistence.
/// When enabled, sessions automatically manage context window limits through background compaction
/// and persist state to a workspace directory.
/// </summary>
public class InfiniteSessionConfig
{
/// <summary>
/// Whether infinite sessions are enabled. Default: true
/// </summary>
[JsonPropertyName("enabled")]
public bool? Enabled { get; set; }

/// <summary>
/// Context utilization threshold (0.0-1.0) at which background compaction starts.
/// Compaction runs asynchronously, allowing the session to continue processing.
/// Default: 0.80
/// </summary>
[JsonPropertyName("backgroundCompactionThreshold")]
public double? BackgroundCompactionThreshold { get; set; }

/// <summary>
/// Context utilization threshold (0.0-1.0) at which the session blocks until compaction completes.
/// This prevents context overflow when compaction hasn't finished in time.
/// Default: 0.95
/// </summary>
[JsonPropertyName("bufferExhaustionThreshold")]
public double? BufferExhaustionThreshold { get; set; }
}

public class SessionConfig
{
public string? SessionId { get; set; }
Expand Down Expand Up @@ -348,6 +378,12 @@ public class SessionConfig
/// List of skill names to disable.
/// </summary>
public List<string>? DisabledSkills { get; set; }

/// <summary>
/// Infinite session configuration for persistent workspaces and automatic compaction.
/// When enabled (default), sessions automatically manage context limits and persist state.
/// </summary>
public InfiniteSessionConfig? InfiniteSessions { get; set; }
}

public class ResumeSessionConfig
Expand Down
110 changes: 110 additions & 0 deletions dotnet/test/CompactionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

using System.Runtime.InteropServices;
using GitHub.Copilot.SDK.Test.Harness;
using Xunit;
using Xunit.Abstractions;

namespace GitHub.Copilot.SDK.Test;

public class CompactionTests(E2ETestFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, "compaction", output)
{
[Fact]
public async Task Should_Trigger_Compaction_With_Low_Threshold_And_Emit_Events()
{
// Create session with very low compaction thresholds to trigger compaction quickly
var session = await Client.CreateSessionAsync(new SessionConfig
{
InfiniteSessions = new InfiniteSessionConfig
{
Enabled = true,
// Trigger background compaction at 0.5% context usage (~1000 tokens)
BackgroundCompactionThreshold = 0.005,
// Block at 1% to ensure compaction runs
BufferExhaustionThreshold = 0.01
}
});

var compactionStartEvents = new List<SessionCompactionStartEvent>();
var compactionCompleteEvents = new List<SessionCompactionCompleteEvent>();

session.On(evt =>
{
if (evt is SessionCompactionStartEvent startEvt)
{
compactionStartEvents.Add(startEvt);
}
if (evt is SessionCompactionCompleteEvent completeEvt)
{
compactionCompleteEvents.Add(completeEvt);
}
});

// Send multiple messages to fill up the context window
await session.SendAndWaitAsync(new MessageOptions
{
Prompt = "Tell me a long story about a dragon. Be very detailed."
});
await session.SendAndWaitAsync(new MessageOptions
{
Prompt = "Continue the story with more details about the dragon's castle."
});
await session.SendAndWaitAsync(new MessageOptions
{
Prompt = "Now describe the dragon's treasure in great detail."
});

// Should have triggered compaction at least once
Assert.True(compactionStartEvents.Count >= 1, "Expected at least 1 compaction_start event");
Assert.True(compactionCompleteEvents.Count >= 1, "Expected at least 1 compaction_complete event");

// Compaction should have succeeded
var lastComplete = compactionCompleteEvents[^1];
Assert.True(lastComplete.Data.Success, "Expected compaction to succeed");

// Should have removed some tokens
if (lastComplete.Data.TokensRemoved.HasValue)
{
Assert.True(lastComplete.Data.TokensRemoved > 0, "Expected tokensRemoved > 0");
}

// Verify the session still works after compaction
var answer = await session.SendAndWaitAsync(new MessageOptions
{
Prompt = "What was the story about?"
});
Assert.NotNull(answer);
Assert.NotNull(answer!.Data.Content);
// Should remember it was about a dragon (context preserved via summary)
Assert.Contains("dragon", answer.Data.Content.ToLower());
}

[Fact]
public async Task Should_Not_Emit_Compaction_Events_When_Infinite_Sessions_Disabled()
{
var session = await Client.CreateSessionAsync(new SessionConfig
{
InfiniteSessions = new InfiniteSessionConfig
{
Enabled = false
}
});

var compactionEvents = new List<SessionEvent>();

session.On(evt =>
{
if (evt is SessionCompactionStartEvent or SessionCompactionCompleteEvent)
{
compactionEvents.Add(evt);
}
});

await session.SendAndWaitAsync(new MessageOptions { Prompt = "What is 2+2?" });

// Should not have any compaction events when disabled
Assert.Empty(compactionEvents);
}
}
4 changes: 3 additions & 1 deletion dotnet/test/Harness/CapiProxy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,9 @@ async Task<string> StartCoreAsync()
}
});

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
// Use longer timeout on Windows due to slower process startup
var timeoutSeconds = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? 30 : 10;
Copy link
Author

Choose a reason for hiding this comment

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

The windows tests were flaking. Bumping up this timeout seems to stabilize things, but I'd love a second set of eyes @SteveSandersonMS

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(timeoutSeconds));
cts.Token.Register(() => tcs.TrySetException(new TimeoutException("Timeout waiting for proxy")));

return await tcs.Task;
Expand Down
38 changes: 38 additions & 0 deletions go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,44 @@ When `Streaming: true`:

Note: `assistant.message` and `assistant.reasoning` (final events) are always sent regardless of streaming setting.

## Infinite Sessions

By default, sessions use **infinite sessions** which automatically manage context window limits through background compaction and persist state to a workspace directory.

```go
// Default: infinite sessions enabled with default thresholds
session, _ := client.CreateSession(&copilot.SessionConfig{
Model: "gpt-5",
})

// Access the workspace path for checkpoints and files
fmt.Println(session.WorkspacePath())
// => ~/.copilot/session-state/{sessionId}/

// Custom thresholds
session, _ := client.CreateSession(&copilot.SessionConfig{
Model: "gpt-5",
InfiniteSessions: &copilot.InfiniteSessionConfig{
Enabled: copilot.Bool(true),
BackgroundCompactionThreshold: copilot.Float64(0.80), // Start compacting at 80% context usage
BufferExhaustionThreshold: copilot.Float64(0.95), // Block at 95% until compaction completes

Choose a reason for hiding this comment

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

Inconsistency Issue: The copilot.Float64() helper function used here doesn't exist in the Go SDK.

The codebase has a copilot.Bool() helper in go/types.go:42 for creating bool pointers, but there's no equivalent Float64() helper for creating float64 pointers.

Recommended fix: Add a Float64 helper function to go/types.go:

// Float64 returns a pointer to the given float64 value.
// Use for setting threshold values: BackgroundCompactionThreshold: Float64(0.80)
func Float64(v float64) *float64 {
	return &v
}

Alternatively, update the README examples to show manual pointer creation without the helper function.

AI generated by SDK Consistency Review Agent for #76

},
})

// Disable infinite sessions
session, _ := client.CreateSession(&copilot.SessionConfig{
Model: "gpt-5",
InfiniteSessions: &copilot.InfiniteSessionConfig{
Enabled: copilot.Bool(false),
},
})
```

When enabled, sessions emit compaction events:

- `session.compaction_start` - Background compaction started
- `session.compaction_complete` - Compaction finished (includes token counts)

## Transport Modes

### stdio (Default)
Expand Down
Loading
Loading