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
1 change: 1 addition & 0 deletions docs/features/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ These guides cover the capabilities you can add to your Copilot SDK application.
| [Streaming Events](./streaming-events.md) | Subscribe to real-time session events (40+ event types) |
| [Usage and Billing](./usage-and-billing.md) | Read token counts, context-window utilization, AI credit cost, and account quota |
| [Steering & Queueing](./steering-and-queueing.md) | Control message delivery—immediate steering vs. sequential queueing |
| [Context Clearing](./context-management.md) | Replace conversation context safely with terminal tools |
| [Session Persistence](./session-persistence.md) | Resume sessions across restarts, manage session storage |
| [Remote Sessions](./remote-sessions.md) | Share locally hosted sessions to GitHub web and mobile via Mission Control |
| [Cloud Sessions](./cloud-sessions.md) | Run sessions on GitHub-hosted compute through Mission Control |
Expand Down
57 changes: 57 additions & 0 deletions docs/features/context-management.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Context clearing and terminal tools

Use `session.history.clearContext` when a host needs to replace the current conversation context without replacing the session. Typical uses include handoffs and host-managed context lifecycle policies.

Context clearing is different from creating a new session: it preserves the session identity, system and developer messages, configuration, and event log while removing the model-facing conversation.

> [!IMPORTANT]
> `clearContext` is a tool-handler primitive. The runtime rejects calls made without a tool call in flight, calls with an empty seed prompt, and calls on remote sessions.

## Define a context-clearing tool

A successful context-clearing tool should be terminal. Otherwise, the agent loop may make another model call against the newly cleared window before starting the seeded turn.

```typescript
import { approveAll, CopilotClient, defineTool } from "@github/copilot-sdk";
import type { CopilotSession } from "@github/copilot-sdk";
import { z } from "zod";

const client = new CopilotClient();
let session: CopilotSession;

session = await client.createSession({
onPermissionRequest: approveAll,
tools: [
defineTool("clear_context", {
description: "Clear the conversation and start a fresh context window",
parameters: z.object({ prompt: z.string() }),
isTerminal: true,
defer: "never",
handler: async ({ prompt }) => {
const { messagesCleared } =
await session.rpc.history.clearContext({ prompt });
return `Cleared ${messagesCleared} messages.`;
},
}),
],
});
```

The required `prompt` becomes the first user message in the fresh context. A successful clear emits `session.context_cleared` with the number of removed messages and the initial message.

## Terminal-tool behavior

`isTerminal` ends the current agent turn only when the tool succeeds. A failure, denial, rejection, timeout, or input-validation error remains visible to the model so it can recover or retry.

The option follows each language's naming conventions:

| SDK | Tool option |
|---|---|
| Node.js | `isTerminal` |
| Python | `is_terminal` |
| Go | `IsTerminal` |
| .NET | `CopilotToolOptions.IsTerminal` |
| Java | `ToolDefinition.isTerminal(true)` or `@CopilotTool(isTerminal = true)` |
| Rust | `with_is_terminal(true)` |

Use terminality only for tools whose successful completion should end the turn. Ordinary tools should leave it unset.
1 change: 1 addition & 0 deletions docs/troubleshooting/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b
| Agent management | `session.rpc.agent.*` | List, select, deselect, get current agent |
| Fleet mode | `session.rpc.fleet.start()` | Parallel sub-agent execution; see [Fleet mode](../features/fleet-mode.md) |
| Manual compaction | `session.rpc.history.compact()` | Trigger compaction on demand |
| Context clearing | `session.rpc.history.clearContext()` | Replace conversation context from a terminal tool |
| History truncation | `session.rpc.history.truncate()` | Remove events from a point onward |
| Session forking | `server.rpc.sessions.fork()` | Fork a session at a point in history |

