Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 6 additions & 5 deletions .claude/skills/copilot-sdk-reference/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: copilot-sdk-reference
description: >
Complete API reference for GitHub.Copilot.SDK v0.2.1. Consult before implementing
Complete API reference for GitHub.Copilot.SDK v0.2.2. Consult before implementing
session lifecycle, orchestration, event handling, hooks, plan management, context
compaction, tool monitoring, or any feature that interacts with the Copilot CLI server.
Use when: (1) Adding new session management code, (2) Modifying event handlers,
Expand All @@ -10,11 +10,11 @@ description: >
(7) Updating the SDK NuGet package version.
---

# GitHub Copilot SDK v0.2.1 — Complete API Reference
# GitHub Copilot SDK v0.2.2 — Complete API Reference

**Package:** `GitHub.Copilot.SDK` v0.2.1
**NuGet:** `<PackageReference Include="GitHub.Copilot.SDK" Version="0.2.1" />`
**XML Docs:** `~/.nuget/packages/github.copilot.sdk/0.2.1/lib/net8.0/GitHub.Copilot.SDK.xml`
**Package:** `GitHub.Copilot.SDK` v0.2.2
**NuGet:** `<PackageReference Include="GitHub.Copilot.SDK" Version="0.2.2" />`
**XML Docs:** `~/.nuget/packages/github.copilot.sdk/0.2.2/lib/net8.0/GitHub.Copilot.SDK.xml`
**Types:** 453 total

> **Rule:** Before implementing custom session/event/orchestration code, check this reference.
Expand Down Expand Up @@ -388,6 +388,7 @@ Three built-in agents (in `definitions/*.agent.yaml`):

| Version | Key Additions |
|---------|---------------|
| **0.2.2** | **Breaking:** `SessionIdleData.BackgroundTasks` removed (use `SessionBackgroundTasksChangedEvent` instead), `ModelApi.SwitchToAsync` added `ModelCapabilitiesOverride?` parameter, `CompactionApi` removed (use `HistoryApi.CompactAsync`). **New:** `ISessionFsHandler` (10 filesystem methods), `HistoryApi.TruncateAsync`, `ServerSessionsApi.ForkAsync`, `ModelCapabilitiesOverride`/`ModelCapabilitiesOverrideLimits`/`ModelCapabilitiesOverrideLimitsVision` (vision support), `EntryType` enum, `ResumeSessionConfig.EnableConfigDiscovery`/`ModelCapabilities`, `ElicitationCompletedData.Action`/`Content`, `AssistantMessageData.RequestId` |
| **0.2.1** | `CommandDefinition`/`CommandHandler` (custom slash commands), `ElicitationHandler`/`ElicitationContext` (structured input callbacks), `ServerMcpApi`, `ServerSessionFsApi`, `CapabilitiesChangedEvent`, `SamplingRequestedEvent`, `SessionRemoteSteerableChangedEvent`, `SessionCustomAgentsUpdatedEvent`, `ISessionUiApi`, `InputOptions` |
| **0.2.0** | Hooks (PreToolUse, PostToolUse, UserPromptSubmitted, SessionStart, SessionEnd, ErrorOccurred), Plan API, Fleet API, Agent API, Skills API, MCP API, Compaction API, Workspace API, Elicitation API, ReasoningEffort, InfiniteSessionConfig, CustomAgentConfig, SystemMessageConfig with SectionOverride |

Expand Down
6 changes: 4 additions & 2 deletions .claude/skills/processing-state-safety/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,8 +431,10 @@ Before adding or modifying watchdog, IsProcessing, or stuck-session detection co
| Detect when agent turn ends | `AgentStop` hook (JS SDK only) | 🔴 **Not in .NET SDK** | JS SDK can block and force continuation; .NET SDK has `HookStartEvent`/`HookEndEvent` but no stop-gate |
| Handle errors | `SessionHooks.OnErrorOccurred` | 🔴 **Not adopted** | Has retry count and user notification fields |
| Session lifecycle | `SessionHooks.OnSessionStart` / `OnSessionEnd` | 🔴 **Not adopted** | Supplementary telemetry only — cannot replace restart/reconnect cleanup logic |
| Context compaction | `session.Rpc.Compaction.CompactAsync()` | 🔴 **Not adopted** | Manual compaction trigger |
| Auto-compaction | `SessionConfig.InfiniteSessions` | 🔴 **Not adopted** | Background compaction with configurable thresholds |
| Context compaction | `session.Rpc.History.CompactAsync()` | 🔴 **Not adopted** | Manual compaction trigger (v0.2.2: moved from `CompactionApi` to `HistoryApi`) |
| Auto-compaction | `SessionConfig.InfiniteSessions` | ✅ **Adopted** | Used in all session configs with `Enabled = true` |
| History truncation | `session.Rpc.History.TruncateAsync()` | 🔴 **Not adopted** | New in v0.2.2 — truncate session history to a specific point |
| Session forking | `ServerSessionsApi.ForkAsync()` | 🔴 **Not adopted** | New in v0.2.2 — create a copy of a session with independent history |

