Skip to content

Commit 058407e

Browse files
examonCopilotSteveSandersonMS
authored
Add history.clearContext and Tool.isTerminal across all SDKs (#2129)
* 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. * 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. * 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> * 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> * 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> * Expose isTerminal through Java tool annotations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Test .NET terminal tools on session requests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Cover terminal tool runtime behavior end to end Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Document context clearing and terminal tools Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Normalize clear context snapshot formatting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: examon <examon@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Steve Sanderson <SteveSandersonMS@users.noreply.github.com>
1 parent 779a989 commit 058407e

24 files changed

Lines changed: 592 additions & 20 deletions

docs/features/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ These guides cover the capabilities you can add to your Copilot SDK application.
2121
| [Streaming Events](./streaming-events.md) | Subscribe to real-time session events (40+ event types) |
2222
| [Usage and Billing](./usage-and-billing.md) | Read token counts, context-window utilization, AI credit cost, and account quota |
2323
| [Steering & Queueing](./steering-and-queueing.md) | Control message delivery—immediate steering vs. sequential queueing |
24+
| [Context Clearing](./context-management.md) | Replace conversation context safely with terminal tools |
2425
| [Session Persistence](./session-persistence.md) | Resume sessions across restarts, manage session storage |
2526
| [Remote Sessions](./remote-sessions.md) | Share locally hosted sessions to GitHub web and mobile via Mission Control |
2627
| [Cloud Sessions](./cloud-sessions.md) | Run sessions on GitHub-hosted compute through Mission Control |
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Context clearing and terminal tools
2+
3+
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.
4+
5+
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.
6+
7+
> [!IMPORTANT]
8+
> `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.
9+
10+
## Define a context-clearing tool
11+
12+
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.
13+
14+
```typescript
15+
import { approveAll, CopilotClient, defineTool } from "@github/copilot-sdk";
16+
import type { CopilotSession } from "@github/copilot-sdk";
17+
import { z } from "zod";
18+
19+
const client = new CopilotClient();
20+
let session: CopilotSession;
21+
22+
session = await client.createSession({
23+
onPermissionRequest: approveAll,
24+
tools: [
25+
defineTool("clear_context", {
26+
description: "Clear the conversation and start a fresh context window",
27+
parameters: z.object({ prompt: z.string() }),
28+
isTerminal: true,
29+
defer: "never",
30+
handler: async ({ prompt }) => {
31+
const { messagesCleared } =
32+
await session.rpc.history.clearContext({ prompt });
33+
return `Cleared ${messagesCleared} messages.`;
34+
},
35+
}),
36+
],
37+
});
38+
```
39+
40+
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.
41+
42+
## Terminal-tool behavior
43+
44+
`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.
45+
46+
The option follows each language's naming conventions:
47+
48+
| SDK | Tool option |
49+
|---|---|
50+
| Node.js | `isTerminal` |
51+
| Python | `is_terminal` |
52+
| Go | `IsTerminal` |
53+
| .NET | `CopilotToolOptions.IsTerminal` |
54+
| Java | `ToolDefinition.isTerminal(true)` or `@CopilotTool(isTerminal = true)` |
55+
| Rust | `with_is_terminal(true)` |
56+
57+
Use terminality only for tools whose successful completion should end the turn. Ordinary tools should leave it unset.

docs/troubleshooting/compatibility.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ The Copilot SDK communicates with the CLI via JSON-RPC protocol. Features must b
8989
| Agent management | `session.rpc.agent.*` | List, select, deselect, get current agent |
9090
| Fleet mode | `session.rpc.fleet.start()` | Parallel sub-agent execution; see [Fleet mode](../features/fleet-mode.md) |
9191
| Manual compaction | `session.rpc.history.compact()` | Trigger compaction on demand |
92+
| Context clearing | `session.rpc.history.clearContext()` | Replace conversation context from a terminal tool |
9293
| History truncation | `session.rpc.history.truncate()` | Remove events from a point onward |
9394
| Session forking | `server.rpc.sessions.fork()` | Fork a session at a point in history |
9495

dotnet/src/Client.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2793,19 +2793,22 @@ internal record ToolDefinition(
27932793
bool? OverridesBuiltInTool = null,
27942794
bool? SkipPermission = null,
27952795
CopilotToolDefer? Defer = null,
2796-
IDictionary<string, JsonNode?>? Metadata = null)
2796+
IDictionary<string, JsonNode?>? Metadata = null,
2797+
bool? IsTerminal = null)
27972798
{
27982799
public static ToolDefinition FromAIFunction(AIFunctionDeclaration function)
27992800
{
28002801
var overrides = function.AdditionalProperties.TryGetValue(CopilotTool.OverridesBuiltInToolKey, out var val) && val is true;
28012802
var skipPerm = function.AdditionalProperties.TryGetValue(CopilotTool.SkipPermissionKey, out var skipVal) && skipVal is true;
28022803
var defer = function.AdditionalProperties.TryGetValue(CopilotTool.DeferKey, out var deferVal) && deferVal is CopilotToolDefer d ? d : (CopilotToolDefer?)null;
28032804
var metadata = function.AdditionalProperties.TryGetValue(CopilotTool.MetadataKey, out var metaVal) && metaVal is IDictionary<string, JsonNode?> m ? m : null;
2805+
var isTerminal = function.AdditionalProperties.TryGetValue(CopilotTool.IsTerminalKey, out var terminalVal) && terminalVal is true;
28042806
return new ToolDefinition(function.Name, function.Description, function.JsonSchema,
28052807
overrides ? true : null,
28062808
skipPerm ? true : null,
28072809
defer,
2808-
metadata);
2810+
metadata,
2811+
isTerminal ? true : null);
28092812
}
28102813
}
28112814

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/test/Unit/ClientSessionLifetimeTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,39 @@ public async Task SessionRequests_Serialize_AdditionalDirectories()
287287
value => Assert.Equal("/repo/resumed", value.GetString()));
288288
}
289289