Expand Down
7 changes: 5 additions & 2 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2788,19 +2788,22 @@ internal record ToolDefinition(
bool? OverridesBuiltInTool = null,
bool? SkipPermission = null,
CopilotToolDefer? Defer = null,
IDictionary<string, JsonNode?>? Metadata = null)
IDictionary<string, JsonNode?>? Metadata = null,
bool? IsTerminal = null)
{
public static ToolDefinition FromAIFunction(AIFunctionDeclaration function)
{
var overrides = function.AdditionalProperties.TryGetValue(CopilotTool.OverridesBuiltInToolKey, out var val) && val is true;
var skipPerm = function.AdditionalProperties.TryGetValue(CopilotTool.SkipPermissionKey, out var skipVal) && skipVal is true;
var defer = function.AdditionalProperties.TryGetValue(CopilotTool.DeferKey, out var deferVal) && deferVal is CopilotToolDefer d ? d : (CopilotToolDefer?)null;
var metadata = function.AdditionalProperties.TryGetValue(CopilotTool.MetadataKey, out var metaVal) && metaVal is IDictionary<string, JsonNode?> m ? m : null;
var isTerminal = function.AdditionalProperties.TryGetValue(CopilotTool.IsTerminalKey, out var terminalVal) && terminalVal is true;
return new ToolDefinition(function.Name, function.Description, function.JsonSchema,
overrides ? true : null,
skipPerm ? true : null,
defer,
metadata);
metadata,
isTerminal ? true : null);
}
}

Expand Down
20 changes: 19 additions & 1 deletion dotnet/src/CopilotTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public static class CopilotTool
/// <summary>The key used in <see cref="AITool.AdditionalProperties"/> to indicate that a tool can execute without a permission prompt.</summary>
internal const string SkipPermissionKey = "skip_permission";

/// <summary>The key used in <see cref="AITool.AdditionalProperties"/> to indicate that a successful call to the tool ends the agent turn.</summary>
internal const string IsTerminalKey = "is_terminal";

/// <summary>The key used in <see cref="AITool.AdditionalProperties"/> to carry the tool's <see cref="CopilotToolDefer"/> deferral mode.</summary>
internal const string DeferKey = "defer";

Expand Down Expand Up @@ -91,7 +94,7 @@ static void ApplyToolInvocationBinding(AIFunctionFactoryOptions factoryOptions)

static void ApplyToolOptions(AIFunctionFactoryOptions factoryOptions, CopilotToolOptions? toolOptions)
{
if (toolOptions is not null && (toolOptions.OverridesBuiltInTool || toolOptions.SkipPermission || toolOptions.Defer is not null || toolOptions.Metadata is not null))
if (toolOptions is not null && (toolOptions.OverridesBuiltInTool || toolOptions.SkipPermission || toolOptions.IsTerminal || toolOptions.Defer is not null || toolOptions.Metadata is not null))
{
Dictionary<string, object?> additionalProperties = new(StringComparer.Ordinal);
if (factoryOptions.AdditionalProperties is not null)
Expand All @@ -112,6 +115,11 @@ static void ApplyToolOptions(AIFunctionFactoryOptions factoryOptions, CopilotToo
additionalProperties[SkipPermissionKey] = true;
}

if (toolOptions.IsTerminal)
{
additionalProperties[IsTerminalKey] = true;
}

if (toolOptions.Defer is { } defer)
{
additionalProperties[DeferKey] = defer;
Expand Down Expand Up @@ -152,6 +160,16 @@ public sealed class CopilotToolOptions
/// </remarks>
public bool SkipPermission { get; set; }

/// <summary>
/// Gets or sets a value indicating whether a successful call to this tool ends the agent turn.
/// </summary>
/// <remarks>
/// When true, the runtime's tool phase halts after a successful call instead of feeding the result back to the
/// model for another round. A failed call leaves the loop running so the model can read the error and retry.
/// The resulting <see cref="AIFunction"/> includes "is_terminal": true in its <see cref="AITool.AdditionalProperties"/>.
/// </remarks>
public bool IsTerminal { get; set; }

/// <summary>
/// Gets or sets a value controlling whether this tool may be deferred (loaded lazily via tool search) rather than always pre-loaded.
/// </summary>
Expand Down
33 changes: 33 additions & 0 deletions dotnet/test/Unit/ClientSessionLifetimeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,39 @@ public async Task SessionRequests_Serialize_AdditionalDirectories()
value => Assert.Equal("/repo/resumed", value.GetString()));
}

[Fact]
public async Task SessionRequests_Serialize_Terminal_Tools()
{
await using var server = await FakeCopilotServer.StartAsync();
await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });
var terminalTool = CopilotTool.DefineTool(
(Func<string>)(() => "done"),
new CopilotToolOptions { IsTerminal = true });
var plainTool = CopilotTool.DefineTool((Func<string>)(() => "continue"));

await using var created = await client.CreateSessionAsync(new SessionConfig
{
Tools = [terminalTool, plainTool],
OnPermissionRequest = PermissionHandler.ApproveAll
});