### What to Keep Custom (and Why)

Expand Down
4 changes: 2 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## SDK-First Development

Before implementing new session lifecycle, orchestration, watchdog, or event handling code, check the Copilot SDK API surface. The `copilot-sdk-reference` skill has the complete v0.2.1 API reference (453 types, 76 events, 6 hooks, 16 RPC APIs). The `processing-state-safety` and `multi-agent-orchestration` skills each contain SDK migration matrices for their domains. Prefer SDK APIs over custom implementations. When custom code is necessary, add a `// SDK-gap: <reason>` comment explaining why.
Before implementing new session lifecycle, orchestration, watchdog, or event handling code, check the Copilot SDK API surface. The `copilot-sdk-reference` skill has the complete v0.2.2 API reference (453 types, 76 events, 6 hooks, 16 RPC APIs). The `processing-state-safety` and `multi-agent-orchestration` skills each contain SDK migration matrices for their domains. Prefer SDK APIs over custom implementations. When custom code is necessary, add a `// SDK-gap: <reason>` comment explaining why.

### SDK Update Audit (run when bumping `GitHub.Copilot.SDK` version)

Expand Down Expand Up @@ -362,7 +362,7 @@ When a user changes the model via the UI dropdown:
- Use `Convert.ToInt32(value)` for conversion, not `value as int?`
- `AssistantUsageData` also includes: `Model`, `Cost` (billing multiplier), `Duration` (ms), `TtftMs` (time to first token), `InterTokenLatencyMs`, `ReasoningEffort`, `Initiator` (e.g., "sub-agent", "mcp-sampling"), `CopilotUsage`, `ApiCallId`, `ProviderCallId`, `ParentToolCallId`
- `QuotaSnapshots` is `Dictionary<string, object>` with `JsonElement` values — the typed fields (`EntitlementRequests`, `UsedRequests`, `RemainingPercentage`, `Overage`, `OverageAllowedWithExhaustedQuota`, `ResetDate`) are defined on `Rpc.AccountGetQuotaResultQuotaSnapshotsValue`
- `SessionIdleData` includes `BackgroundTasks` (agents + shells) and `Aborted` (bool?, true when turn was cancelled via abort)
- `SessionIdleData` includes `Aborted` (bool?, true when turn was cancelled via abort). **Note (v0.2.2):** `BackgroundTasks` was removed from `SessionIdleData` — background task tracking is now exclusively via `SessionBackgroundTasksChangedEvent`. The idle handler reads tracked state from `DeferredBackgroundTaskFingerprint`/`DeferredBackgroundTasksFirstSeenAtTicks` (set by the background tasks changed handler).
- `MessageOptions` has 3 properties: `Prompt`, `Attachments`, `Mode` — no `Model` or `ReasoningEffort` (those are session-level via `SwitchToAsync`)

### Blazor Input Performance
Expand Down
2 changes: 1 addition & 1 deletion PolyPilot.Gtk/PolyPilot.Gtk.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
<PackageReference Include="Platform.Maui.Linux.Gtk4" Version="0.6.0" />
<PackageReference Include="Platform.Maui.Linux.Gtk4.Essentials" Version="0.6.0" />
<PackageReference Include="Platform.Maui.Linux.Gtk4.BlazorWebView" Version="0.6.0" />
<PackageReference Include="GitHub.Copilot.SDK" Version="0.2.0" />
<PackageReference Include="GitHub.Copilot.SDK" Version="0.2.2" />
<PackageReference Include="Markdig" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView" Version="10.0.5" GeneratePathProperty="true" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="$(MauiVersion)" />
Expand Down
189 changes: 70 additions & 119 deletions PolyPilot.Tests/BackgroundTasksIdleTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using GitHub.Copilot.SDK;
using PolyPilot.Models;
using PolyPilot.Services;

