Skip to content

Commit 736bd6e

Browse files
authored
Add defer parameter to tool definition (#1632)
* Add defer parameter to tool definition * Fix defer parameter comments and docs
1 parent 1600b57 commit 736bd6e

19 files changed

Lines changed: 509 additions & 10 deletions

File tree

dotnet/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,26 @@ var safeLookup = CopilotTool.DefineTool(
504504

505505
If you want to use `AIFunctionFactory.Create` directly, you can set `skip_permission` in the tool's `AdditionalProperties`.
506506

507+
#### Deferring Tools
508+
509+
Set `CopilotToolOptions.Defer` to control whether a tool may be loaded lazily via tool search rather than always pre-loaded. Use `CopilotToolDefer.Auto` to allow the tool to be deferred and surfaced through tool search, or `CopilotToolDefer.Never` to force it to always be pre-loaded. Defaults to `CopilotToolDefer.Auto`.
510+
511+
```csharp
512+
var lookupIssue = CopilotTool.DefineTool(
513+
async ([Description("Issue ID")] string id) => {
514+
// your logic
515+
},
516+
toolOptions: new CopilotToolOptions
517+
{
518+
Defer = CopilotToolDefer.Auto
519+
},
520+
factoryOptions: new AIFunctionFactoryOptions
521+
{
522+
Name = "lookup_issue",
523+
Description = "Fetch issue details",
524+
});
525+
```
526+
507527
## Commands
508528

509529
Register slash commands so that users of the CLI's TUI can invoke custom actions via `/commandName`. Each command has a `Name`, optional `Description`, and a `Handler` called when the user executes it.

dotnet/src/Client.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2336,15 +2336,18 @@ internal record ToolDefinition(
23362336
string? Description,
23372337
JsonElement Parameters, /* JSON schema */
23382338
bool? OverridesBuiltInTool = null,
2339-
bool? SkipPermission = null)
2339+
bool? SkipPermission = null,
2340+
CopilotToolDefer? Defer = null)
23402341
{
23412342
public static ToolDefinition FromAIFunction(AIFunctionDeclaration function)
23422343
{
23432344
var overrides = function.AdditionalProperties.TryGetValue(CopilotTool.OverridesBuiltInToolKey, out var val) && val is true;
23442345
var skipPerm = function.AdditionalProperties.TryGetValue(CopilotTool.SkipPermissionKey, out var skipVal) && skipVal is true;
2346+
var defer = function.AdditionalProperties.TryGetValue(CopilotTool.DeferKey, out var deferVal) && deferVal is CopilotToolDefer d ? d : (CopilotToolDefer?)null;
23452347
return new ToolDefinition(function.Name, function.Description, function.JsonSchema,
23462348
overrides ? true : null,
2347-
skipPerm ? true : null);
2349+
skipPerm ? true : null,
2350+
defer);
23482351
}
23492352
}
23502353