290+
[Fact]
291+
public async Task SessionRequests_Serialize_Terminal_Tools()
292+
{
293+
await using var server = await FakeCopilotServer.StartAsync();
294+
await using var client = new CopilotClient(new CopilotClientOptions { Connection = RuntimeConnection.ForUri(server.Url) });
295+
var terminalTool = CopilotTool.DefineTool(
296+
(Func<string>)(() => "done"),
297+
new CopilotToolOptions { IsTerminal = true });
298+
var plainTool = CopilotTool.DefineTool((Func<string>)(() => "continue"));
299+
300+
await using var created = await client.CreateSessionAsync(new SessionConfig
301+
{
302+
Tools = [terminalTool, plainTool],
303+
OnPermissionRequest = PermissionHandler.ApproveAll
304+
});
305+
306+
var createRequest = Assert.Single(server.Requests, request => request.Method == "session.create");
307+
var createTools = createRequest.Params.GetProperty("tools");
308+
Assert.True(createTools[0].GetProperty("isTerminal").GetBoolean());
309+
Assert.False(createTools[1].TryGetProperty("isTerminal", out _));
310+
311+
server.ClearRequests();
312+
313+
await using var resumed = await client.ResumeSessionAsync("resume-with-terminal-tool", new ResumeSessionConfig
314+
{
315+
Tools = [terminalTool],
316+
OnPermissionRequest = PermissionHandler.ApproveAll
317+
});
318+
319+
var resumeRequest = Assert.Single(server.Requests, request => request.Method == "session.resume");
320+
Assert.True(resumeRequest.Params.GetProperty("tools")[0].GetProperty("isTerminal").GetBoolean());
321+
}
322+
290323
[Fact]
291324
public async Task CreateSessionAsync_Registers_McpAuth_Interest_Only_When_Handler_Configured()
292325
{

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
@@ -3526,3 +3526,40 @@ func TestResumeSessionRequest_ExpAssignments(t *testing.T) {
35263526
}
35273527
})
35283528
}
3529+
3530+
func TestIsTerminal(t *testing.T) {
3531+
t.Run("IsTerminal is serialized in tool definition", func(t *testing.T) {
3532+
tool := Tool{
3533+
Name: "clear_context",
3534+
Description: "Clear the conversation",
3535+
IsTerminal: true,
3536+
Handler: func(_ ToolInvocation) (ToolResult, error) { return ToolResult{}, nil },
3537+
}
3538+
data, err := json.Marshal(tool)
3539+
if err != nil {
3540+
t.Fatalf("Failed to marshal: %v", err)
3541+
}
3542+
var m map[string]any
3543+
if err := json.Unmarshal(data, &m); err != nil {
3544+
t.Fatalf("Failed to unmarshal: %v", err)
3545+
}
3546+
if m["isTerminal"] != true {
3547+
t.Errorf("Expected isTerminal to be true, got %v", m["isTerminal"])
3548+
}
3549+
})
3550+
3551+
t.Run("IsTerminal is omitted when false", func(t *testing.T) {
3552+
tool := Tool{Name: "plain", Description: "A plain tool"}
3553+
data, err := json.Marshal(tool)
3554+
if err != nil {
3555+
t.Fatalf("Failed to marshal: %v", err)
3556+
}
3557+
var m map[string]any
3558+
if err := json.Unmarshal(data, &m); err != nil {
3559+
t.Fatalf("Failed to unmarshal: %v", err)
3560+
}
3561+
if _, ok := m["isTerminal"]; ok {
3562+
t.Error("Expected isTerminal to be omitted when false")
3563+
}
3564+
})
3565+
}