Expand All @@ -8,108 +7,99 @@ namespace PolyPilot.Tests;
/// Tests for HasActiveBackgroundTasks — the fix that prevents premature idle completion
/// when the SDK reports active background tasks (sub-agents, shells) in SessionIdleEvent.
/// See: session.idle with backgroundTasks means "foreground quiesced, background still running."
///
/// SDK v0.2.2: SessionIdleDataBackgroundTasks removed. Background tasks are now tracked
/// via BackgroundTaskSnapshot and SessionBackgroundTasksChangedEvent.
/// </summary>
public class BackgroundTasksIdleTests
{
private static SessionIdleEvent MakeIdle(SessionIdleDataBackgroundTasks? bt = null)
// Test DTOs for reflection-based GetBackgroundTaskSnapshot / GetBackgroundTaskFirstSeenTicks
private class FakeBackgroundTasks
{
return new SessionIdleEvent
public FakeAgent[] Agents { get; set; } = Array.Empty<FakeAgent>();
public FakeShell[] Shells { get; set; } = Array.Empty<FakeShell>();
}
private class FakeAgent
{
public string AgentId { get; set; } = "";
public string AgentType { get; set; } = "";
public string Description { get; set; } = "";
}
private class FakeShell
{
public string ShellId { get; set; } = "";
public string Description { get; set; } = "";
}

private static CopilotService.BackgroundTaskSnapshot MakeSnapshot(int agents = 0, int shells = 0)
{
var parts = new List<string>();
for (int i = 0; i < agents; i++) parts.Add($"agent:agent-{i}");
for (int i = 0; i < shells; i++) parts.Add($"shell:shell-{i}");
return new CopilotService.BackgroundTaskSnapshot(agents, shells, string.Join("|", parts), IsKnown: true);
}

private static FakeBackgroundTasks MakeFakeBt(int agents = 0, int shells = 0)
{
return new FakeBackgroundTasks
{
Data = new SessionIdleData { BackgroundTasks = bt }
Agents = Enumerable.Range(0, agents)
.Select(i => new FakeAgent { AgentId = $"agent-{i}", AgentType = "copilot", Description = "" })
.ToArray(),
Shells = Enumerable.Range(0, shells)
.Select(i => new FakeShell { ShellId = $"shell-{i}", Description = "" })
.ToArray()
};
}

[Fact]
public void HasActiveBackgroundTasks_NullBackgroundTasks_ReturnsFalse()
{
var idle = MakeIdle(bt: null);
Assert.False(CopilotService.HasActiveBackgroundTasks(idle));
var snapshot = CopilotService.GetBackgroundTaskSnapshot(null);
Assert.False(CopilotService.HasActiveBackgroundTasks(snapshot));
}

[Fact]
public void HasActiveBackgroundTasks_EmptyBackgroundTasks_ReturnsFalse()
{
var idle = MakeIdle(new SessionIdleDataBackgroundTasks
{
Agents = Array.Empty<SessionIdleDataBackgroundTasksAgentsItem>(),
Shells = Array.Empty<SessionIdleDataBackgroundTasksShellsItem>()
});
Assert.False(CopilotService.HasActiveBackgroundTasks(idle));
var snapshot = CopilotService.GetBackgroundTaskSnapshot(MakeFakeBt());
Assert.False(CopilotService.HasActiveBackgroundTasks(snapshot));
}

[Fact]
public void HasActiveBackgroundTasks_WithAgents_ReturnsTrue()
{
var idle = MakeIdle(new SessionIdleDataBackgroundTasks
{
Agents = new[]
{
new SessionIdleDataBackgroundTasksAgentsItem
{
AgentId = "agent-42",
AgentType = "copilot",
Description = "PR reviewer"
}
},
Shells = Array.Empty<SessionIdleDataBackgroundTasksShellsItem>()
});
Assert.True(CopilotService.HasActiveBackgroundTasks(idle));
var snapshot = CopilotService.GetBackgroundTaskSnapshot(MakeFakeBt(agents: 1));
Assert.True(CopilotService.HasActiveBackgroundTasks(snapshot));
}

[Fact]
public void HasActiveBackgroundTasks_WithShells_ReturnsTrue()
{
var idle = MakeIdle(new SessionIdleDataBackgroundTasks
{
Agents = Array.Empty<SessionIdleDataBackgroundTasksAgentsItem>(),
Shells = new[]
{
new SessionIdleDataBackgroundTasksShellsItem
{
ShellId = "shell-1",
Description = "Running npm test"
}
}
});
Assert.True(CopilotService.HasActiveBackgroundTasks(idle));
var snapshot = CopilotService.GetBackgroundTaskSnapshot(MakeFakeBt(shells: 1));
Assert.True(CopilotService.HasActiveBackgroundTasks(snapshot));
}

[Fact]
public void HasActiveBackgroundTasks_WithBothAgentsAndShells_ReturnsTrue()
{
var idle = MakeIdle(new SessionIdleDataBackgroundTasks
{
Agents = new[]
{
new SessionIdleDataBackgroundTasksAgentsItem
{
AgentId = "a1", AgentType = "copilot", Description = ""
}
},
Shells = new[]
{
new SessionIdleDataBackgroundTasksShellsItem
{
ShellId = "s1", Description = ""
}
}
});
Assert.True(CopilotService.HasActiveBackgroundTasks(idle));
var snapshot = CopilotService.GetBackgroundTaskSnapshot(MakeFakeBt(agents: 1, shells: 1));
Assert.True(CopilotService.HasActiveBackgroundTasks(snapshot));
}

[Fact]
public void HasActiveBackgroundTasks_DefaultIdle_ReturnsFalse()
public void HasActiveBackgroundTasks_DefaultEmpty_ReturnsFalse()
{
// Default SessionIdleEventData is auto-initialized but BackgroundTasks is null
var idle = new SessionIdleEvent { Data = new SessionIdleData() };
Assert.False(CopilotService.HasActiveBackgroundTasks(idle));
// Empty snapshotno agents, no shells
var snapshot = new CopilotService.BackgroundTaskSnapshot(0, 0, string.Empty, IsKnown: true);
Assert.False(CopilotService.HasActiveBackgroundTasks(snapshot));
}

[Fact]
public void HasActiveBackgroundTasks_DataNull_ReturnsFalse()
public void HasActiveBackgroundTasks_NullInput_ReturnsFalse()
{
var idle = new SessionIdleEvent { Data = null! };
Assert.False(CopilotService.HasActiveBackgroundTasks(idle));
var snapshot = CopilotService.GetBackgroundTaskSnapshot(null);
Assert.False(CopilotService.HasActiveBackgroundTasks(snapshot));
}

// --- IDLE-DEFER-REARM tests (issue #403) ---
Expand All @@ -125,24 +115,14 @@ public void IdleDeferRearm_ShouldRearm_WhenBackgroundTasksActiveAndNotProcessing
var info = new AgentSessionInfo { Name = "test", Model = "test-model" };
info.IsProcessing = false; // Cleared by watchdog

var idle = MakeIdle(new SessionIdleDataBackgroundTasks
{
Agents = new[]
{
new SessionIdleDataBackgroundTasksAgentsItem
{
AgentId = "agent-1", AgentType = "copilot", Description = "worker"
}
},
Shells = Array.Empty<SessionIdleDataBackgroundTasksShellsItem>()
});
var snapshot = MakeSnapshot(agents: 1);

// Verify preconditions
Assert.False(info.IsProcessing);
Assert.True(CopilotService.HasActiveBackgroundTasks(idle));
Assert.True(CopilotService.HasActiveBackgroundTasks(snapshot));

// The re-arm logic: if !IsProcessing && HasActiveBackgroundTasks && !WasUserAborted → re-arm
bool shouldRearm = !info.IsProcessing && CopilotService.HasActiveBackgroundTasks(idle);
bool shouldRearm = !info.IsProcessing && CopilotService.HasActiveBackgroundTasks(snapshot);
Assert.True(shouldRearm, "Should re-arm when background tasks active and not processing");

// Simulate re-arm
Expand All @@ -163,9 +143,9 @@ public void IdleDeferRearm_ShouldNotRearm_WhenNoBackgroundTasks()
var info = new AgentSessionInfo { Name = "test", Model = "test-model" };
info.IsProcessing = false;

var idle = MakeIdle(bt: null);
var snapshot = CopilotService.GetBackgroundTaskSnapshot(null);

bool shouldRearm = !info.IsProcessing && CopilotService.HasActiveBackgroundTasks(idle);
bool shouldRearm = !info.IsProcessing && CopilotService.HasActiveBackgroundTasks(snapshot);
Assert.False(shouldRearm, "Should NOT re-arm when no background tasks");
}