@@ -2501,6 +2504,7 @@ internal record HooksInvokeResponse(
25012504
[JsonSerializable(typeof(SystemMessageTransformRpcResponse))]
25022505
[JsonSerializable(typeof(CommandWireDefinition))]
25032506
[JsonSerializable(typeof(ToolDefinition))]
2507+
[JsonSerializable(typeof(CopilotToolDefer))]
25042508
[JsonSerializable(typeof(ToolResultAIContent))]
25052509
[JsonSerializable(typeof(ToolResultObject))]
25062510
[JsonSerializable(typeof(UserInputRequestResponse))]

dotnet/src/CopilotTool.cs

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

20+
/// <summary>The key used in <see cref="AITool.AdditionalProperties"/> to carry the tool's <see cref="CopilotToolDefer"/> deferral mode.</summary>
21+
internal const string DeferKey = "defer";
22+
2023
/// <summary>
2124
/// Defines a tool for use in a <see cref="CopilotSession"/>.
2225
/// </summary>
@@ -84,7 +87,7 @@ static void ApplyToolInvocationBinding(AIFunctionFactoryOptions factoryOptions)
8487

8588
static void ApplyToolOptions(AIFunctionFactoryOptions factoryOptions, CopilotToolOptions? toolOptions)
8689
{
87-
if (toolOptions is not null && (toolOptions.OverridesBuiltInTool || toolOptions.SkipPermission))
90+
if (toolOptions is not null && (toolOptions.OverridesBuiltInTool || toolOptions.SkipPermission || toolOptions.Defer is not null))
8891
{
8992
Dictionary<string, object?> additionalProperties = new(StringComparer.Ordinal);
9093
if (factoryOptions.AdditionalProperties is not null)
@@ -105,6 +108,11 @@ static void ApplyToolOptions(AIFunctionFactoryOptions factoryOptions, CopilotToo
105108
additionalProperties[SkipPermissionKey] = true;
106109
}
107110

111+
if (toolOptions.Defer is { } defer)
112+
{
113+
additionalProperties[DeferKey] = defer;
114+
}
115+
108116
factoryOptions.AdditionalProperties = additionalProperties;
109117
}
110118
}
@@ -121,7 +129,7 @@ public sealed class CopilotToolOptions
121129
/// Gets or sets a value indicating whether this tool intentionally overrides a built-in Copilot tool with the same name.
122130
/// </summary>
123131
/// <remarks>
124-
/// When a <see cref="CopilotToolOptions"/> with <see cref="OverridesBuiltInTool"/> set to true is used to define a tool,
132+
/// When a <see cref="CopilotToolOptions"/> with <see cref="OverridesBuiltInTool"/> set to true is used to define a tool,
125133
/// the resulting <see cref="AIFunction"/> will include "is_override": true in its <see cref="AITool.AdditionalProperties"/>.
126134
/// </remarks>
127135
public bool OverridesBuiltInTool { get; set; }
@@ -130,8 +138,32 @@ public sealed class CopilotToolOptions
130138
/// Gets or sets a value indicating whether this tool can execute without a permission prompt.
131139
/// </summary>
132140
/// <remarks>
133-
/// When a <see cref="CopilotToolOptions"/> with <see cref="SkipPermission"/> set to true is used to define a tool,
141+
/// When a <see cref="CopilotToolOptions"/> with <see cref="SkipPermission"/> set to true is used to define a tool,
134142
/// the resulting <see cref="AIFunction"/> will include "skip_permission": true in its <see cref="AITool.AdditionalProperties"/>.
135143
/// </remarks>
136144
public bool SkipPermission { get; set; }
145+
146+
/// <summary>
147+
/// Gets or sets a value controlling whether this tool may be deferred (loaded lazily via tool search) rather than always pre-loaded.
148+
/// </summary>
149+
/// <remarks>
150+
/// When set, the resulting <see cref="AIFunction"/> carries the value in its <see cref="AITool.AdditionalProperties"/> and the
151+
/// SDK forwards it to the CLI as the tool's <c>defer</c> mode. Defaults to "auto".
152+
/// </remarks>
153+
public CopilotToolDefer? Defer { get; set; }
154+
}
155+
156+
/// <summary>
157+
/// Controls whether a tool may be deferred (loaded lazily via tool search) rather than always pre-loaded.
158+
/// </summary>
159+
[System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter<CopilotToolDefer>))]
160+
public enum CopilotToolDefer
161+
{
162+
/// <summary>The tool can be deferred and surfaced through tool search.</summary>
163+
[System.Text.Json.Serialization.JsonStringEnumMemberName("auto")]
164+
Auto,
165+
166+
/// <summary>The tool is always pre-loaded.</summary>
167+
[System.Text.Json.Serialization.JsonStringEnumMemberName("never")]
168+
Never
137169
}

dotnet/test/Unit/CopilotToolTests.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ public void DefineTool_Sets_Name_Description_And_Copilot_Metadata()
1919
new CopilotToolOptions
2020
{
2121
OverridesBuiltInTool = true,
22-
SkipPermission = true
22+
SkipPermission = true,
23+
Defer = CopilotToolDefer.Auto
2324
});
2425

2526
Assert.Equal("test_tool", function.Name);
@@ -28,6 +29,8 @@ public void DefineTool_Sets_Name_Description_And_Copilot_Metadata()
2829
Assert.True((bool)isOverride!);
2930
Assert.True(function.AdditionalProperties.TryGetValue("skip_permission", out var skipPermission));
3031
Assert.True((bool)skipPermission!);
32+
Assert.True(function.AdditionalProperties.TryGetValue("defer", out var defer));
33+
Assert.Equal(CopilotToolDefer.Auto, defer);
3134
}
3235

3336
[Fact]
@@ -37,6 +40,7 @@ public void DefineTool_Omits_Copilot_Metadata_When_Flags_Are_False()
3740

3841
Assert.False(function.AdditionalProperties.ContainsKey("is_override"));
3942
Assert.False(function.AdditionalProperties.ContainsKey("skip_permission"));
43+
Assert.False(function.AdditionalProperties.ContainsKey("defer"));
4044
}
4145

4246
[Fact]