var createRequest = Assert.Single(server.Requests, request => request.Method == "session.create");
var createTools = createRequest.Params.GetProperty("tools");
Assert.True(createTools[0].GetProperty("isTerminal").GetBoolean());
Assert.False(createTools[1].TryGetProperty("isTerminal", out _));

server.ClearRequests();

await using var resumed = await client.ResumeSessionAsync("resume-with-terminal-tool", new ResumeSessionConfig
{
Tools = [terminalTool],
OnPermissionRequest = PermissionHandler.ApproveAll
});

var resumeRequest = Assert.Single(server.Requests, request => request.Method == "session.resume");
Assert.True(resumeRequest.Params.GetProperty("tools")[0].GetProperty("isTerminal").GetBoolean());
}

[Fact]
public async Task CreateSessionAsync_Registers_McpAuth_Interest_Only_When_Handler_Configured()
{
Expand Down
22 changes: 22 additions & 0 deletions dotnet/test/Unit/CopilotToolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,28 @@ public void DefineTool_Sets_Name_Description_And_Copilot_Metadata()
Assert.Equal(CopilotToolDefer.Auto, defer);
}

[Fact]
public void DefineTool_Sets_IsTerminal_Metadata()
{
var function = CopilotTool.DefineTool(
ReturnsOk,
new CopilotToolOptions
{
IsTerminal = true
});

Assert.True(function.AdditionalProperties.TryGetValue("is_terminal", out var isTerminal));
Assert.True((bool)isTerminal!);
}

[Fact]
public void DefineTool_Omits_IsTerminal_When_Not_Set()
{
var function = CopilotTool.DefineTool(ReturnsOk);

Assert.False(function.AdditionalProperties.ContainsKey("is_terminal"));
}

[Fact]
public void DefineTool_Omits_Copilot_Metadata_When_Flags_Are_False()
{
Expand Down
37 changes: 37 additions & 0 deletions go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3478,3 +3478,40 @@ func TestResumeSessionRequest_ExpAssignments(t *testing.T) {
}
})
}

func TestIsTerminal(t *testing.T) {
t.Run("IsTerminal is serialized in tool definition", func(t *testing.T) {
tool := Tool{
Name: "clear_context",
Description: "Clear the conversation",
IsTerminal: true,
Handler: func(_ ToolInvocation) (ToolResult, error) { return ToolResult{}, nil },
}
data, err := json.Marshal(tool)
if err != nil {
t.Fatalf("Failed to marshal: %v", err)
}
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("Failed to unmarshal: %v", err)
}
if m["isTerminal"] != true {
t.Errorf("Expected isTerminal to be true, got %v", m["isTerminal"])
}
})

