Skip to content

Commit 756630b

Browse files
committed
Add history.clearContext and Tool.isTerminal across all SDKs
Regenerates the RPC clients for the new `session.history.clearContext` method and the `session.context_cleared` event, and adds a hand-authored `isTerminal` tool flag to every language surface. `isTerminal` lets a tool declare that a successful call ends the agent turn: the runtime's tool phase 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. Without it a turn-ending tool can only approximate the behavior by returning a rejected result, which halts the loop but is semantically wrong. Per language: - Node.js: `Tool.isTerminal`, `defineTool` config, both session-config serialization sites. - Go: `Tool.IsTerminal` with `json:"isTerminal,omitempty"`. - Python: `Tool.is_terminal`, `define_tool` overloads, both client serialization sites. - Rust: `Tool::is_terminal`, skipped when false. - Java: `ToolDefinition.isTerminal` as a record component, plus a seven-argument convenience constructor so existing call sites keep compiling. - .NET: `CopilotToolOptions.IsTerminal`, the `is_terminal` additional-property key, and the wire `ToolDefinition`. Adds serialization tests in Go, Rust and Java covering both the camelCase wire name and omission when unset; the Java test also pins the seven-argument constructor so the record change stays source-compatible.
1 parent f324f72 commit 756630b

16 files changed

Lines changed: 378 additions & 13 deletions

File tree

dotnet/src/Client.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2788,19 +2788,22 @@ internal record ToolDefinition(
27882788
bool? OverridesBuiltInTool = null,
27892789
bool? SkipPermission = null,
27902790
CopilotToolDefer? Defer = null,
2791-
IDictionary<string, JsonNode?>? Metadata = null)
2791+
IDictionary<string, JsonNode?>? Metadata = null,
2792+
bool? IsTerminal = null)
27922793
{
27932794
public static ToolDefinition FromAIFunction(AIFunctionDeclaration function)
27942795
{
27952796
var overrides = function.AdditionalProperties.TryGetValue(CopilotTool.OverridesBuiltInToolKey, out var val) && val is true;
27962797
var skipPerm = function.AdditionalProperties.TryGetValue(CopilotTool.SkipPermissionKey, out var skipVal) && skipVal is true;
27972798
var defer = function.AdditionalProperties.TryGetValue(CopilotTool.DeferKey, out var deferVal) && deferVal is CopilotToolDefer d ? d : (CopilotToolDefer?)null;
27982799
var metadata = function.AdditionalProperties.TryGetValue(CopilotTool.MetadataKey, out var metaVal) && metaVal is IDictionary<string, JsonNode?> m ? m : null;
2800+
var isTerminal = function.AdditionalProperties.TryGetValue(CopilotTool.IsTerminalKey, out var terminalVal) && terminalVal is true;
27992801
return new ToolDefinition(function.Name, function.Description, function.JsonSchema,
28002802
overrides ? true : null,
28012803
skipPerm ? true : null,
28022804
defer,
2803-
metadata);
2805+
metadata,
2806+
isTerminal ? true : null);
28042807
}
28052808
}
28062809

dotnet/src/CopilotTool.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ public static class CopilotTool
1818
/// <summary>The key used in <see cref="AITool.AdditionalProperties"/> to indicate that a tool can execute without a permission prompt.</summary>
1919
internal const string SkipPermissionKey = "skip_permission";
2020

21+
/// <summary>The key used in <see cref="AITool.AdditionalProperties"/> to indicate that a successful call to the tool ends the agent turn.</summary>
22+
internal const string IsTerminalKey = "is_terminal";
23+
2124
/// <summary>The key used in <see cref="AITool.AdditionalProperties"/> to carry the tool's <see cref="CopilotToolDefer"/> deferral mode.</summary>
2225
internal const string DeferKey = "defer";
2326

@@ -91,7 +94,7 @@ static void ApplyToolInvocationBinding(AIFunctionFactoryOptions factoryOptions)
9194

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

