From 756630b1015e691750650d60a249e2f3370e292d Mon Sep 17 00:00:00 2001 From: examon Date: Wed, 29 Jul 2026 15:54:42 +0200 Subject: [PATCH 1/5] 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. --- dotnet/src/Client.cs | 7 +- dotnet/src/CopilotTool.cs | 20 +++++- dotnet/src/Generated/SessionEvents.cs | 31 +++++++++ dotnet/test/Unit/CopilotToolTests.cs | 22 ++++++ go/client_test.go | 37 ++++++++++ go/types.go | 5 ++ .../copilot/generated/SessionEvent.java | 4 +- .../github/copilot/rpc/ToolDefinition.java | 69 ++++++++++++++++--- .../rpc/ToolDefinitionIsTerminalTest.java | 51 ++++++++++++++ nodejs/src/client.ts | 2 + nodejs/src/generated/session-events.ts | 50 +++++++++++++- nodejs/src/types.ts | 12 ++++ python/copilot/client.py | 4 ++ python/copilot/tools.py | 15 ++++ rust/src/generated/session_events.rs | 18 +++++ rust/src/types.rs | 44 ++++++++++++ 16 files changed, 378 insertions(+), 13 deletions(-) create mode 100644 java/src/test/java/com/github/copilot/rpc/ToolDefinitionIsTerminalTest.java diff --git a/dotnet/src/Client.cs b/dotnet/src/Client.cs index b1199dac8a..d2a455d2f3 100644 --- a/dotnet/src/Client.cs +++ b/dotnet/src/Client.cs @@ -2788,7 +2788,8 @@ internal record ToolDefinition( bool? OverridesBuiltInTool = null, bool? SkipPermission = null, CopilotToolDefer? Defer = null, - IDictionary? Metadata = null) + IDictionary? Metadata = null, + bool? IsTerminal = null) { public static ToolDefinition FromAIFunction(AIFunctionDeclaration function) { @@ -2796,11 +2797,13 @@ public static ToolDefinition FromAIFunction(AIFunctionDeclaration function) 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 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); } } diff --git a/dotnet/src/CopilotTool.cs b/dotnet/src/CopilotTool.cs index e22296bccd..ca62ccc5d7 100644 --- a/dotnet/src/CopilotTool.cs +++ b/dotnet/src/CopilotTool.cs @@ -18,6 +18,9 @@ public static class CopilotTool /// The key used in to indicate that a tool can execute without a permission prompt. internal const string SkipPermissionKey = "skip_permission"; + /// The key used in to indicate that a successful call to the tool ends the agent turn. + internal const string IsTerminalKey = "is_terminal"; + /// The key used in to carry the tool's deferral mode. internal const string DeferKey = "defer"; @@ -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 additionalProperties = new(StringComparer.Ordinal); if (factoryOptions.AdditionalProperties is not null) @@ -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; @@ -152,6 +160,16 @@ public sealed class CopilotToolOptions /// public bool SkipPermission { get; set; } + /// + /// Gets or sets a value indicating whether a successful call to this tool ends the agent turn. + /// + /// + /// 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 includes "is_terminal": true in its . + /// + public bool IsTerminal { get; set; } + /// /// Gets or sets a value controlling whether this tool may be deferred (loaded lazily via tool search) rather than always pre-loaded. /// diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 6b81a4770d..2e05bd5418 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -1687,6 +1687,19 @@ public sealed partial class McpAppToolCallCompleteEvent : SessionEvent public required McpAppToolCallCompleteData Data { get; set; } } +/// Context-cleared details emitted when the clear_context tool resets the conversation. +/// Represents the session.context_cleared event. +public sealed partial class SessionContextClearedEvent : SessionEvent +{ + /// + [JsonIgnore] + public override string Type => "session.context_cleared"; + + /// The session.context_cleared event payload. + [JsonPropertyName("data")] + public required SessionContextClearedData Data { get; set; } +} + /// Session initialization metadata including context and configuration. public sealed partial class SessionStartData { @@ -4709,6 +4722,24 @@ public sealed partial class McpAppToolCallCompleteData public required string ToolName { get; set; } } +/// Context-cleared details emitted when the clear_context tool resets the conversation. +public sealed partial class SessionContextClearedData +{ + /// Optional initial message set after clearing. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("initialMessage")] + public string? InitialMessage { get; set; } + + /// Number of conversation messages that were cleared. + [JsonPropertyName("messagesCleared")] + public required long MessagesCleared { get; set; } + + /// Runtime-injected messages re-seeded into the freshly-cleared context (e.g. self-paced loop wrappers). Persisted so a resumed session reproduces the same post-clear window instead of resurrecting the pre-clear history. + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("prependMessages")] + public string[]? PrependMessages { get; set; } +} + /// Working directory and git context at session start. /// Nested data type for WorkingDirectoryContext. public sealed partial class WorkingDirectoryContext diff --git a/dotnet/test/Unit/CopilotToolTests.cs b/dotnet/test/Unit/CopilotToolTests.cs index c3f5861492..19fa6258be 100644 --- a/dotnet/test/Unit/CopilotToolTests.cs +++ b/dotnet/test/Unit/CopilotToolTests.cs @@ -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() { diff --git a/go/client_test.go b/go/client_test.go index 14131bc4ce..baeb77575a 100644 --- a/go/client_test.go +++ b/go/client_test.go @@ -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") + } + }) +} diff --git a/go/types.go b/go/types.go index 3ddcf930dc..28d3087f56 100644 --- a/go/types.go +++ b/go/types.go @@ -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"` diff --git a/java/src/generated/java/com/github/copilot/generated/SessionEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionEvent.java index 582ecd3d4f..1292a5a3e2 100644 --- a/java/src/generated/java/com/github/copilot/generated/SessionEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/SessionEvent.java @@ -139,7 +139,8 @@ @JsonSubTypes.Type(value = SessionCanvasRecordedEvent.class, name = "session.canvas.recorded"), @JsonSubTypes.Type(value = SessionCanvasRemovedEvent.class, name = "session.canvas.removed"), @JsonSubTypes.Type(value = SessionExtensionsAttachmentsPushedEvent.class, name = "session.extensions.attachments_pushed"), - @JsonSubTypes.Type(value = McpAppToolCallCompleteEvent.class, name = "mcp_app.tool_call_complete") + @JsonSubTypes.Type(value = McpAppToolCallCompleteEvent.class, name = "mcp_app.tool_call_complete"), + @JsonSubTypes.Type(value = SessionContextClearedEvent.class, name = "session.context_cleared") }) @javax.annotation.processing.Generated("copilot-sdk-codegen") public abstract sealed class SessionEvent permits @@ -258,6 +259,7 @@ public abstract sealed class SessionEvent permits SessionCanvasRemovedEvent, SessionExtensionsAttachmentsPushedEvent, McpAppToolCallCompleteEvent, + SessionContextClearedEvent, UnknownSessionEvent { /** Unique event identifier (UUID v4), generated when the event is emitted. */ diff --git a/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java b/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java index ccf0ef5309..67616c71c9 100644 --- a/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java +++ b/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java @@ -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 @@ -87,13 +92,14 @@ 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 metadata) { + @JsonProperty("metadata") Map metadata, @JsonProperty("isTerminal") Boolean isTerminal) { /** - * Creates a tool definition without a {@code metadata} bag. + * Creates a tool definition without a {@code metadata} bag or terminality + * hint. *