t.Run("IsTerminal is omitted when false", func(t *testing.T) {
tool := Tool{Name: "plain", Description: "A plain tool"}
data, err := json.Marshal(tool)
if err != nil {
t.Fatalf("Failed to marshal: %v", err)
}
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("Failed to unmarshal: %v", err)
}
if _, ok := m["isTerminal"]; ok {
t.Error("Expected isTerminal to be omitted when false")
}
})
}
5 changes: 5 additions & 0 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1472,6 +1472,11 @@ type Tool struct {
Parameters map[string]any `json:"parameters,omitzero"`
OverridesBuiltInTool bool `json:"overridesBuiltInTool,omitempty"`
SkipPermission bool `json:"skipPermission,omitempty"`
// IsTerminal reports that a successful call to this tool ends the agent
// turn: the runtime halts instead of feeding the result back to the model
// for another round. A failed call leaves the loop running so the model can
// read the error and retry.
IsTerminal bool `json:"isTerminal,omitempty"`
// Defer controls whether the tool may be deferred (loaded lazily via tool
// search) rather than always pre-loaded. When empty, the runtime decides.
Defer ToolDefer `json:"defer,omitempty"`
Expand Down
68 changes: 60 additions & 8 deletions java/src/main/java/com/github/copilot/rpc/ToolDefinition.java
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@
* @param metadata
* opaque, host-defined metadata; keys are namespaced and not part of
* the stable public API; {@code null} when unset
* @param isTerminal
* when {@code true}, a successful call to this tool ends the agent
* turn: the runtime's tool phase halts instead of feeding the result
* back to the model for another round; {@code null} or {@code false}
* leaves the turn running
* @see SessionConfig#setTools(java.util.List)
* @see ToolHandler
* @since 1.0.0
Expand All @@ -87,13 +92,13 @@ public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("d
@JsonProperty("parameters") Object parameters, @JsonIgnore ToolHandler handler,
@JsonProperty("overridesBuiltInTool") Boolean overridesBuiltInTool,
@JsonProperty("skipPermission") Boolean skipPermission, @JsonProperty("defer") ToolDefer defer,
@JsonProperty("metadata") Map<String, Object> metadata) {
@JsonProperty("metadata") Map<String, Object> metadata, @JsonProperty("isTerminal") Boolean isTerminal) {
Comment thread
examon marked this conversation as resolved.

/**
* Creates a tool definition without a {@code metadata} bag.
* Creates a tool definition without a {@code metadata} bag or terminality hint.
* <p>
* Convenience overload equivalent to the canonical constructor with
* {@code metadata} set to {@code null}.
* {@code metadata} and {@code isTerminal} set to {@code null}.
*
* @param name
* the unique name of the tool
Expand All @@ -114,7 +119,37 @@ public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("d
*/
public ToolDefinition(String name, String description, Object parameters, ToolHandler handler,
Boolean overridesBuiltInTool, Boolean skipPermission, ToolDefer defer) {
this(name, description, parameters, handler, overridesBuiltInTool, skipPermission, defer, null);
this(name, description, parameters, handler, overridesBuiltInTool, skipPermission, defer, null, null);
}

/**
* Creates a tool definition without a terminality hint.
* <p>
* Convenience overload equivalent to the canonical constructor with
* {@code isTerminal} set to {@code null}.
*
* @param name
* the unique name of the tool
* @param description
* a description of what the tool does
* @param parameters
* the JSON Schema for the tool's parameters
* @param handler
* the handler function to execute when invoked
* @param overridesBuiltInTool
* whether this tool overrides a built-in tool; {@code null} for the
* default
* @param skipPermission
* whether the tool may run without a permission check; {@code null}
* for the default
* @param defer
* the deferral mode; {@code null} lets the runtime decide
* @param metadata
* the opaque, host-defined metadata; {@code null} when unset
*/
public ToolDefinition(String name, String description, Object parameters, ToolHandler handler,
Boolean overridesBuiltInTool, Boolean skipPermission, ToolDefer defer, Map<String, Object> metadata) {
this(name, description, parameters, handler, overridesBuiltInTool, skipPermission, defer, metadata, null);
}

/**
Expand Down Expand Up @@ -304,7 +339,8 @@ public static List<ToolDefinition> fromClass(Class<?> clazz) {
*/
@CopilotExperimental
public ToolDefinition overridesBuiltInTool(boolean value) {
return new ToolDefinition(name, description, parameters, handler, value, skipPermission, defer, metadata);
return new ToolDefinition(name, description, parameters, handler, value, skipPermission, defer, metadata,
isTerminal);
}

/**
Expand All @@ -318,7 +354,8 @@ public ToolDefinition overridesBuiltInTool(boolean value) {
*/
@CopilotExperimental
public ToolDefinition skipPermission(boolean value) {
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, value, defer, metadata);
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, value, defer, metadata,
isTerminal);
}

/**
Expand All @@ -333,7 +370,7 @@ public ToolDefinition skipPermission(boolean value) {
@CopilotExperimental
public ToolDefinition defer(ToolDefer value) {
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, skipPermission, value,
metadata);
metadata, isTerminal);
}

/**
Expand All @@ -348,7 +385,22 @@ public ToolDefinition defer(ToolDefer value) {
@CopilotExperimental
public ToolDefinition metadata(Map<String, Object> value) {
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, skipPermission, defer,
value);
value, isTerminal);
}

/**
* Returns a copy with the {@code isTerminal} flag set.
*
* @param value
* {@code true} to end the agent turn after a successful call to this
* tool
* @return a new {@code ToolDefinition} with the flag applied
* @since 1.0.11
*/
@CopilotExperimental
public ToolDefinition isTerminal(boolean value) {
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, skipPermission, defer,
metadata, value);
}

// ------------------------------------------------------------------
Expand Down
3 changes: 3 additions & 0 deletions java/src/main/java/com/github/copilot/tool/CopilotTool.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
/** Whether to skip permission checks. */
boolean skipPermission() default false;

/** Whether a successful call to this tool ends the agent turn. */
boolean isTerminal() default false;

/** Defer configuration for this tool. */
ToolDefer defer() default ToolDefer.NONE;

Expand Down
Loading
Loading