118+
if (toolOptions.IsTerminal)
119+
{
120+
additionalProperties[IsTerminalKey] = true;
121+
}
122+
115123
if (toolOptions.Defer is { } defer)
116124
{
117125
additionalProperties[DeferKey] = defer;
@@ -152,6 +160,16 @@ public sealed class CopilotToolOptions
152160
/// </remarks>
153161
public bool SkipPermission { get; set; }
154162

163+
/// <summary>
164+
/// Gets or sets a value indicating whether a successful call to this tool ends the agent turn.
165+
/// </summary>
166+
/// <remarks>
167+
/// When true, the runtime's tool phase halts after a successful call instead of feeding the result back to the
168+
/// model for another round. A failed call leaves the loop running so the model can read the error and retry.
169+
/// The resulting <see cref="AIFunction"/> includes "is_terminal": true in its <see cref="AITool.AdditionalProperties"/>.
170+
/// </remarks>
171+
public bool IsTerminal { get; set; }
172+
155173
/// <summary>
156174
/// Gets or sets a value controlling whether this tool may be deferred (loaded lazily via tool search) rather than always pre-loaded.
157175
/// </summary>

dotnet/src/Generated/SessionEvents.cs

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dotnet/test/Unit/CopilotToolTests.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,28 @@ public void DefineTool_Sets_Name_Description_And_Copilot_Metadata()
3434
Assert.Equal(CopilotToolDefer.Auto, defer);
3535
}
3636

37+
[Fact]
38+
public void DefineTool_Sets_IsTerminal_Metadata()
39+
{
40+
var function = CopilotTool.DefineTool(
41+
ReturnsOk,
42+
new CopilotToolOptions
43+
{
44+
IsTerminal = true
45+
});
46+
47+
Assert.True(function.AdditionalProperties.TryGetValue("is_terminal", out var isTerminal));
48+
Assert.True((bool)isTerminal!);
49+
}
50+
51+
[Fact]
52+
public void DefineTool_Omits_IsTerminal_When_Not_Set()
53+
{
54+
var function = CopilotTool.DefineTool(ReturnsOk);
55+
56+
Assert.False(function.AdditionalProperties.ContainsKey("is_terminal"));
57+
}
58+
3759
[Fact]
3860
public void DefineTool_Omits_Copilot_Metadata_When_Flags_Are_False()
3961
{

go/client_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3478,3 +3478,40 @@ func TestResumeSessionRequest_ExpAssignments(t *testing.T) {
34783478
}
34793479
})
34803480
}
3481+
3482+
func TestIsTerminal(t *testing.T) {
3483+
t.Run("IsTerminal is serialized in tool definition", func(t *testing.T) {
3484+
tool := Tool{
3485+
Name: "clear_context",
3486+
Description: "Clear the conversation",
3487+
IsTerminal: true,
3488+
Handler: func(_ ToolInvocation) (ToolResult, error) { return ToolResult{}, nil },
3489+
}
3490+
data, err := json.Marshal(tool)
3491+
if err != nil {
3492+
t.Fatalf("Failed to marshal: %v", err)
3493+
}
3494+
var m map[string]any
3495+
if err := json.Unmarshal(data, &m); err != nil {
3496+
t.Fatalf("Failed to unmarshal: %v", err)
3497+
}
3498+
if m["isTerminal"] != true {
3499+
t.Errorf("Expected isTerminal to be true, got %v", m["isTerminal"])
3500+
}
3501+
})
3502+
3503+
t.Run("IsTerminal is omitted when false", func(t *testing.T) {
3504+
tool := Tool{Name: "plain", Description: "A plain tool"}
3505+
data, err := json.Marshal(tool)
3506+
if err != nil {
3507+
t.Fatalf("Failed to marshal: %v", err)
3508+
}
3509+
var m map[string]any
3510+
if err := json.Unmarshal(data, &m); err != nil {
3511+
t.Fatalf("Failed to unmarshal: %v", err)
3512+
}
3513+
if _, ok := m["isTerminal"]; ok {
3514+
t.Error("Expected isTerminal to be omitted when false")
3515+
}
3516+
})
3517+
}