* 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 @@ -114,7 +120,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. + *

+ * 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 metadata) { + this(name, description, parameters, handler, overridesBuiltInTool, skipPermission, defer, metadata, null); } /** @@ -304,7 +340,8 @@ public static List 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); } /** @@ -318,7 +355,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); } /** @@ -333,7 +371,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); } /** @@ -348,7 +386,22 @@ public ToolDefinition defer(ToolDefer value) { @CopilotExperimental public ToolDefinition metadata(Map 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.7 + */ + @CopilotExperimental + public ToolDefinition isTerminal(boolean value) { + return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, skipPermission, defer, + metadata, value); } // ------------------------------------------------------------------ diff --git a/java/src/test/java/com/github/copilot/rpc/ToolDefinitionIsTerminalTest.java b/java/src/test/java/com/github/copilot/rpc/ToolDefinitionIsTerminalTest.java new file mode 100644 index 0000000000..850dfa251f --- /dev/null +++ b/java/src/test/java/com/github/copilot/rpc/ToolDefinitionIsTerminalTest.java @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ +package com.github.copilot.rpc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Map; +import org.junit.jupiter.api.Test; + +/** Wire-level coverage for {@link ToolDefinition#isTerminal()}. */ +class ToolDefinitionIsTerminalTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + void isTerminalSerializesAsCamelCaseWhenSet() throws Exception { + ToolDefinition definition = new ToolDefinition("clear_context", "Clear the conversation", + Map.of("type", "object"), null, null, null, null, null, true); + + JsonNode node = MAPPER.valueToTree(definition); + + assertTrue(node.has("isTerminal"), "isTerminal should be serialized"); + assertTrue(node.get("isTerminal").asBoolean(), "isTerminal should be true"); + } + + @Test + void isTerminalIsOmittedWhenNull() throws Exception { + ToolDefinition definition = new ToolDefinition("plain", "A plain tool", Map.of("type", "object"), null, null, + null, null, null, null); + + JsonNode node = MAPPER.valueToTree(definition); + + assertFalse(node.has("isTerminal"), "isTerminal should be omitted when null"); + } + + @Test + void sevenArgumentConstructorStillCompilesAndLeavesTerminalityUnset() throws Exception { + // Guards source compatibility for call sites written before isTerminal + // was added as a record component. + ToolDefinition definition = new ToolDefinition("legacy", "Legacy call site", Map.of("type", "object"), null, + null, null, null); + + assertEquals(null, definition.isTerminal()); + assertFalse(MAPPER.valueToTree(definition).has("isTerminal")); + } +} diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 3e100edddd..d887065376 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -1535,6 +1535,7 @@ export class CopilotClient { skipPermission: tool.skipPermission, defer: tool.defer, metadata: tool.metadata, + isTerminal: tool.isTerminal, })), toolSearch: config.toolSearch, canvases: config.canvases?.map((canvas) => canvas.declaration), @@ -1784,6 +1785,7 @@ export class CopilotClient { skipPermission: tool.skipPermission, defer: tool.defer, metadata: tool.metadata, + isTerminal: tool.isTerminal, })), toolSearch: config.toolSearch, canvases: config.canvases?.map((canvas) => canvas.declaration), diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 1dad4ab166..07cfceadef 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -119,7 +119,8 @@ export type SessionEvent = | CanvasRecordedEvent | CanvasRemovedEvent | ExtensionsAttachmentsPushedEvent - | McpAppToolCallCompleteEvent; + | McpAppToolCallCompleteEvent + | ContextClearedEvent; /** * Hosting platform type of the repository (github or ado) */ @@ -10013,3 +10014,50 @@ export interface McpAppToolCallCompleteToolMetaUI { */ visibility?: string[]; } +/** + * Session event "session.context_cleared". Context-cleared details emitted when the clear_context tool resets the conversation + */ +export interface ContextClearedEvent { + /** + * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. + */ + agentId?: string; + data: ContextClearedData; + /** + * When true, the event is transient and not persisted to the session event log on disk + */ + ephemeral?: boolean; + /** + * Unique event identifier (UUID v4), generated when the event is emitted + */ + id: string; + /** + * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. + */ + parentId: string | null; + /** + * ISO 8601 timestamp when the event was created + */ + timestamp: string; + /** + * Type discriminator. Always "session.context_cleared". + */ + type: "session.context_cleared"; +} +/** + * Context-cleared details emitted when the clear_context tool resets the conversation + */ +export interface ContextClearedData { + /** + * Optional initial message set after clearing + */ + initialMessage?: string; + /** + * Number of conversation messages that were cleared + */ + messagesCleared: number; + /** + * Runtime-injected messages re-seeded into the freshly-cleared context (e.g. self-paced loop wrappers). Persisted so a resumed session reproduces the same post-clear window instead of resurrecting the pre-clear history. + */ + prependMessages?: string[]; +} diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index a8a9410f84..bec38657dd 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -656,6 +656,17 @@ export interface Tool { * Unknown keys are preserved and round-tripped untouched. */ metadata?: Record; + /** + * When true, a successful call to this tool ends the agent turn: the runtime's + * tool phase halts instead of feeding the tool result back to the model for + * another round. A failed call (for example input validation) leaves the loop + * running so the model can read the error and retry. + * + * Use this for tools whose whole purpose is to terminate the turn, such as a + * context clear that replaces the conversation the model would otherwise + * continue from. + */ + isTerminal?: boolean; } /** @@ -672,6 +683,7 @@ export function defineTool( skipPermission?: boolean; defer?: "auto" | "never"; metadata?: Record; + isTerminal?: boolean; } ): Tool { return { name, ...config }; diff --git a/python/copilot/client.py b/python/copilot/client.py index 737619ef35..ee5bb258c2 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -2277,6 +2277,8 @@ async def create_session( definition["defer"] = tool.defer if tool.metadata is not None: definition["metadata"] = tool.metadata + if tool.is_terminal: + definition["isTerminal"] = True tool_defs.append(definition) # Empty-mode validation and normalization @@ -2980,6 +2982,8 @@ async def resume_session( definition["defer"] = tool.defer if tool.metadata is not None: definition["metadata"] = tool.metadata + if tool.is_terminal: + definition["isTerminal"] = True tool_defs.append(definition) # Empty-mode validation and normalization diff --git a/python/copilot/tools.py b/python/copilot/tools.py index de81fe7fd8..dc709cf7d5 100644 --- a/python/copilot/tools.py +++ b/python/copilot/tools.py @@ -82,6 +82,11 @@ class Tool: skip_permission: bool = False defer: Literal["auto", "never"] | None = None metadata: dict[str, Any] | None = None + #: When true, 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. + is_terminal: bool = False T = TypeVar("T", bound=BaseModel) @@ -97,6 +102,7 @@ def define_tool( skip_permission: bool = False, defer: Literal["auto", "never"] | None = None, metadata: dict[str, Any] | None = None, + is_terminal: bool = False, ) -> Callable[[Callable[..., Any]], Tool]: pass @@ -112,6 +118,7 @@ def define_tool( skip_permission: bool = False, defer: Literal["auto", "never"] | None = None, metadata: dict[str, Any] | None = None, + is_terminal: bool = False, ) -> Tool: pass @@ -127,6 +134,7 @@ def define_tool( skip_permission: bool = False, defer: Literal["auto", "never"] | None = None, metadata: dict[str, Any] | None = None, + is_terminal: bool = False, ) -> Tool: pass @@ -141,6 +149,7 @@ def define_tool( skip_permission: bool = False, defer: Literal["auto", "never"] | None = None, metadata: dict[str, Any] | None = None, + is_terminal: bool = False, ) -> Tool | Callable[[Callable[[Any, ToolInvocation], Any]], Tool]: """ Define a tool with automatic JSON schema generation from Pydantic models. @@ -193,6 +202,10 @@ def lookup_issue(params: LookupIssueParams) -> str: Keys are namespaced and not part of the stable public API; values are not interpreted and may be recognized to inform host-specific behavior. Unknown keys are preserved. + is_terminal: When True, 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. Returns: A Tool instance @@ -288,6 +301,7 @@ async def wrapped_handler(invocation: ToolInvocation) -> ToolResult: skip_permission=skip_permission, defer=defer, metadata=metadata, + is_terminal=is_terminal, ) # If handler is provided, call decorator immediately @@ -308,6 +322,7 @@ async def wrapped_handler(invocation: ToolInvocation) -> ToolResult: skip_permission=skip_permission, defer=defer, metadata=metadata, + is_terminal=is_terminal, ) # Otherwise return decorator for @define_tool(...) usage diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index ef3c395b10..abc51e1112 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -318,6 +318,8 @@ pub enum SessionEventType { SessionExtensionsAttachmentsPushed, #[serde(rename = "mcp_app.tool_call_complete")] McpAppToolCallComplete, + #[serde(rename = "session.context_cleared")] + SessionContextCleared, /// Unknown event type for forward compatibility. #[default] #[serde(other)] @@ -630,6 +632,8 @@ pub enum SessionEventData { SessionExtensionsAttachmentsPushed(SessionExtensionsAttachmentsPushedData), #[serde(rename = "mcp_app.tool_call_complete")] McpAppToolCallComplete(McpAppToolCallCompleteData), + #[serde(rename = "session.context_cleared")] + SessionContextCleared(SessionContextClearedData), } /// A session event with typed data payload. @@ -5018,6 +5022,20 @@ pub struct McpAppToolCallCompleteData { pub tool_name: String, } +/// Session event "session.context_cleared". Context-cleared details emitted when the clear_context tool resets the conversation +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionContextClearedData { + /// Optional initial message set after clearing + #[serde(skip_serializing_if = "Option::is_none")] + pub initial_message: Option, + /// Number of conversation messages that were cleared + pub messages_cleared: i64, + /// Runtime-injected messages re-seeded into the freshly-cleared context (e.g. self-paced loop wrappers). Persisted so a resumed session reproduces the same post-clear window instead of resurrecting the pre-clear history. + #[serde(skip_serializing_if = "Option::is_none")] + pub prepend_messages: Option>, +} + /// Hosting platform type of the repository (github or ado) #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum WorkingDirectoryContextHostType { diff --git a/rust/src/types.rs b/rust/src/types.rs index d2b8dcb93a..bee5e854d0 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -346,6 +346,12 @@ pub struct Tool { /// access control. #[serde(default, skip_serializing_if = "is_false")] pub skip_permission: bool, + /// When `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. A failed call leaves the loop running so the + /// model can read the error and retry. + #[serde(default, skip_serializing_if = "is_false")] + pub is_terminal: bool, /// Controls whether the tool may be deferred (loaded lazily via tool /// search) rather than always pre-loaded. When [`DeferMode::Auto`], the /// tool can be deferred and surfaced through tool search. When @@ -470,6 +476,18 @@ impl Tool { self } + /// Sets whether a successful call to this tool ends the agent turn. + /// + /// 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. + #[must_use] + pub fn with_is_terminal(mut self, is_terminal: bool) -> Self { + self.is_terminal = is_terminal; + self + } + /// Set the deferral mode controlling whether the tool may be loaded /// lazily via tool search ([`DeferMode::Auto`]) or always pre-loaded /// ([`DeferMode::Never`]). @@ -7516,3 +7534,29 @@ mod permission_builder_tests { assert!(json.get("isExperimentalMode").is_none()); } } + +#[cfg(test)] +mod is_terminal_tests { + use super::Tool; + + #[test] + fn is_terminal_serializes_as_camel_case_when_set() { + let tool = Tool { + name: "clear_context".to_owned(), + is_terminal: true, + ..Default::default() + }; + let value = serde_json::to_value(&tool).expect("tool serializes"); + assert_eq!(value.get("isTerminal"), Some(&serde_json::Value::Bool(true))); + } + + #[test] + fn is_terminal_is_omitted_when_false() { + let tool = Tool { + name: "plain".to_owned(), + ..Default::default() + }; + let value = serde_json::to_value(&tool).expect("tool serializes"); + assert!(value.get("isTerminal").is_none()); + } +} From 1ca9787ce17e76ad5aa7f62d94af941343668940 Mon Sep 17 00:00:00 2001 From: examon Date: Wed, 29 Jul 2026 21:20:14 +0000 Subject: [PATCH 2/5] Preserve isTerminal in Java fluent copies, add Node isTerminal wire tests Resolves the rebase onto main where both sides added an eighth tool option: main added metadata and this branch added isTerminal. - Java ToolDefinition now carries metadata and isTerminal as separate record components, keeps the seven- and eight-argument convenience constructors, and threads isTerminal through every fluent copy method so it is no longer dropped by .metadata()/.defer()/etc. - Adds ToolDefinition.isTerminal(boolean) so lambda-defined tools can set it, matching the other flags. - Adds the missing Node regression tests asserting isTerminal is forwarded on both session.create and session.resume, and omitted when unset. - Applies the repo rust formatter to the new is_terminal test. --- .../github/copilot/rpc/ToolDefinition.java | 7 +-- nodejs/test/client.test.ts | 62 +++++++++++++++++++ rust/src/types.rs | 5 +- 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java b/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java index 67616c71c9..05ad1b9cf6 100644 --- a/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java +++ b/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java @@ -95,8 +95,7 @@ public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("d @JsonProperty("metadata") Map metadata, @JsonProperty("isTerminal") Boolean isTerminal) { /** - * Creates a tool definition without a {@code metadata} bag or terminality - * hint. + * Creates a tool definition without a {@code metadata} bag or terminality hint. *