go/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,18 @@ safeLookup := copilot.DefineTool("safe_lookup", "A read-only lookup that needs n
372372
safeLookup.SkipPermission = true
373373
```
374374

375+
#### Deferring Tools
376+
377+
Set `Defer` to control whether a tool may be loaded lazily via tool search rather than always pre-loaded. Use `copilot.ToolDeferAuto` to allow the tool to be deferred and surfaced through tool search, or `copilot.ToolDeferNever` to force it to always be pre-loaded. Defaults to `copilot.ToolDeferAuto`.
378+
379+
```go
380+
lookupIssue := copilot.DefineTool("lookup_issue", "Fetch issue details",
381+
func(params LookupParams, inv copilot.ToolInvocation) (any, error) {
382+
// your logic
383+
})
384+
lookupIssue.Defer = copilot.ToolDeferAuto
385+
```
386+
375387
## Streaming
376388

377389
Enable streaming to receive assistant response chunks as they're generated:

go/client_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -823,6 +823,47 @@ func TestOverridesBuiltInTool(t *testing.T) {
823823
})
824824
}
825825

826+
func TestToolDefer(t *testing.T) {
827+
t.Run("Defer is serialized in tool definition", func(t *testing.T) {
828+
tool := Tool{
829+
Name: "lookup_issue",
830+
Description: "Fetch issue details",
831+
Defer: ToolDeferAuto,
832+
Handler: func(_ ToolInvocation) (ToolResult, error) { return ToolResult{}, nil },
833+
}
834+
data, err := json.Marshal(tool)
835+
if err != nil {
836+
t.Fatalf("failed to marshal: %v", err)
837+
}
838+
var m map[string]any
839+
if err := json.Unmarshal(data, &m); err != nil {
840+
t.Fatalf("failed to unmarshal: %v", err)
841+
}
842+
if v, ok := m["defer"]; !ok || v != "auto" {
843+
t.Errorf("expected defer=auto, got %v", m)
844+
}
845+
})
846+
847+
t.Run("Defer omitted when unset", func(t *testing.T) {
848+
tool := Tool{
849+
Name: "custom_tool",
850+
Description: "A custom tool",
851+
Handler: func(_ ToolInvocation) (ToolResult, error) { return ToolResult{}, nil },
852+
}
853+
data, err := json.Marshal(tool)
854+
if err != nil {
855+
t.Fatalf("failed to marshal: %v", err)
856+
}
857+
var m map[string]any
858+
if err := json.Unmarshal(data, &m); err != nil {
859+
t.Fatalf("failed to unmarshal: %v", err)
860+
}
861+
if _, ok := m["defer"]; ok {
862+
t.Errorf("expected defer to be omitted, got %v", m)
863+
}
864+
})
865+
}
866+
826867
func TestClient_CreateSession_AllowsMissingPermissionHandler(t *testing.T) {
827868
t.Run("accepts nil config before connection validation", func(t *testing.T) {
828869
client := NewClient(&ClientOptions{Connection: StdioConnection{Path: "/__nonexistent_copilot_binary__"}})

go/types.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,12 +1108,27 @@ type SessionConfig struct {
11081108
// ExtensionInfo identifies the stable extension providing this session's canvases.
11091109
ExtensionInfo *ExtensionInfo
11101110
}
1111+
1112+
// ToolDefer controls whether a tool may be deferred (loaded lazily via tool
1113+
// search) rather than always pre-loaded.
1114+
type ToolDefer string
1115+
1116+
const (
1117+
// ToolDeferAuto allows the tool to be deferred and surfaced through tool search.
1118+
ToolDeferAuto ToolDefer = "auto"
1119+
// ToolDeferNever forces the tool to always be pre-loaded.
1120+
ToolDeferNever ToolDefer = "never"
1121+
)
1122+
11111123
type Tool struct {
11121124
Name string `json:"name"`
11131125
Description string `json:"description,omitempty"`
11141126
Parameters map[string]any `json:"parameters,omitzero"`
11151127
OverridesBuiltInTool bool `json:"overridesBuiltInTool,omitempty"`
11161128
SkipPermission bool `json:"skipPermission,omitempty"`
1129+
// Defer controls whether the tool may be deferred (loaded lazily via tool
1130+
// search) rather than always pre-loaded. When empty, the runtime decides.
1131+
Defer ToolDefer `json:"defer,omitempty"`
11171132
// Handler is optional. When nil, the SDK exposes the tool declaration but does
11181133
// not automatically invoke it.
11191134
Handler ToolHandler `json:"-"`
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.rpc;
6+
7+
import com.fasterxml.jackson.annotation.JsonCreator;
8+
import com.fasterxml.jackson.annotation.JsonValue;
9+
10+
/**
11+
* Controls whether a {@link ToolDefinition} may be deferred (loaded lazily via
12+
* tool search) rather than always pre-loaded.
13+
* <p>
14+
* Set on
15+
* {@link ToolDefinition#createWithDefer(String, String, java.util.Map, ToolHandler, ToolDefer)}
16+
* to express the tool's deferral preference; defaults to letting the runtime
17+
* decide when unset.
18+
*
19+
* @see ToolDefinition
20+
* @since 1.2.0
21+
*/
22+
public enum ToolDefer {
23+
24+
/** The tool can be deferred and surfaced through tool search. */
25+
AUTO("auto"),
26+
27+
/** The tool is always pre-loaded. */
28+
NEVER("never");
29+
30+
private final String value;
31+
32+
ToolDefer(String value) {
33+
this.value = value;
34+
}
35+
36+
/**
37+
* Returns the JSON value for this deferral mode.
38+
*
39+
* @return the string value used in JSON serialization
40+
*/
41+
@JsonValue
42+
public String getValue() {
43+
return value;
44+
}
45+
46+
/**
47+
* Deserializes a JSON string value into the corresponding {@code ToolDefer}
48+
* enum constant.
49+
*
50+
* @param value
51+
* the JSON string value
52+
* @return the matching {@code ToolDefer}, or {@code null} if value is
53+
* {@code null}
54+
* @throws IllegalArgumentException
55+
* if the value does not match any known deferral mode
56+
*/
57+
@JsonCreator
58+
public static ToolDefer fromValue(String value) {
59+
if (value == null) {
60+
return null;
61+
}
62+
for (ToolDefer mode : values()) {
63+
if (mode.value.equals(value)) {
64+
return mode;
65+
}
66+
}
67+
throw new IllegalArgumentException("Unknown ToolDefer value: " + value);
68+
}
69+
}

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

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@
5656
* when {@code true}, the CLI skips the permission request for this
5757
* tool invocation; {@code null} or {@code false} uses normal
5858
* permission handling
59+
* @param defer
60+
* controls whether the tool may be deferred (loaded lazily via tool
61+
* search) rather than always pre-loaded; {@code null} lets the
62+
* runtime decide
5963
* @see SessionConfig#setTools(java.util.List)
6064
* @see ToolHandler
6165
* @since 1.0.0
@@ -64,7 +68,7 @@
6468
public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("description") String description,
6569
@JsonProperty("parameters") Object parameters, @JsonIgnore ToolHandler handler,
6670
@JsonProperty("overridesBuiltInTool") Boolean overridesBuiltInTool,
67-
@JsonProperty("skipPermission") Boolean skipPermission) {
71+
@JsonProperty("skipPermission") Boolean skipPermission, @JsonProperty("defer") ToolDefer defer) {
6872

6973
/**
7074
* Creates a tool definition with a JSON schema for parameters.
@@ -84,7 +88,7 @@ public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("d
8488
*/
8589
public static ToolDefinition create(String name, String description, Map<String, Object> schema,
8690
ToolHandler handler) {
87-
return new ToolDefinition(name, description, schema, handler, null, null);
91+
return new ToolDefinition(name, description, schema, handler, null, null, null);
8892
}
8993

9094
/**
@@ -108,7 +112,7 @@ public static ToolDefinition create(String name, String description, Map<String,
108112
*/
109113
public static ToolDefinition createOverride(String name, String description, Map<String, Object> schema,
110114
ToolHandler handler) {
111-
return new ToolDefinition(name, description, schema, handler, true, null);
115+
return new ToolDefinition(name, description, schema, handler, true, null, null);
112116
}
113117

114118
/**
@@ -131,6 +135,32 @@ public static ToolDefinition createOverride(String name, String description, Map
131135
*/
132136
public static ToolDefinition createSkipPermission(String name, String description, Map<String, Object> schema,
133137
ToolHandler handler) {
134-
return new ToolDefinition(name, description, schema, handler, null, true);
138+
return new ToolDefinition(name, description, schema, handler, null, true, null);
139+
}
140+
141+
/**
142+
* Creates a tool definition with an explicit deferral mode.
143+
* <p>
144+
* Use this factory method to control whether the tool may be deferred (loaded
145+
* lazily via tool search) rather than always pre-loaded. Pass
146+
* {@link ToolDefer#AUTO} to allow deferral and {@link ToolDefer#NEVER} to force
147+
* the tool to always be pre-loaded.
148+
*
149+
* @param name
150+
* the unique name of the tool
151+
* @param description
152+
* a description of what the tool does
153+
* @param schema
154+
* the JSON Schema as a {@code Map}
155+
* @param handler
156+
* the handler function to execute when invoked
157+
* @param defer
158+
* the deferral mode for the tool
159+
* @return a new tool definition with the deferral mode set
160+
* @since 1.2.0
161+
*/
162+
public static ToolDefinition createWithDefer(String name, String description, Map<String, Object> schema,
163+
ToolHandler handler, ToolDefer defer) {
164+
return new ToolDefinition(name, description, schema, handler, null, null, defer);
135165
}
136166
}

0 commit comments

Comments
 (0)