go/types.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1472,6 +1472,11 @@ type Tool struct {
14721472
Parameters map[string]any `json:"parameters,omitzero"`
14731473
OverridesBuiltInTool bool `json:"overridesBuiltInTool,omitempty"`
14741474
SkipPermission bool `json:"skipPermission,omitempty"`
1475+
// IsTerminal reports that a successful call to this tool ends the agent
1476+
// turn: the runtime halts instead of feeding the result back to the model
1477+
// for another round. A failed call leaves the loop running so the model can
1478+
// read the error and retry.
1479+
IsTerminal bool `json:"isTerminal,omitempty"`
14751480
// Defer controls whether the tool may be deferred (loaded lazily via tool
14761481
// search) rather than always pre-loaded. When empty, the runtime decides.
14771482
Defer ToolDefer `json:"defer,omitempty"`

java/src/generated/java/com/github/copilot/generated/SessionEvent.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,8 @@
139139
@JsonSubTypes.Type(value = SessionCanvasRecordedEvent.class, name = "session.canvas.recorded"),
140140
@JsonSubTypes.Type(value = SessionCanvasRemovedEvent.class, name = "session.canvas.removed"),
141141
@JsonSubTypes.Type(value = SessionExtensionsAttachmentsPushedEvent.class, name = "session.extensions.attachments_pushed"),
142-
@JsonSubTypes.Type(value = McpAppToolCallCompleteEvent.class, name = "mcp_app.tool_call_complete")
142+
@JsonSubTypes.Type(value = McpAppToolCallCompleteEvent.class, name = "mcp_app.tool_call_complete"),
143+
@JsonSubTypes.Type(value = SessionContextClearedEvent.class, name = "session.context_cleared")
143144
})
144145
@javax.annotation.processing.Generated("copilot-sdk-codegen")
145146
public abstract sealed class SessionEvent permits
@@ -258,6 +259,7 @@ public abstract sealed class SessionEvent permits
258259
SessionCanvasRemovedEvent,
259260
SessionExtensionsAttachmentsPushedEvent,
260261
McpAppToolCallCompleteEvent,
262+
SessionContextClearedEvent,
261263
UnknownSessionEvent {
262264

263265
/** Unique event identifier (UUID v4), generated when the event is emitted. */

java/src/main/java/com/github/copilot/rpc/ToolDefinition.java

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@
7878
* @param metadata
7979
* opaque, host-defined metadata; keys are namespaced and not part of
8080
* the stable public API; {@code null} when unset
81+
* @param isTerminal
82+
* when {@code true}, a successful call to this tool ends the agent
83+
* turn: the runtime's tool phase halts instead of feeding the result
84+
* back to the model for another round; {@code null} or {@code false}
85+
* leaves the turn running
8186
* @see SessionConfig#setTools(java.util.List)
8287
* @see ToolHandler
8388
* @since 1.0.0
@@ -87,13 +92,14 @@ public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("d
8792
@JsonProperty("parameters") Object parameters, @JsonIgnore ToolHandler handler,
8893
@JsonProperty("overridesBuiltInTool") Boolean overridesBuiltInTool,
8994
@JsonProperty("skipPermission") Boolean skipPermission, @JsonProperty("defer") ToolDefer defer,
90-
@JsonProperty("metadata") Map<String, Object> metadata) {
95+
@JsonProperty("metadata") Map<String, Object> metadata, @JsonProperty("isTerminal") Boolean isTerminal) {
9196

9297
/**
93-
* Creates a tool definition without a {@code metadata} bag.
98+
* Creates a tool definition without a {@code metadata} bag or terminality
99+
* hint.
94100
* <p>
95101
* Convenience overload equivalent to the canonical constructor with
96-
* {@code metadata} set to {@code null}.
102+
* {@code metadata} and {@code isTerminal} set to {@code null}.
97103
*
98104
* @param name
99105
* the unique name of the tool
@@ -114,7 +120,37 @@ public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("d
114120
*/
115121
public ToolDefinition(String name, String description, Object parameters, ToolHandler handler,
116122
Boolean overridesBuiltInTool, Boolean skipPermission, ToolDefer defer) {
117-
this(name, description, parameters, handler, overridesBuiltInTool, skipPermission, defer, null);
123+
this(name, description, parameters, handler, overridesBuiltInTool, skipPermission, defer, null, null);
124+
}
125+
126+
/**
127+
* Creates a tool definition without a terminality hint.
128+
* <p>
129+
* Convenience overload equivalent to the canonical constructor with
130+
* {@code isTerminal} set to {@code null}.
131+
*
132+
* @param name
133+
* the unique name of the tool
134+
* @param description
135+
* a description of what the tool does
136+
* @param parameters
137+
* the JSON Schema for the tool's parameters
138+
* @param handler
139+
* the handler function to execute when invoked
140+
* @param overridesBuiltInTool
141+
* whether this tool overrides a built-in tool; {@code null} for the
142+
* default
143+
* @param skipPermission
144+
* whether the tool may run without a permission check; {@code null}
145+
* for the default
146+
* @param defer
147+
* the deferral mode; {@code null} lets the runtime decide
148+
* @param metadata
149+
* the opaque, host-defined metadata; {@code null} when unset
150+
*/
151+
public ToolDefinition(String name, String description, Object parameters, ToolHandler handler,
152+
Boolean overridesBuiltInTool, Boolean skipPermission, ToolDefer defer, Map<String, Object> metadata) {
153+
this(name, description, parameters, handler, overridesBuiltInTool, skipPermission, defer, metadata, null);
118154
}
119155

120156
/**
@@ -304,7 +340,8 @@ public static List<ToolDefinition> fromClass(Class<?> clazz) {
304340
*/
305341
@CopilotExperimental
306342
public ToolDefinition overridesBuiltInTool(boolean value) {
307-
return new ToolDefinition(name, description, parameters, handler, value, skipPermission, defer, metadata);
343+
return new ToolDefinition(name, description, parameters, handler, value, skipPermission, defer, metadata,
344+
isTerminal);
308345
}
309346

310347
/**
@@ -318,7 +355,8 @@ public ToolDefinition overridesBuiltInTool(boolean value) {
318355
*/
319356
@CopilotExperimental
320357
public ToolDefinition skipPermission(boolean value) {
321-
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, value, defer, metadata);
358+
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, value, defer, metadata,
359+
isTerminal);
322360
}
323361