* Convenience overload equivalent to the canonical constructor with * {@code metadata} and {@code isTerminal} set to {@code null}. @@ -393,8 +392,8 @@ public ToolDefinition metadata(Map value) { * 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 + * {@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.7 */ diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index bbe6fbe666..f667932ad4 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -714,6 +714,68 @@ describe("CopilotClient", () => { expect(createPayload.tools[0].metadata).toBeUndefined(); }); + it("forwards tool isTerminal in session.create and session.resume", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + + const tool = { + name: "clear_context", + description: "Clears the conversation", + parameters: { type: "object", properties: {} }, + isTerminal: true, + }; + + const session = await client.createSession({ + onPermissionRequest: approveAll, + tools: [tool], + }); + await client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + tools: [tool], + }); + + const createPayload = spy.mock.calls.find( + ([method]) => method === "session.create" + )![1] as any; + const resumePayload = spy.mock.calls.find( + ([method]) => method === "session.resume" + )![1] as any; + expect(createPayload.tools[0].isTerminal).toBe(true); + expect(resumePayload.tools[0].isTerminal).toBe(true); + }); + + it("omits tool isTerminal from session.create when unset", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + + await client.createSession({ + onPermissionRequest: approveAll, + tools: [{ name: "my_tool", description: "a tool" }], + }); + + const createPayload = spy.mock.calls.find( + ([method]) => method === "session.create" + )![1] as any; + expect(createPayload.tools[0].isTerminal).toBeUndefined(); + }); + it("forwards new session options in session.create and session.resume", async () => { const client = new CopilotClient(); await client.start(); diff --git a/rust/src/types.rs b/rust/src/types.rs index bee5e854d0..fd22efc9cc 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -7547,7 +7547,10 @@ mod is_terminal_tests { ..Default::default() }; let value = serde_json::to_value(&tool).expect("tool serializes"); - assert_eq!(value.get("isTerminal"), Some(&serde_json::Value::Bool(true))); + assert_eq!( + value.get("isTerminal"), + Some(&serde_json::Value::Bool(true)) + ); } #[test] From 012f98134199fa1e68782bdb4db86fded6d955e3 Mon Sep 17 00:00:00 2001 From: Tomas Date: Fri, 31 Jul 2026 12:29:07 +0200 Subject: [PATCH 3/5] Regenerate clearContext bindings for the tightened runtime contract Mirrors github/copilot-agent-runtime#14002 after review: - `HistoryClearContextRequest.prompt` is now required. A cleared window holding only system and developer messages is not a conversation a model can answer, so every clear seeds the window it creates. - `HistoryClearContextResult` loses the `cleared` discriminator. The RPC now rejects the cases it was meant to describe - a remote session, or a call made while no tool call is in flight - so there is one error channel instead of a success flag plus an error channel. - `ContextClearedData.prependMessages` is gone. It had no producer, and it was a permanent commitment on a durable event for a code path nothing exercised. Regenerated with `scripts/codegen`; only the clear-context hunks are taken, so unrelated schema drift stays out of this PR. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Generated/SessionEvents.cs | 31 ------------ .../copilot/generated/SessionEvent.java | 4 +- nodejs/src/generated/session-events.ts | 50 +------------------ rust/src/generated/session_events.rs | 18 ------- 4 files changed, 2 insertions(+), 101 deletions(-) diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index 2e05bd5418..6b81a4770d 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -1687,19 +1687,6 @@ public sealed partial class McpAppToolCallCompleteEvent : SessionEvent public required McpAppToolCallCompleteData Data { get; set; } } -///

Context-cleared details emitted when the clear_context tool resets the conversation. -/// Represents the session.context_cleared event. -public sealed partial class SessionContextClearedEvent : SessionEvent -{ - /// - [JsonIgnore] - public override string Type => "session.context_cleared"; - - /// The session.context_cleared event payload. - [JsonPropertyName("data")] - public required SessionContextClearedData Data { get; set; } -} - /// Session initialization metadata including context and configuration. public sealed partial class SessionStartData { @@ -4722,24 +4709,6 @@ public sealed partial class McpAppToolCallCompleteData public required string ToolName { get; set; } } -/// Context-cleared details emitted when the clear_context tool resets the conversation. -public sealed partial class SessionContextClearedData -{ - /// Optional initial message set after clearing. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("initialMessage")] - public string? InitialMessage { get; set; } - - /// Number of conversation messages that were cleared. - [JsonPropertyName("messagesCleared")] - public required long MessagesCleared { get; set; } - - /// Runtime-injected messages re-seeded into the freshly-cleared context (e.g. self-paced loop wrappers). Persisted so a resumed session reproduces the same post-clear window instead of resurrecting the pre-clear history. - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("prependMessages")] - public string[]? PrependMessages { get; set; } -} - /// Working directory and git context at session start. /// Nested data type for WorkingDirectoryContext. public sealed partial class WorkingDirectoryContext diff --git a/java/src/generated/java/com/github/copilot/generated/SessionEvent.java b/java/src/generated/java/com/github/copilot/generated/SessionEvent.java index 1292a5a3e2..582ecd3d4f 100644 --- a/java/src/generated/java/com/github/copilot/generated/SessionEvent.java +++ b/java/src/generated/java/com/github/copilot/generated/SessionEvent.java @@ -139,8 +139,7 @@ @JsonSubTypes.Type(value = SessionCanvasRecordedEvent.class, name = "session.canvas.recorded"), @JsonSubTypes.Type(value = SessionCanvasRemovedEvent.class, name = "session.canvas.removed"), @JsonSubTypes.Type(value = SessionExtensionsAttachmentsPushedEvent.class, name = "session.extensions.attachments_pushed"), - @JsonSubTypes.Type(value = McpAppToolCallCompleteEvent.class, name = "mcp_app.tool_call_complete"), - @JsonSubTypes.Type(value = SessionContextClearedEvent.class, name = "session.context_cleared") + @JsonSubTypes.Type(value = McpAppToolCallCompleteEvent.class, name = "mcp_app.tool_call_complete") }) @javax.annotation.processing.Generated("copilot-sdk-codegen") public abstract sealed class SessionEvent permits @@ -259,7 +258,6 @@ public abstract sealed class SessionEvent permits SessionCanvasRemovedEvent, SessionExtensionsAttachmentsPushedEvent, McpAppToolCallCompleteEvent, - SessionContextClearedEvent, UnknownSessionEvent { /** Unique event identifier (UUID v4), generated when the event is emitted. */ diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 07cfceadef..1dad4ab166 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -119,8 +119,7 @@ export type SessionEvent = | CanvasRecordedEvent | CanvasRemovedEvent | ExtensionsAttachmentsPushedEvent - | McpAppToolCallCompleteEvent - | ContextClearedEvent; + | McpAppToolCallCompleteEvent; /** * Hosting platform type of the repository (github or ado) */ @@ -10014,50 +10013,3 @@ export interface McpAppToolCallCompleteToolMetaUI { */ visibility?: string[]; } -/** - * Session event "session.context_cleared". Context-cleared details emitted when the clear_context tool resets the conversation - */ -export interface ContextClearedEvent { - /** - * Sub-agent instance identifier. Absent for events from the root/main agent and session-level events. - */ - agentId?: string; - data: ContextClearedData; - /** - * When true, the event is transient and not persisted to the session event log on disk - */ - ephemeral?: boolean; - /** - * Unique event identifier (UUID v4), generated when the event is emitted - */ - id: string; - /** - * ID of the chronologically preceding event in the session, forming a linked chain. Null for the first event. - */ - parentId: string | null; - /** - * ISO 8601 timestamp when the event was created - */ - timestamp: string; - /** - * Type discriminator. Always "session.context_cleared". - */ - type: "session.context_cleared"; -} -/** - * Context-cleared details emitted when the clear_context tool resets the conversation - */ -export interface ContextClearedData { - /** - * Optional initial message set after clearing - */ - initialMessage?: string; - /** - * Number of conversation messages that were cleared - */ - messagesCleared: number; - /** - * Runtime-injected messages re-seeded into the freshly-cleared context (e.g. self-paced loop wrappers). Persisted so a resumed session reproduces the same post-clear window instead of resurrecting the pre-clear history. - */ - prependMessages?: string[]; -} diff --git a/rust/src/generated/session_events.rs b/rust/src/generated/session_events.rs index abc51e1112..ef3c395b10 100644 --- a/rust/src/generated/session_events.rs +++ b/rust/src/generated/session_events.rs @@ -318,8 +318,6 @@ pub enum SessionEventType { SessionExtensionsAttachmentsPushed, #[serde(rename = "mcp_app.tool_call_complete")] McpAppToolCallComplete, - #[serde(rename = "session.context_cleared")] - SessionContextCleared, /// Unknown event type for forward compatibility. #[default] #[serde(other)] @@ -632,8 +630,6 @@ pub enum SessionEventData { SessionExtensionsAttachmentsPushed(SessionExtensionsAttachmentsPushedData), #[serde(rename = "mcp_app.tool_call_complete")] McpAppToolCallComplete(McpAppToolCallCompleteData), - #[serde(rename = "session.context_cleared")] - SessionContextCleared(SessionContextClearedData), } /// A session event with typed data payload. @@ -5022,20 +5018,6 @@ pub struct McpAppToolCallCompleteData { pub tool_name: String, } -/// Session event "session.context_cleared". Context-cleared details emitted when the clear_context tool resets the conversation -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SessionContextClearedData { - /// Optional initial message set after clearing - #[serde(skip_serializing_if = "Option::is_none")] - pub initial_message: Option, - /// Number of conversation messages that were cleared - pub messages_cleared: i64, - /// Runtime-injected messages re-seeded into the freshly-cleared context (e.g. self-paced loop wrappers). Persisted so a resumed session reproduces the same post-clear window instead of resurrecting the pre-clear history. - #[serde(skip_serializing_if = "Option::is_none")] - pub prepend_messages: Option>, -} - /// Hosting platform type of the repository (github or ado) #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum WorkingDirectoryContextHostType { From 5000d029e9fb8a92ce9252afb378a68c5f764ebe Mon Sep 17 00:00:00 2001 From: examon Date: Wed, 5 Aug 2026 11:42:39 +0200 Subject: [PATCH 4/5] Close the two remaining reviewer findings on isTerminal The automated reviewer raised two suppressed findings against the new Tool.isTerminal field. Both were accurate. Rust: Tool has a hand-written Debug impl that enumerates every other serializable field, so is_terminal was silently missing from its output and a terminal tool debug-printed identically to a plain one. Add the field in declaration order, plus a test that fails if the hand-written impl drifts again. Python: dotnet, go, java, nodejs and rust all assert that isTerminal reaches the wire on both the session.create and session.resume paths and is omitted at its default. Python was the only SDK without that coverage. Add the test, mirroring the adjacent tool-metadata test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- python/test_client.py | 39 +++++++++++++++++++++++++++++++++++++++ rust/src/types.rs | 19 +++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/python/test_client.py b/python/test_client.py index 0bba1ccd7f..028b665fd5 100644 --- a/python/test_client.py +++ b/python/test_client.py @@ -793,6 +793,45 @@ async def mock_request(method, params, **kwargs): finally: await client.force_stop() + @pytest.mark.asyncio + async def test_create_and_resume_session_forward_tool_is_terminal(self): + client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) + await client.start() + try: + captured = {} + + async def mock_request(method, params, **kwargs): + captured[method] = params + if method in ("session.create", "session.resume"): + result = {"sessionId": params.get("sessionId") or "session-1"} + callback = kwargs.get("on_response_inline") + if callback is not None: + callback(result) + return result + return {} + + client._client.request = mock_request + tool = Tool(name="my_tool", description="a tool", is_terminal=True) + plain_tool = Tool(name="plain_tool", description="a tool") + + session = await client.create_session( + on_permission_request=PermissionHandler.approve_all, + tools=[tool, plain_tool], + ) + await client.resume_session( + session.session_id, + on_permission_request=PermissionHandler.approve_all, + tools=[tool], + ) + + create_tools = captured["session.create"]["tools"] + assert create_tools[0]["isTerminal"] is True + # Omitted when left at its default. + assert "isTerminal" not in create_tools[1] + assert captured["session.resume"]["tools"][0]["isTerminal"] is True + finally: + await client.force_stop() + @pytest.mark.asyncio async def test_create_and_resume_session_forward_canvas_provider(self): client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH)) diff --git a/rust/src/types.rs b/rust/src/types.rs index fd22efc9cc..3370843696 100644 --- a/rust/src/types.rs +++ b/rust/src/types.rs @@ -530,6 +530,7 @@ impl std::fmt::Debug for Tool { .field("parameters", &self.parameters) .field("overrides_built_in_tool", &self.overrides_built_in_tool) .field("skip_permission", &self.skip_permission) + .field("is_terminal", &self.is_terminal) .field("defer", &self.defer) .field("metadata", &self.metadata) .field( @@ -7562,4 +7563,22 @@ mod is_terminal_tests { let value = serde_json::to_value(&tool).expect("tool serializes"); assert!(value.get("isTerminal").is_none()); } + + /// `Tool` has a hand-written `Debug` impl, so a new field is only reported + /// if it is added there by hand. Guard against that drift. + #[test] + fn is_terminal_appears_in_debug_output() { + let terminal = Tool { + name: "clear_context".to_owned(), + is_terminal: true, + ..Default::default() + }; + assert!(format!("{terminal:?}").contains("is_terminal: true")); + + let plain = Tool { + name: "plain".to_owned(), + ..Default::default() + }; + assert!(format!("{plain:?}").contains("is_terminal: false")); + } } From 3ba3ff2ff172ce8217f6ed51fc09190414eabfad Mon Sep 17 00:00:00 2001 From: examon Date: Wed, 5 Aug 2026 12:15:39 +0200 Subject: [PATCH 5/5] Fix @since on the new Java isTerminal setter ToolDefinition.isTerminal(boolean) is introduced by this PR but was tagged @since 1.0.7, a version released long ago in which the method did not exist, so the generated javadoc would misstate its availability. 1.0.11 is the current unreleased version: the pom is at 1.0.10-preview.3-SNAPSHOT, and the eight java/src/main files carrying @since 1.0.11 include ToolDefinition.createOverride in this same file. This was the only @since tag the PR added. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- java/src/main/java/com/github/copilot/rpc/ToolDefinition.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java b/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java index 05ad1b9cf6..de274b66a1 100644 --- a/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java +++ b/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java @@ -395,7 +395,7 @@ public ToolDefinition metadata(Map 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.7 + * @since 1.0.11 */ @CopilotExperimental public ToolDefinition isTerminal(boolean value) {