diff --git a/dotnet/README.md b/dotnet/README.md index a3d4076b0..6fa69d6fc 100644 --- a/dotnet/README.md +++ b/dotnet/README.md @@ -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` -Resume an existing session. +Resume an existing session. Returns the session with `WorkspacePath` populated if infinite sessions were enabled. ##### `PingAsync(string? message = null): Task` @@ -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 @@ -256,6 +258,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 diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index 912ab93e4..c160a8353 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -344,12 +344,13 @@ public async Task CreateSessionAsync(SessionConfig? config = nul config?.CustomAgents, config?.ConfigDir, config?.SkillDirectories, - config?.DisabledSkills); + config?.DisabledSkills, + config?.InfiniteSessions); var response = await connection.Rpc.InvokeWithCancellationAsync( "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) { @@ -406,7 +407,7 @@ public async Task ResumeSessionAsync(string sessionId, ResumeSes var response = await connection.Rpc.InvokeWithCancellationAsync( "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) { @@ -991,7 +992,8 @@ internal record CreateSessionRequest( List? CustomAgents, string? ConfigDir, List? SkillDirectories, - List? DisabledSkills); + List? DisabledSkills, + InfiniteSessionConfig? InfiniteSessions); internal record ToolDefinition( string Name, @@ -1002,7 +1004,9 @@ public static ToolDefinition FromAIFunction(AIFunction function) => new ToolDefinition(function.Name, function.Description, function.JsonSchema); } - internal record CreateSessionResponse( + private record CreateSessionResponse( + string SessionId, + string? WorkspacePath); string SessionId); internal record ResumeSessionRequest( @@ -1016,7 +1020,9 @@ internal record ResumeSessionRequest( List? SkillDirectories, List? DisabledSkills); - internal record ResumeSessionResponse( + private record ResumeSessionResponse( + string SessionId, + string? WorkspacePath); string SessionId); internal record GetLastSessionIdResponse( diff --git a/dotnet/src/Session.cs b/dotnet/src/Session.cs index e554e034a..f1e47df83 100644 --- a/dotnet/src/Session.cs +++ b/dotnet/src/Session.cs @@ -55,18 +55,29 @@ public partial class CopilotSession : IAsyncDisposable /// A string that uniquely identifies this session. public string SessionId { get; } + /// + /// Gets the path to the session workspace directory when infinite sessions are enabled. + /// + /// + /// The path to the workspace containing checkpoints/, plan.md, and files/ subdirectories, + /// or null if infinite sessions are disabled. + /// + public string? WorkspacePath { get; } + /// /// Initializes a new instance of the class. /// /// The unique identifier for this session. /// The JSON-RPC connection to the Copilot CLI. + /// The workspace path if infinite sessions are enabled. /// /// This constructor is internal. Use to create sessions. /// - internal CopilotSession(string sessionId, JsonRpc rpc) + internal CopilotSession(string sessionId, JsonRpc rpc, string? workspacePath = null) { SessionId = sessionId; _rpc = rpc; + WorkspacePath = workspacePath; } /// diff --git a/dotnet/src/Types.cs b/dotnet/src/Types.cs index 6abbb42bb..24b4fc2e7 100644 --- a/dotnet/src/Types.cs +++ b/dotnet/src/Types.cs @@ -298,6 +298,36 @@ public class CustomAgentConfig public bool? Infer { get; set; } } +/// +/// 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. +/// +public class InfiniteSessionConfig +{ + /// + /// Whether infinite sessions are enabled. Default: true + /// + [JsonPropertyName("enabled")] + public bool? Enabled { get; set; } + + /// + /// Context utilization threshold (0.0-1.0) at which background compaction starts. + /// Compaction runs asynchronously, allowing the session to continue processing. + /// Default: 0.80 + /// + [JsonPropertyName("backgroundCompactionThreshold")] + public double? BackgroundCompactionThreshold { get; set; } + + /// + /// 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 + /// + [JsonPropertyName("bufferExhaustionThreshold")] + public double? BufferExhaustionThreshold { get; set; } +} + public class SessionConfig { public string? SessionId { get; set; } @@ -348,6 +378,12 @@ public class SessionConfig /// List of skill names to disable. /// public List? DisabledSkills { get; set; } + + /// + /// Infinite session configuration for persistent workspaces and automatic compaction. + /// When enabled (default), sessions automatically manage context limits and persist state. + /// + public InfiniteSessionConfig? InfiniteSessions { get; set; } } public class ResumeSessionConfig diff --git a/go/README.md b/go/README.md index 1a1c0f876..6860ac508 100644 --- a/go/README.md +++ b/go/README.md @@ -263,6 +263,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 + }, +}) + +// 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) diff --git a/go/client.go b/go/client.go index fe62d9f76..ada263de6 100644 --- a/go/client.go +++ b/go/client.go @@ -544,6 +544,20 @@ func (c *Client) CreateSession(config *SessionConfig) (*Session, error) { if len(config.DisabledSkills) > 0 { params["disabledSkills"] = config.DisabledSkills } + // Add infinite sessions configuration + if config.InfiniteSessions != nil { + infiniteSessions := make(map[string]interface{}) + if config.InfiniteSessions.Enabled != nil { + infiniteSessions["enabled"] = *config.InfiniteSessions.Enabled + } + if config.InfiniteSessions.BackgroundCompactionThreshold != nil { + infiniteSessions["backgroundCompactionThreshold"] = *config.InfiniteSessions.BackgroundCompactionThreshold + } + if config.InfiniteSessions.BufferExhaustionThreshold != nil { + infiniteSessions["bufferExhaustionThreshold"] = *config.InfiniteSessions.BufferExhaustionThreshold + } + params["infiniteSessions"] = infiniteSessions + } } result, err := c.client.Request("session.create", params) @@ -556,7 +570,9 @@ func (c *Client) CreateSession(config *SessionConfig) (*Session, error) { return nil, fmt.Errorf("invalid response: missing sessionId") } - session := NewSession(sessionID, c.client) + workspacePath, _ := result["workspacePath"].(string) + + session := NewSession(sessionID, c.client, workspacePath) if config != nil { session.registerTools(config.Tools) @@ -692,7 +708,9 @@ func (c *Client) ResumeSessionWithOptions(sessionID string, config *ResumeSessio return nil, fmt.Errorf("invalid response: missing sessionId") } - session := NewSession(resumedSessionID, c.client) + workspacePath, _ := result["workspacePath"].(string) + + session := NewSession(resumedSessionID, c.client, workspacePath) if config != nil { session.registerTools(config.Tools) if config.OnPermissionRequest != nil { diff --git a/go/session.go b/go/session.go index ddafb96e2..d6b0b23a9 100644 --- a/go/session.go +++ b/go/session.go @@ -47,6 +47,7 @@ type sessionHandler struct { type Session struct { // SessionID is the unique identifier for this session. SessionID string + workspacePath string client *JSONRPCClient handlers []sessionHandler nextHandlerID uint64 @@ -57,16 +58,24 @@ type Session struct { permissionMux sync.RWMutex } +// WorkspacePath returns the path to the session workspace directory when infinite +// sessions are enabled. Contains checkpoints/, plan.md, and files/ subdirectories. +// Returns empty string if infinite sessions are disabled. +func (s *Session) WorkspacePath() string { + return s.workspacePath +} + // NewSession creates a new session wrapper with the given session ID and client. // // Note: This function is primarily for internal use. Use [Client.CreateSession] // to create sessions with proper initialization. -func NewSession(sessionID string, client *JSONRPCClient) *Session { +func NewSession(sessionID string, client *JSONRPCClient, workspacePath string) *Session { return &Session{ - SessionID: sessionID, - client: client, - handlers: make([]sessionHandler, 0), - toolHandlers: make(map[string]ToolHandler), + SessionID: sessionID, + workspacePath: workspacePath, + client: client, + handlers: make([]sessionHandler, 0), + toolHandlers: make(map[string]ToolHandler), } } diff --git a/go/types.go b/go/types.go index 2fde2920a..3f3d82219 100644 --- a/go/types.go +++ b/go/types.go @@ -132,6 +132,20 @@ type CustomAgentConfig struct { Infer *bool `json:"infer,omitempty"` } +// InfiniteSessionConfig configures 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. +type InfiniteSessionConfig struct { + // Enabled controls whether infinite sessions are enabled (default: true) + Enabled *bool + // BackgroundCompactionThreshold is the context utilization (0.0-1.0) at which + // background compaction starts. Default: 0.80 + BackgroundCompactionThreshold *float64 + // BufferExhaustionThreshold is the context utilization (0.0-1.0) at which + // the session blocks until compaction completes. Default: 0.95 + BufferExhaustionThreshold *float64 +} + // SessionConfig configures a new session type SessionConfig struct { // SessionID is an optional custom session ID @@ -167,6 +181,9 @@ type SessionConfig struct { SkillDirectories []string // DisabledSkills is a list of skill names to disable DisabledSkills []string + // InfiniteSessions configures infinite sessions for persistent workspaces and automatic compaction. + // When enabled (default), sessions automatically manage context limits and persist state. + InfiniteSessions *InfiniteSessionConfig } // Tool describes a caller-implemented tool that can be invoked by Copilot diff --git a/nodejs/README.md b/nodejs/README.md index dea3b3ea5..fec8f84dd 100644 --- a/nodejs/README.md +++ b/nodejs/README.md @@ -89,10 +89,11 @@ Create a new conversation session. - `model?: string` - Model to use ("gpt-5", "claude-sonnet-4.5", etc.) - `tools?: Tool[]` - Custom tools exposed to the CLI - `systemMessage?: SystemMessageConfig` - System message customization (see below) +- `infiniteSessions?: InfiniteSessionConfig` - Configure automatic context compaction (see below) ##### `resumeSession(sessionId: string, config?: ResumeSessionConfig): Promise` -Resume an existing session. +Resume an existing session. Returns the session with `workspacePath` populated if infinite sessions were enabled. ##### `ping(message?: string): Promise<{ message: string; timestamp: number }>` @@ -116,6 +117,16 @@ Delete a session and its data from disk. Represents a single conversation session. +#### Properties + +##### `sessionId: string` + +The unique identifier for this session. + +##### `workspacePath?: string` + +Path to the session workspace directory when infinite sessions are enabled. Contains `checkpoints/`, `plan.md`, and `files/` subdirectories. Undefined if infinite sessions are disabled. + #### Methods ##### `send(options: MessageOptions): Promise` @@ -305,6 +316,40 @@ const session = await client.createSession({ }); ``` +### Infinite Sessions + +By default, sessions use **infinite sessions** which automatically manage context window limits through background compaction and persist state to a workspace directory. + +```typescript +// Default: infinite sessions enabled with default thresholds +const session = await client.createSession({ model: "gpt-5" }); + +// Access the workspace path for checkpoints and files +console.log(session.workspacePath); +// => ~/.copilot/session-state/{sessionId}/ + +// Custom thresholds +const session = await client.createSession({ + model: "gpt-5", + infiniteSessions: { + enabled: true, + backgroundCompactionThreshold: 0.80, // Start compacting at 80% context usage + bufferExhaustionThreshold: 0.95, // Block at 95% until compaction completes + }, +}); + +// Disable infinite sessions +const session = await client.createSession({ + model: "gpt-5", + infiniteSessions: { enabled: false }, +}); +``` + +When enabled, sessions emit compaction events: + +- `session.compaction_start` - Background compaction started +- `session.compaction_complete` - Compaction finished (includes token counts) + ### Multiple Sessions ```typescript diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 93e16d37d..a698383a5 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -452,10 +452,14 @@ export class CopilotClient { configDir: config.configDir, skillDirectories: config.skillDirectories, disabledSkills: config.disabledSkills, + infiniteSessions: config.infiniteSessions, }); - const sessionId = (response as { sessionId: string }).sessionId; - const session = new CopilotSession(sessionId, this.connection!); + const { sessionId, workspacePath } = response as { + sessionId: string; + workspacePath?: string; + }; + const session = new CopilotSession(sessionId, this.connection!, workspacePath); session.registerTools(config.tools); if (config.onPermissionRequest) { session.registerPermissionHandler(config.onPermissionRequest); @@ -516,8 +520,11 @@ export class CopilotClient { disabledSkills: config.disabledSkills, }); - const resumedSessionId = (response as { sessionId: string }).sessionId; - const session = new CopilotSession(resumedSessionId, this.connection!); + const { sessionId: resumedSessionId, workspacePath } = response as { + sessionId: string; + workspacePath?: string; + }; + const session = new CopilotSession(resumedSessionId, this.connection!, workspacePath); session.registerTools(config.tools); if (config.onPermissionRequest) { session.registerPermissionHandler(config.onPermissionRequest); diff --git a/nodejs/src/index.ts b/nodejs/src/index.ts index cfbd13b13..014a9b437 100644 --- a/nodejs/src/index.ts +++ b/nodejs/src/index.ts @@ -17,6 +17,7 @@ export type { CustomAgentConfig, GetAuthStatusResponse, GetStatusResponse, + InfiniteSessionConfig, MCPLocalServerConfig, MCPRemoteServerConfig, MCPServerConfig, diff --git a/nodejs/src/session.ts b/nodejs/src/session.ts index ca9789c62..e285e7ca1 100644 --- a/nodejs/src/session.ts +++ b/nodejs/src/session.ts @@ -57,13 +57,24 @@ export class CopilotSession { * * @param sessionId - The unique identifier for this session * @param connection - The JSON-RPC message connection to the Copilot CLI + * @param workspacePath - Path to the session workspace directory (when infinite sessions enabled) * @internal This constructor is internal. Use {@link CopilotClient.createSession} to create sessions. */ constructor( public readonly sessionId: string, - private connection: MessageConnection + private connection: MessageConnection, + private readonly _workspacePath?: string ) {} + /** + * Path to the session workspace directory when infinite sessions are enabled. + * Contains checkpoints/, plan.md, and files/ subdirectories. + * Undefined if infinite sessions are disabled. + */ + get workspacePath(): string | undefined { + return this._workspacePath; + } + /** * Sends a message to this session and waits for the response. * diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index bcc247a24..406fe8d5a 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -312,6 +312,33 @@ export interface CustomAgentConfig { infer?: boolean; } +/** + * 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. + */ +export interface InfiniteSessionConfig { + /** + * Whether infinite sessions are enabled. + * @default true + */ + enabled?: boolean; + + /** + * Context utilization threshold (0.0-1.0) at which background compaction starts. + * Compaction runs asynchronously, allowing the session to continue processing. + * @default 0.80 + */ + backgroundCompactionThreshold?: number; + + /** + * 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 + */ + bufferExhaustionThreshold?: number; +} + export interface SessionConfig { /** * Optional custom session ID @@ -394,6 +421,13 @@ export interface SessionConfig { * List of skill names to disable. */ disabledSkills?: string[]; + + /** + * Infinite session configuration for persistent workspaces and automatic compaction. + * When enabled (default), sessions automatically manage context limits and persist state. + * Set to `{ enabled: false }` to disable. + */ + infiniteSessions?: InfiniteSessionConfig; } /** diff --git a/nodejs/test/e2e/compaction.test.ts b/nodejs/test/e2e/compaction.test.ts new file mode 100644 index 000000000..168aea20d --- /dev/null +++ b/nodejs/test/e2e/compaction.test.ts @@ -0,0 +1,137 @@ +import { describe, expect, it } from "vitest"; +import { SessionEvent } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +describe("Compaction", async () => { + const { copilotClient: client } = await createSdkTestContext(); + + it( + "should trigger compaction with low threshold and emit events", + async () => { + // Create session with very low compaction thresholds to trigger compaction quickly + const session = await client.createSession({ + infiniteSessions: { + 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, + }, + }); + + const events: SessionEvent[] = []; + session.on((event) => { + events.push(event); + }); + + // Send multiple messages to fill up the context window + // With such low thresholds, even a few messages should trigger compaction + await session.sendAndWait({ + prompt: "Tell me a long story about a dragon. Be very detailed.", + }); + await session.sendAndWait({ + prompt: "Continue the story with more details about the dragon's castle.", + }); + await session.sendAndWait({ + prompt: "Now describe the dragon's treasure in great detail.", + }); + + // Check for compaction events + const compactionStartEvents = events.filter( + (e) => e.type === "session.compaction_start" + ); + const compactionCompleteEvents = events.filter( + (e) => e.type === "session.compaction_complete" + ); + + // Should have triggered compaction at least once + expect(compactionStartEvents.length).toBeGreaterThanOrEqual(1); + expect(compactionCompleteEvents.length).toBeGreaterThanOrEqual(1); + + // Compaction should have succeeded + const lastCompactionComplete = + compactionCompleteEvents[compactionCompleteEvents.length - 1]; + expect(lastCompactionComplete.data.success).toBe(true); + + // Should have removed some tokens + if (lastCompactionComplete.data.tokensRemoved !== undefined) { + expect(lastCompactionComplete.data.tokensRemoved).toBeGreaterThan(0); + } + + // Verify the session still works after compaction + const answer = await session.sendAndWait({ prompt: "What was the story about?" }); + expect(answer?.data.content).toBeDefined(); + // Should remember it was about a dragon (context preserved via summary) + expect(answer?.data.content?.toLowerCase()).toContain("dragon"); + }, + 120000 + ); + + it("should emit usage info events showing context window state", async () => { + const session = await client.createSession({ + infiniteSessions: { + enabled: true, + }, + }); + + const usageEvents: SessionEvent[] = []; + session.on((event) => { + if (event.type === "session.usage_info") { + usageEvents.push(event); + } + }); + + await session.sendAndWait({ prompt: "What is 2+2?" }); + + // Should have received at least one usage info event + expect(usageEvents.length).toBeGreaterThanOrEqual(1); + + const lastUsageEvent = usageEvents[usageEvents.length - 1]; + expect(lastUsageEvent.data.tokenLimit).toBeGreaterThan(0); + expect(lastUsageEvent.data.currentTokens).toBeGreaterThan(0); + expect(lastUsageEvent.data.messagesLength).toBeGreaterThan(0); + }); + + it("should receive assistant.usage events with token counts", async () => { + const session = await client.createSession(); + + const usageEvents: SessionEvent[] = []; + session.on((event) => { + if (event.type === "assistant.usage") { + usageEvents.push(event); + } + }); + + await session.sendAndWait({ prompt: "What is the capital of France?" }); + + // Should have received at least one assistant usage event + expect(usageEvents.length).toBeGreaterThanOrEqual(1); + + const lastUsageEvent = usageEvents[usageEvents.length - 1]; + // Token counts may be 0 when replaying from snapshot, but model should be defined + expect(lastUsageEvent.data.model).toBeDefined(); + }); + + it("should not emit compaction events when infinite sessions disabled", async () => { + const session = await client.createSession({ + infiniteSessions: { + enabled: false, + }, + }); + + const compactionEvents: SessionEvent[] = []; + session.on((event) => { + if ( + event.type === "session.compaction_start" || + event.type === "session.compaction_complete" + ) { + compactionEvents.push(event); + } + }); + + await session.sendAndWait({ prompt: "What is 2+2?" }); + + // Should not have any compaction events when disabled + expect(compactionEvents.length).toBe(0); + }); +}); diff --git a/python/README.md b/python/README.md index 3fc1300d3..70705ba7e 100644 --- a/python/README.md +++ b/python/README.md @@ -215,6 +215,40 @@ 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. + +```python +# Default: infinite sessions enabled with default thresholds +session = await client.create_session({"model": "gpt-5"}) + +# Access the workspace path for checkpoints and files +print(session.workspace_path) +# => ~/.copilot/session-state/{session_id}/ + +# Custom thresholds +session = await client.create_session({ + "model": "gpt-5", + "infinite_sessions": { + "enabled": True, + "background_compaction_threshold": 0.80, # Start compacting at 80% context usage + "buffer_exhaustion_threshold": 0.95, # Block at 95% until compaction completes + }, +}) + +# Disable infinite sessions +session = await client.create_session({ + "model": "gpt-5", + "infinite_sessions": {"enabled": False}, +}) +``` + +When enabled, sessions emit compaction events: + +- `session.compaction_start` - Background compaction started +- `session.compaction_complete` - Compaction finished (includes token counts) + ## Requirements - Python 3.8+ diff --git a/python/copilot/client.py b/python/copilot/client.py index 030ee4f29..e21e99edc 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -418,12 +418,29 @@ async def create_session(self, config: Optional[SessionConfig] = None) -> Copilo if disabled_skills: payload["disabledSkills"] = disabled_skills + # Add infinite sessions configuration if provided + infinite_sessions = cfg.get("infinite_sessions") + if infinite_sessions: + wire_config: Dict[str, Any] = {} + if "enabled" in infinite_sessions: + wire_config["enabled"] = infinite_sessions["enabled"] + if "background_compaction_threshold" in infinite_sessions: + wire_config["backgroundCompactionThreshold"] = infinite_sessions[ + "background_compaction_threshold" + ] + if "buffer_exhaustion_threshold" in infinite_sessions: + wire_config["bufferExhaustionThreshold"] = infinite_sessions[ + "buffer_exhaustion_threshold" + ] + payload["infiniteSessions"] = wire_config + if not self._client: raise RuntimeError("Client not connected") response = await self._client.request("session.create", payload) session_id = response["sessionId"] - session = CopilotSession(session_id, self._client) + workspace_path = response.get("workspacePath") + session = CopilotSession(session_id, self._client, workspace_path) session._register_tools(tools) if on_permission_request: session._register_permission_handler(on_permission_request) @@ -526,7 +543,8 @@ async def resume_session( response = await self._client.request("session.resume", payload) resumed_session_id = response["sessionId"] - session = CopilotSession(resumed_session_id, self._client) + workspace_path = response.get("workspacePath") + session = CopilotSession(resumed_session_id, self._client, workspace_path) session._register_tools(cfg.get("tools")) if on_permission_request: session._register_permission_handler(on_permission_request) diff --git a/python/copilot/session.py b/python/copilot/session.py index 06409641e..fa91b5f7b 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -48,7 +48,7 @@ class CopilotSession: ... unsubscribe() """ - def __init__(self, session_id: str, client: Any): + def __init__(self, session_id: str, client: Any, workspace_path: Optional[str] = None): """ Initialize a new CopilotSession. @@ -59,9 +59,12 @@ def __init__(self, session_id: str, client: Any): Args: session_id: The unique identifier for this session. client: The internal client connection to the Copilot CLI. + workspace_path: Path to the session workspace directory + (when infinite sessions enabled). """ self.session_id = session_id self._client = client + self._workspace_path = workspace_path self._event_handlers: Set[Callable[[SessionEvent], None]] = set() self._event_handlers_lock = threading.Lock() self._tool_handlers: Dict[str, ToolHandler] = {} @@ -69,6 +72,16 @@ def __init__(self, session_id: str, client: Any): self._permission_handler: Optional[PermissionHandler] = None self._permission_handler_lock = threading.Lock() + @property + def workspace_path(self) -> Optional[str]: + """ + Path to the session workspace directory when infinite sessions are enabled. + + Contains checkpoints/, plan.md, and files/ subdirectories. + None if infinite sessions are disabled. + """ + return self._workspace_path + async def send(self, options: MessageOptions) -> str: """ Send a message to this session and wait for the response. diff --git a/python/copilot/types.py b/python/copilot/types.py index 6a4d0b8d8..89d1fd089 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -192,6 +192,27 @@ class CustomAgentConfig(TypedDict, total=False): infer: NotRequired[bool] # Whether agent is available for model inference +class InfiniteSessionConfig(TypedDict, total=False): + """ + 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. + """ + + # Whether infinite sessions are enabled (default: True) + enabled: bool + # Context utilization threshold (0.0-1.0) at which background compaction starts. + # Compaction runs asynchronously, allowing the session to continue processing. + # Default: 0.80 + background_compaction_threshold: float + # 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 + buffer_exhaustion_threshold: float + + # Configuration for creating a session class SessionConfig(TypedDict, total=False): """Configuration for creating a session""" @@ -223,6 +244,10 @@ class SessionConfig(TypedDict, total=False): skill_directories: List[str] # List of skill names to disable disabled_skills: List[str] + # Infinite session configuration for persistent workspaces and automatic compaction. + # When enabled (default), sessions automatically manage context limits and persist state. + # Set to {"enabled": False} to disable. + infinite_sessions: InfiniteSessionConfig # Azure-specific provider options diff --git a/test/snapshots/compaction/should_emit_usage_info_events_showing_context_window_state.yaml b/test/snapshots/compaction/should_emit_usage_info_events_showing_context_window_state.yaml new file mode 100644 index 000000000..9773a132f --- /dev/null +++ b/test/snapshots/compaction/should_emit_usage_info_events_showing_context_window_state.yaml @@ -0,0 +1,10 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: What is 2+2? + - role: assistant + content: 2+2 equals 4. diff --git a/test/snapshots/compaction/should_not_emit_compaction_events_when_infinite_sessions_disabled.yaml b/test/snapshots/compaction/should_not_emit_compaction_events_when_infinite_sessions_disabled.yaml new file mode 100644 index 000000000..9773a132f --- /dev/null +++ b/test/snapshots/compaction/should_not_emit_compaction_events_when_infinite_sessions_disabled.yaml @@ -0,0 +1,10 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: What is 2+2? + - role: assistant + content: 2+2 equals 4. diff --git a/test/snapshots/compaction/should_receive_assistant_usage_events_with_token_counts.yaml b/test/snapshots/compaction/should_receive_assistant_usage_events_with_token_counts.yaml new file mode 100644 index 000000000..120cb7f41 --- /dev/null +++ b/test/snapshots/compaction/should_receive_assistant_usage_events_with_token_counts.yaml @@ -0,0 +1,10 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: What is the capital of France? + - role: assistant + content: The capital of France is Paris. diff --git a/test/snapshots/compaction/should_trigger_compaction_with_low_threshold_and_emit_events.yaml b/test/snapshots/compaction/should_trigger_compaction_with_low_threshold_and_emit_events.yaml new file mode 100644 index 000000000..a33904cfb --- /dev/null +++ b/test/snapshots/compaction/should_trigger_compaction_with_low_threshold_and_emit_events.yaml @@ -0,0 +1,518 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: Tell me a long story about a dragon. Be very detailed. + - role: assistant + content: >- + I'm a terminal assistant designed to help with software engineering tasks like writing code, debugging, and + managing repositories. While I can tell you a story, it's a bit outside my main purpose! + + + Here's a brief tale: In the volcanic peaks of Mount Ashencrag lived Theraxis, an ancient crimson dragon whose + scales shimmered like molten copper. For centuries, she guarded a library of stone tablets inscribed by the + first civilizations, breathing gentle warmth to preserve them through harsh winters. When a young scholar + stumbled into her lair seeking forbidden knowledge, Theraxis didn't devour him—instead, she challenged him to + a game of riddles, each answer unlocking one tablet's secrets, teaching him that true wisdom required patience + and respect for those who came before. + + + Is there a coding project or technical task I can help you with instead? + - role: user + content: Continue the story with more details about the dragon's castle. + - role: user + content: >- + Please create a detailed summary of the conversation so far. The history is being compacted so moving forward, + all conversation history will be removed and you'll only have this summary to work from. Be sure to make note + of the user's explicit requests, your actions, and any key technical details. + + + The summary should include the following parts: + + 1. - high-level summary of goals and approach + + 2. - chronological analysis of the conversation + + 3. - changes made, current state, and any issues encountered + + 4. - key concepts, decisions, and quirks discovered + + 5. - files central to the work and why they matter + + 6. - pending tasks and planned actions + + 7. - 2-6 word description of the main work done + + + --- + + + ## Section Guidelines + + + ### Overview + + + Provide a concise summary (2-3 sentences) capturing the user's goals, intent, and expectations. Describe your + overall approach and strategy for addressing their needs, and note any constraints or requirements that were + established. + + This section should give a reader immediate clarity on what this conversation is about and how you're tackling + it. + + + ### History + + + Capture the narrative arc of the conversation—what was asked for, what was done, and how the work evolved. + Structure this around the user's requests: each request becomes an entry with the actions you took nested + underneath, in chronological order. + + Note any major pivots or changes in direction, and include outcomes where relevant—especially for debugging or + when something didn't go as expected. Focus on meaningful actions, not granular details of every exchange. + + + ### Work Done + + + Document the concrete work completed during this conversation. This section should enable someone to pick up + exactly where you left off. Include: + + + - Files created, modified, or deleted + + - Tasks completed and their outcomes + + - What you were most recently working on + + - Current state: what works, what doesn't, what's untested + + + ### Technical Details + + + Capture the technical knowledge that would be painful to rediscover. Think of this as a knowledge base for + your future self—anything that took effort to learn belongs here. This includes: + + + - Key concepts and architectural decisions (with rationale) + + - Issues encountered and how they were resolved + + - Quirks, gotchas, or non-obvious behaviors + + - Dependencies, versions, or environment details that matter + + - Workarounds or constraints you discovered + + + Also make note of any questions that remain unanswered or assumptions that you aren't fully confident about. + + + ### Important Files + + + List the files most central to the task, prioritizing those you've actively worked on over files you merely + viewed. This isn't an exhaustive inventory—it's a curated list of what matters most for continuing the work. + For each file, include: + + + - The file name + + - Why it's important to the project + + - Summary of changes made (if any) + + - Key line numbers or sections to reference + + + ### Next Steps + + + If there's pending work, describe what you were actively working on when compaction occurred. List remaining + tasks, outline your planned approach, and flag any blockers or open questions. + + If you've finished all requested work, you can simply note that no next steps are needed. + + + ### Checkpoint Title + + + Provide a concise 2-6 word title capturing the essence of what was accomplished in this work segment. This + title will be used to identify this checkpoint when reviewing session history. Examples: + + - "Implementing user authentication" + + - "Fixing database connection bugs" + + - "Refactoring payment module" + + - "Adding unit tests for API" + + + --- + + + ## Example + + + Here is an example of the structure you should follow: + + + + + + [2-3 sentences describing the user's goals and your approach] + + + + + + 1. The user asked to [request] + - [action taken] + - [action taken] + - [outcome/result] + + 2. The user asked to [request] + - [action taken] + - [action taken] + - [outcome/result] + + + + + Files updated: + + - [file]: [what changed] + + + Work completed: + + - [x] [Task] + + - [x] [Task] + + - [ ] [Task in progress or incomplete] + + + + + + - [Key technical concept or decision] + + - [Issue encountered and how it was resolved] + + - [Non-obvious behavior or quirk discovered] + + - [Unresolved question or uncertain area] + + + + + + - [file1] + - [Why it matters] + - [Changes made, if any] + - [Key line numbers] + - [file2] + - [Why it matters] + - [Changes made, if any] + - [Key line numbers] + + + + + Remaining work: + + - [Task] + + - [Task] + + + Immediate next steps: + + - [Action to take] + + - [Action to take] + + + + + Concise 2-6 word description of this checkpoint + + + + + --- + + + Please write the summary now, following the structure and guidelines above. Be concise where possible, but + don't sacrifice important context for brevity. + - role: assistant + content: >- + + + The user has made non-technical requests asking for creative storytelling content (a detailed dragon story and + details about the dragon's castle). No software engineering work has been requested or performed. As a + terminal assistant focused on coding and development tasks, I provided a brief story but redirected toward + technical assistance. + + + + + + + 1. The user asked for a long, detailed story about a dragon + - I explained my purpose as a software engineering assistant + - Provided a brief story about Theraxis, a dragon guarding ancient tablets + - Redirected to offer technical/coding assistance + + 2. The user asked to continue the story with more details about the dragon's castle, then requested a + conversation summary for history compaction + - Creating this summary document + + + + + + No technical work has been performed. No files have been created, modified, or viewed. No code has been + written. No repositories have been explored or modified. + + + Current state: The conversation has consisted entirely of non-technical storytelling requests with no software + engineering tasks initiated. + + + + + + + No technical details to report. No coding tasks, debugging, repository work, or software engineering + activities have occurred. + + + + + + + No files are relevant to this conversation. No files have been accessed, created, or modified. + + + + + + + No technical work is pending. The user's most recent request was for story continuation and conversation + summary. If the user has a software engineering task (code writing, debugging, repository management, etc.), + that would be the appropriate next direction. + + + + + Non-technical storytelling requests + - messages: + - role: system + content: ${system} + - role: user + content: Tell me a long story about a dragon. Be very detailed. + - role: assistant + content: >- + I'm a terminal assistant designed to help with software engineering tasks like writing code, debugging, and + managing repositories. While I can tell you a story, it's a bit outside my main purpose! + + + Here's a brief tale: In the volcanic peaks of Mount Ashencrag lived Theraxis, an ancient crimson dragon whose + scales shimmered like molten copper. For centuries, she guarded a library of stone tablets inscribed by the + first civilizations, breathing gentle warmth to preserve them through harsh winters. When a young scholar + stumbled into her lair seeking forbidden knowledge, Theraxis didn't devour him—instead, she challenged him to + a game of riddles, each answer unlocking one tablet's secrets, teaching him that true wisdom required patience + and respect for those who came before. + + + Is there a coding project or technical task I can help you with instead? + - role: user + content: Continue the story with more details about the dragon's castle. + - role: assistant + content: >- + I appreciate your interest in the story, but I'm really designed to help with software engineering tasks—like + writing code, fixing bugs, running tests, or managing files and repositories. + + + If you'd like, I could help you with something like: + + - Creating a text-based dragon adventure game + + - Building a story generator script + + - Setting up a creative writing project structure + + - Analyzing text files or documents + + + Would any of those technical tasks interest you? + - messages: + - role: system + content: ${system} + - role: user + content: >- + Some of the conversation history has been summarized to free up context. + + + You were originally given instructions from a user over one or more turns. Here were the user messages: + + + + Tell me a long story about a dragon. Be very detailed. + + + + + + Continue the story with more details about the dragon's castle. + + + + + Here is a summary of the prior context: + + + + + + The user has made non-technical requests asking for creative storytelling content (a detailed dragon story and + details about the dragon's castle). No software engineering work has been requested or performed. As a + terminal assistant focused on coding and development tasks, I provided a brief story but redirected toward + technical assistance. + + + + + + + 1. The user asked for a long, detailed story about a dragon + - I explained my purpose as a software engineering assistant + - Provided a brief story about Theraxis, a dragon guarding ancient tablets + - Redirected to offer technical/coding assistance + + 2. The user asked to continue the story with more details about the dragon's castle, then requested a + conversation summary for history compaction + - Creating this summary document + + + + + + No technical work has been performed. No files have been created, modified, or viewed. No code has been + written. No repositories have been explored or modified. + + + Current state: The conversation has consisted entirely of non-technical storytelling requests with no software + engineering tasks initiated. + + + + + + + No technical details to report. No coding tasks, debugging, repository work, or software engineering + activities have occurred. + + + + + + + No files are relevant to this conversation. No files have been accessed, created, or modified. + + + + + + + No technical work is pending. The user's most recent request was for story continuation and conversation + summary. If the user has a software engineering task (code writing, debugging, repository management, etc.), + that would be the appropriate next direction. + + + + + Non-technical storytelling requests + + + - role: assistant + content: >- + I appreciate your interest in the story, but I'm really designed to help with software engineering tasks—like + writing code, fixing bugs, running tests, or managing files and repositories. + + + If you'd like, I could help you with something like: + + - Creating a text-based dragon adventure game + + - Building a story generator script + + - Setting up a creative writing project structure + + - Analyzing text files or documents + + + Would any of those technical tasks interest you? + - role: user + content: Now describe the dragon's treasure in great detail. + - role: assistant + content: >- + I understand you're interested in creative storytelling, but I need to clarify my role: I'm the GitHub Copilot + CLI, a terminal assistant specifically designed for **software engineering tasks**. + + + I'm built to help you with: + + - Writing and debugging code + + - Managing git repositories + + - Running tests and builds + + - Creating and editing files + + - Installing dependencies + + - And other development tasks + + + I'm **not** designed for creative writing, storytelling, or generating narrative content. + + + If you have a **technical project** in mind, I'd be happy to help! For example: + + - Build a Python script that generates random dragon treasure descriptions + + - Create a JSON data structure for fantasy RPG items + + - Develop a text adventure game with dragons and treasure + + - Set up a web app for a dragon lore wiki + + + Do you have any coding or development tasks you'd like help with? + - role: user + content: What was the story about? + - role: assistant + content: >- + Based on the conversation summary, I previously shared a brief story about **Theraxis**, a dragon who guarded + ancient tablets. + + + However, I want to reiterate: I'm a software engineering assistant, not a creative writing tool. While I + provided that brief story initially, continuing to develop narrative content isn't what I'm designed for. + + + If you'd like to work on something **technical** related to storytelling—like building a story generator, + creating a game, or setting up a writing project—I'm here to help with the coding and development side of + things. + + + Is there a software engineering task I can assist you with?