324362
/**
@@ -333,7 +371,7 @@ public ToolDefinition skipPermission(boolean value) {
333371
@CopilotExperimental
334372
public ToolDefinition defer(ToolDefer value) {
335373
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, skipPermission, value,
336-
metadata);
374+
metadata, isTerminal);
337375
}
338376

339377
/**
@@ -348,7 +386,22 @@ public ToolDefinition defer(ToolDefer value) {
348386
@CopilotExperimental
349387
public ToolDefinition metadata(Map<String, Object> value) {
350388
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, skipPermission, defer,
351-
value);
389+
value, isTerminal);
390+
}
391+
392+
/**
393+
* Returns a copy with the {@code isTerminal} flag set.
394+
*
395+
* @param value
396+
* {@code true} to end the agent turn after a successful call to
397+
* this tool
398+
* @return a new {@code ToolDefinition} with the flag applied
399+
* @since 1.0.7
400+
*/
401+
@CopilotExperimental
402+
public ToolDefinition isTerminal(boolean value) {
403+
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, skipPermission, defer,
404+
metadata, value);
352405
}
353406

354407
// ------------------------------------------------------------------
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
package com.github.copilot.rpc;
5+
6+
import static org.junit.jupiter.api.Assertions.assertEquals;
7+
import static org.junit.jupiter.api.Assertions.assertFalse;
8+
import static org.junit.jupiter.api.Assertions.assertTrue;
9+
10+
import com.fasterxml.jackson.databind.JsonNode;
11+
import com.fasterxml.jackson.databind.ObjectMapper;
12+
import java.util.Map;
13+
import org.junit.jupiter.api.Test;
14+
15+
/** Wire-level coverage for {@link ToolDefinition#isTerminal()}. */
16+
class ToolDefinitionIsTerminalTest {
17+
18+
private static final ObjectMapper MAPPER = new ObjectMapper();
19+
20+
@Test
21+
void isTerminalSerializesAsCamelCaseWhenSet() throws Exception {
22+
ToolDefinition definition = new ToolDefinition("clear_context", "Clear the conversation",
23+
Map.of("type", "object"), null, null, null, null, null, true);
24+
25+
JsonNode node = MAPPER.valueToTree(definition);
26+
27+
assertTrue(node.has("isTerminal"), "isTerminal should be serialized");
28+
assertTrue(node.get("isTerminal").asBoolean(), "isTerminal should be true");
29+
}
30+
31+
@Test
32+
void isTerminalIsOmittedWhenNull() throws Exception {
33+
ToolDefinition definition = new ToolDefinition("plain", "A plain tool", Map.of("type", "object"), null, null,
34+
null, null, null, null);
35+
36+
JsonNode node = MAPPER.valueToTree(definition);
37+
38+
assertFalse(node.has("isTerminal"), "isTerminal should be omitted when null");
39+
}
40+
41+
@Test
42+
void sevenArgumentConstructorStillCompilesAndLeavesTerminalityUnset() throws Exception {
43+
// Guards source compatibility for call sites written before isTerminal
44+
// was added as a record component.
45+
ToolDefinition definition = new ToolDefinition("legacy", "Legacy call site", Map.of("type", "object"), null,
46+
null, null, null);
47+
48+
assertEquals(null, definition.isTerminal());
49+
assertFalse(MAPPER.valueToTree(definition).has("isTerminal"));
50+
}
51+
}

0 commit comments

Comments
 (0)