Expand All @@ -177,19 +157,9 @@ public void IdleDeferRearm_ShouldNotRearm_WhenAlreadyProcessing()
var info = new AgentSessionInfo { Name = "test", Model = "test-model" };
info.IsProcessing = true; // Already processing

var idle = MakeIdle(new SessionIdleDataBackgroundTasks
{
Agents = new[]
{
new SessionIdleDataBackgroundTasksAgentsItem
{
AgentId = "a1", AgentType = "copilot", Description = ""
}
},
Shells = Array.Empty<SessionIdleDataBackgroundTasksShellsItem>()
});

bool shouldRearm = !info.IsProcessing && CopilotService.HasActiveBackgroundTasks(idle);
var snapshot = MakeSnapshot(agents: 1);

bool shouldRearm = !info.IsProcessing && CopilotService.HasActiveBackgroundTasks(snapshot);
Assert.False(shouldRearm, "Should NOT re-arm when already processing");
}

Expand All @@ -202,20 +172,10 @@ public void IdleDeferRearm_ShouldNotRearm_WhenUserAborted()
info.IsProcessing = false;
bool wasUserAborted = true;

var idle = MakeIdle(new SessionIdleDataBackgroundTasks
{
Agents = new[]
{
new SessionIdleDataBackgroundTasksAgentsItem
{
AgentId = "a1", AgentType = "copilot", Description = ""
}
},
Shells = Array.Empty<SessionIdleDataBackgroundTasksShellsItem>()
});
var snapshot = MakeSnapshot(agents: 1);