go/types.go

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

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

Lines changed: 60 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,13 @@ 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 hint.
9499
* <p>
95100
* Convenience overload equivalent to the canonical constructor with
96-
* {@code metadata} set to {@code null}.
101+
* {@code metadata} and {@code isTerminal} set to {@code null}.
97102
*
98103
* @param name
99104
* the unique name of the tool
@@ -114,7 +119,37 @@ public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("d
114119
*/
115120
public ToolDefinition(String name, String description, Object parameters, ToolHandler handler,
116121
Boolean overridesBuiltInTool, Boolean skipPermission, ToolDefer defer) {
117-
this(name, description, parameters, handler, overridesBuiltInTool, skipPermission, defer, null);
122+
this(name, description, parameters, handler, overridesBuiltInTool, skipPermission, defer, null, null);
123+
}
124+
125+
/**
126+
* Creates a tool definition without a terminality hint.
127+
* <p>
128+
* Convenience overload equivalent to the canonical constructor with
129+
* {@code isTerminal} set to {@code null}.
130+
*
131+
* @param name
132+
* the unique name of the tool
133+
* @param description
134+
* a description of what the tool does
135+
* @param parameters
136+
* the JSON Schema for the tool's parameters
137+
* @param handler
138+
* the handler function to execute when invoked
139+
* @param overridesBuiltInTool
140+
* whether this tool overrides a built-in tool; {@code null} for the
141+
* default
142+
* @param skipPermission
143+
* whether the tool may run without a permission check; {@code null}
144+
* for the default
145+
* @param defer
146+
* the deferral mode; {@code null} lets the runtime decide
147+
* @param metadata
148+
* the opaque, host-defined metadata; {@code null} when unset
149+
*/
150+
public ToolDefinition(String name, String description, Object parameters, ToolHandler handler,
151+
Boolean overridesBuiltInTool, Boolean skipPermission, ToolDefer defer, Map<String, Object> metadata) {
152+
this(name, description, parameters, handler, overridesBuiltInTool, skipPermission, defer, metadata, null);
118153
}
119154

120155
/**
@@ -304,7 +339,8 @@ public static List<ToolDefinition> fromClass(Class<?> clazz) {
304339
*/
305340
@CopilotExperimental
306341
public ToolDefinition overridesBuiltInTool(boolean value) {
307-
return new ToolDefinition(name, description, parameters, handler, value, skipPermission, defer, metadata);
342+
return new ToolDefinition(name, description, parameters, handler, value, skipPermission, defer, metadata,
343+
isTerminal);
308344
}
309345

310346
/**
@@ -318,7 +354,8 @@ public ToolDefinition overridesBuiltInTool(boolean value) {
318354
*/
319355
@CopilotExperimental
320356
public ToolDefinition skipPermission(boolean value) {
321-
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, value, defer, metadata);
357+
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, value, defer, metadata,
358+
isTerminal);
322359
}
323360

324361
/**
@@ -333,7 +370,7 @@ public ToolDefinition skipPermission(boolean value) {
333370
@CopilotExperimental
334371
public ToolDefinition defer(ToolDefer value) {
335372
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, skipPermission, value,
336-
metadata);
373+
metadata, isTerminal);
337374
}
338375

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

354406
// ------------------------------------------------------------------

0 commit comments

Comments
 (0)