// The full re-arm condition includes WasUserAborted check
bool shouldRearm = !info.IsProcessing && CopilotService.HasActiveBackgroundTasks(idle) && !wasUserAborted;
bool shouldRearm = !info.IsProcessing && CopilotService.HasActiveBackgroundTasks(snapshot) && !wasUserAborted;
Assert.False(shouldRearm, "Should NOT re-arm when user aborted");
}

Expand Down Expand Up @@ -277,14 +237,7 @@ public void ProactiveIdleDefer_Handler_ResolvesDeferredIdleViaHelperWhenTasksCle
[Fact]
public void GetBackgroundTaskFirstSeenTicks_PreservesExistingTimestampWhenFingerprintMatches()
{
var bt = new SessionIdleDataBackgroundTasks
{
Agents = Array.Empty<SessionIdleDataBackgroundTasksAgentsItem>(),
Shells = new[]
{
new SessionIdleDataBackgroundTasksShellsItem { ShellId = "shell-1", Description = "" }
}
};
var bt = MakeFakeBt(shells: 1);
var snapshot = CopilotService.GetBackgroundTaskSnapshot(bt);
var existingTicks = DateTime.UtcNow.AddMinutes(-5).Ticks;

Expand All @@ -300,13 +253,10 @@ public void GetBackgroundTaskFirstSeenTicks_PreservesExistingTimestampWhenFinger
[Fact]
public void GetBackgroundTaskFirstSeenTicks_RefreshesWhenFingerprintChanges()
{
var bt = new SessionIdleDataBackgroundTasks
var bt = new FakeBackgroundTasks
{
Agents = Array.Empty<SessionIdleDataBackgroundTasksAgentsItem>(),
Shells = new[]
{
new SessionIdleDataBackgroundTasksShellsItem { ShellId = "shell-2", Description = "" }
}
Agents = Array.Empty<FakeAgent>(),
Shells = new[] { new FakeShell { ShellId = "shell-2", Description = "" } }
};
var before = DateTime.UtcNow;

Expand Down Expand Up @@ -348,7 +298,8 @@ public void SessionIdle_StalePayload_NotDeferredWhenBgTasksAlreadyConfirmedEmpty
Assert.Contains("idlePayloadIsStale", handler);
Assert.Contains("preIdleFingerprint == string.Empty", handler);
Assert.Contains("preIdleTicks == 0", handler);
Assert.Contains("tracking.Snapshot.HasAny", handler);
// hasActiveTasks uses tracked fingerprint state (not null snapshot)
Assert.Contains("hasTrackedTasks", handler);
// hasActiveTasks must be guarded by !idlePayloadIsStale
Assert.Contains("!idlePayloadIsStale", handler);
}
Expand Down
Loading