))]
+public enum CopilotToolDefer
+{
+ /// The tool can be deferred and surfaced through tool search.
+ [System.Text.Json.Serialization.JsonStringEnumMemberName("auto")]
+ Auto,
+
+ /// The tool is always pre-loaded.
+ [System.Text.Json.Serialization.JsonStringEnumMemberName("never")]
+ Never
}
diff --git a/dotnet/test/Unit/CopilotToolTests.cs b/dotnet/test/Unit/CopilotToolTests.cs
index 76ad0e425..9c9e2a93b 100644
--- a/dotnet/test/Unit/CopilotToolTests.cs
+++ b/dotnet/test/Unit/CopilotToolTests.cs
@@ -19,7 +19,8 @@ public void DefineTool_Sets_Name_Description_And_Copilot_Metadata()
new CopilotToolOptions
{
OverridesBuiltInTool = true,
- SkipPermission = true
+ SkipPermission = true,
+ Defer = CopilotToolDefer.Auto
});
Assert.Equal("test_tool", function.Name);
@@ -28,6 +29,8 @@ public void DefineTool_Sets_Name_Description_And_Copilot_Metadata()
Assert.True((bool)isOverride!);
Assert.True(function.AdditionalProperties.TryGetValue("skip_permission", out var skipPermission));
Assert.True((bool)skipPermission!);
+ Assert.True(function.AdditionalProperties.TryGetValue("defer", out var defer));
+ Assert.Equal(CopilotToolDefer.Auto, defer);
}
[Fact]
@@ -37,6 +40,7 @@ public void DefineTool_Omits_Copilot_Metadata_When_Flags_Are_False()
Assert.False(function.AdditionalProperties.ContainsKey("is_override"));
Assert.False(function.AdditionalProperties.ContainsKey("skip_permission"));
+ Assert.False(function.AdditionalProperties.ContainsKey("defer"));
}
[Fact]
diff --git a/go/README.md b/go/README.md
index 0ceaabb73..4bdbc75f5 100644
--- a/go/README.md
+++ b/go/README.md
@@ -372,6 +372,18 @@ safeLookup := copilot.DefineTool("safe_lookup", "A read-only lookup that needs n
safeLookup.SkipPermission = true
```
+#### Deferring Tools
+
+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`.
+
+```go
+lookupIssue := copilot.DefineTool("lookup_issue", "Fetch issue details",
+ func(params LookupParams, inv copilot.ToolInvocation) (any, error) {
+ // your logic
+ })
+lookupIssue.Defer = copilot.ToolDeferAuto
+```
+
## Streaming
Enable streaming to receive assistant response chunks as they're generated:
diff --git a/go/client_test.go b/go/client_test.go
index d5ba47da8..bba1a130d 100644
--- a/go/client_test.go
+++ b/go/client_test.go
@@ -823,6 +823,47 @@ func TestOverridesBuiltInTool(t *testing.T) {
})
}
+func TestToolDefer(t *testing.T) {
+ t.Run("Defer is serialized in tool definition", func(t *testing.T) {
+ tool := Tool{
+ Name: "lookup_issue",
+ Description: "Fetch issue details",
+ Defer: ToolDeferAuto,
+ 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 v, ok := m["defer"]; !ok || v != "auto" {
+ t.Errorf("expected defer=auto, got %v", m)
+ }
+ })
+
+ t.Run("Defer omitted when unset", func(t *testing.T) {
+ tool := Tool{
+ Name: "custom_tool",
+ Description: "A custom tool",
+ 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 _, ok := m["defer"]; ok {
+ t.Errorf("expected defer to be omitted, got %v", m)
+ }
+ })
+}
+
func TestClient_CreateSession_AllowsMissingPermissionHandler(t *testing.T) {
t.Run("accepts nil config before connection validation", func(t *testing.T) {
client := NewClient(&ClientOptions{Connection: StdioConnection{Path: "/__nonexistent_copilot_binary__"}})
diff --git a/go/types.go b/go/types.go
index 7ffd454a3..6245ce519 100644
--- a/go/types.go
+++ b/go/types.go
@@ -1108,12 +1108,27 @@ type SessionConfig struct {
// ExtensionInfo identifies the stable extension providing this session's canvases.
ExtensionInfo *ExtensionInfo
}
+
+// ToolDefer controls whether a tool may be deferred (loaded lazily via tool
+// search) rather than always pre-loaded.
+type ToolDefer string
+
+const (
+ // ToolDeferAuto allows the tool to be deferred and surfaced through tool search.
+ ToolDeferAuto ToolDefer = "auto"
+ // ToolDeferNever forces the tool to always be pre-loaded.
+ ToolDeferNever ToolDefer = "never"
+)
+
type Tool struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Parameters map[string]any `json:"parameters,omitzero"`
OverridesBuiltInTool bool `json:"overridesBuiltInTool,omitempty"`
SkipPermission bool `json:"skipPermission,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"`
// Handler is optional. When nil, the SDK exposes the tool declaration but does
// not automatically invoke it.
Handler ToolHandler `json:"-"`
diff --git a/java/src/main/java/com/github/copilot/rpc/ToolDefer.java b/java/src/main/java/com/github/copilot/rpc/ToolDefer.java
new file mode 100644
index 000000000..bbcae850c
--- /dev/null
+++ b/java/src/main/java/com/github/copilot/rpc/ToolDefer.java
@@ -0,0 +1,69 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.rpc;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+/**
+ * Controls whether a {@link ToolDefinition} may be deferred (loaded lazily via
+ * tool search) rather than always pre-loaded.
+ *
+ * Set on
+ * {@link ToolDefinition#createWithDefer(String, String, java.util.Map, ToolHandler, ToolDefer)}
+ * to express the tool's deferral preference; defaults to letting the runtime
+ * decide when unset.
+ *
+ * @see ToolDefinition
+ * @since 1.2.0
+ */
+public enum ToolDefer {
+
+ /** The tool can be deferred and surfaced through tool search. */
+ AUTO("auto"),
+
+ /** The tool is always pre-loaded. */
+ NEVER("never");
+
+ private final String value;
+
+ ToolDefer(String value) {
+ this.value = value;
+ }
+
+ /**
+ * Returns the JSON value for this deferral mode.
+ *
+ * @return the string value used in JSON serialization
+ */
+ @JsonValue
+ public String getValue() {
+ return value;
+ }
+
+ /**
+ * Deserializes a JSON string value into the corresponding {@code ToolDefer}
+ * enum constant.
+ *
+ * @param value
+ * the JSON string value
+ * @return the matching {@code ToolDefer}, or {@code null} if value is
+ * {@code null}
+ * @throws IllegalArgumentException
+ * if the value does not match any known deferral mode
+ */
+ @JsonCreator
+ public static ToolDefer fromValue(String value) {
+ if (value == null) {
+ return null;
+ }
+ for (ToolDefer mode : values()) {
+ if (mode.value.equals(value)) {
+ return mode;
+ }
+ }
+ throw new IllegalArgumentException("Unknown ToolDefer value: " + value);
+ }
+}
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 c880e5a77..d8cebe325 100644
--- a/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java
+++ b/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java
@@ -56,6 +56,10 @@
* when {@code true}, the CLI skips the permission request for this
* tool invocation; {@code null} or {@code false} uses normal
* permission handling
+ * @param defer
+ * controls whether the tool may be deferred (loaded lazily via tool
+ * search) rather than always pre-loaded; {@code null} lets the
+ * runtime decide
* @see SessionConfig#setTools(java.util.List)
* @see ToolHandler
* @since 1.0.0
@@ -64,7 +68,7 @@
public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("description") String description,
@JsonProperty("parameters") Object parameters, @JsonIgnore ToolHandler handler,
@JsonProperty("overridesBuiltInTool") Boolean overridesBuiltInTool,
- @JsonProperty("skipPermission") Boolean skipPermission) {
+ @JsonProperty("skipPermission") Boolean skipPermission, @JsonProperty("defer") ToolDefer defer) {
/**
* Creates a tool definition with a JSON schema for parameters.
@@ -84,7 +88,7 @@ public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("d
*/
public static ToolDefinition create(String name, String description, Map schema,
ToolHandler handler) {
- return new ToolDefinition(name, description, schema, handler, null, null);
+ return new ToolDefinition(name, description, schema, handler, null, null, null);
}
/**
@@ -108,7 +112,7 @@ public static ToolDefinition create(String name, String description, Map schema,
ToolHandler handler) {
- return new ToolDefinition(name, description, schema, handler, true, null);
+ return new ToolDefinition(name, description, schema, handler, true, null, null);
}
/**
@@ -131,6 +135,32 @@ public static ToolDefinition createOverride(String name, String description, Map
*/
public static ToolDefinition createSkipPermission(String name, String description, Map schema,
ToolHandler handler) {
- return new ToolDefinition(name, description, schema, handler, null, true);
+ return new ToolDefinition(name, description, schema, handler, null, true, null);
+ }
+
+ /**
+ * Creates a tool definition with an explicit deferral mode.
+ *
+ * Use this factory method to control whether the tool may be deferred (loaded
+ * lazily via tool search) rather than always pre-loaded. Pass
+ * {@link ToolDefer#AUTO} to allow deferral and {@link ToolDefer#NEVER} to force
+ * the tool to always be pre-loaded.
+ *
+ * @param name
+ * the unique name of the tool
+ * @param description
+ * a description of what the tool does
+ * @param schema
+ * the JSON Schema as a {@code Map}
+ * @param handler
+ * the handler function to execute when invoked
+ * @param defer
+ * the deferral mode for the tool
+ * @return a new tool definition with the deferral mode set
+ * @since 1.2.0
+ */
+ public static ToolDefinition createWithDefer(String name, String description, Map schema,
+ ToolHandler handler, ToolDefer defer) {
+ return new ToolDefinition(name, description, schema, handler, null, null, defer);
}
}
diff --git a/java/src/test/java/com/github/copilot/ToolDefinitionTest.java b/java/src/test/java/com/github/copilot/ToolDefinitionTest.java
new file mode 100644
index 000000000..614e6ab4f
--- /dev/null
+++ b/java/src/test/java/com/github/copilot/ToolDefinitionTest.java
@@ -0,0 +1,62 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import com.github.copilot.rpc.ToolDefer;
+import com.github.copilot.rpc.ToolDefinition;
+
+/**
+ * Unit tests for {@link ToolDefinition} JSON serialization.
+ */
+public class ToolDefinitionTest {
+
+ private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper();
+
+ private static Map schema() {
+ return Map.of("type", "object", "properties",
+ Map.of("query", Map.of("type", "string", "description", "Search query")), "required", List.of("query"));
+ }
+
+ @Test
+ void testDeferIsSerialized() throws Exception {
+ ToolDefinition tool = ToolDefinition.createWithDefer("lookup_issue", "Fetch issue details", schema(),
+ invocation -> CompletableFuture.completedFuture("ok"), ToolDefer.AUTO);
+
+ ObjectNode json = (ObjectNode) MAPPER.readTree(MAPPER.writeValueAsString(tool));
+
+ assertEquals("auto", json.get("defer").asText());
+ }
+
+ @Test
+ void testDeferOmittedWhenNull() throws Exception {
+ ToolDefinition tool = ToolDefinition.create("lookup_issue", "Fetch issue details", schema(),
+ invocation -> CompletableFuture.completedFuture("ok"));
+
+ ObjectNode json = (ObjectNode) MAPPER.readTree(MAPPER.writeValueAsString(tool));
+
+ assertFalse(json.has("defer"));
+ }
+
+ @Test
+ void testDeferNeverIsSerialized() throws Exception {
+ ToolDefinition tool = ToolDefinition.createWithDefer("lookup_issue", "Fetch issue details", schema(),
+ invocation -> CompletableFuture.completedFuture("ok"), ToolDefer.NEVER);
+
+ ObjectNode json = (ObjectNode) MAPPER.readTree(MAPPER.writeValueAsString(tool));
+
+ assertEquals("never", json.get("defer").asText());
+ }
+}
diff --git a/nodejs/README.md b/nodejs/README.md
index 4219d3bc2..91aa5c4be 100644
--- a/nodejs/README.md
+++ b/nodejs/README.md
@@ -482,6 +482,21 @@ defineTool("safe_lookup", {
});
```
+#### Deferring Tools
+
+Set `defer` to control whether a tool may be loaded lazily via tool search rather than always pre-loaded. Use `"auto"` to allow the tool to be deferred and surfaced through tool search, or `"never"` to force it to always be pre-loaded. Defaults to `"auto"`.
+
+```ts
+defineTool("lookup_issue", {
+ description: "Fetch issue details",
+ parameters: z.object({ id: z.string() }),
+ defer: "auto",
+ handler: async ({ id }) => {
+ /* your logic */
+ },
+});
+```
+
### Commands
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.
diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts
index 8dc35b8d7..c59bd3e94 100644
--- a/nodejs/src/client.ts
+++ b/nodejs/src/client.ts
@@ -1107,6 +1107,7 @@ export class CopilotClient {
parameters: toJsonSchema(tool.parameters),
overridesBuiltInTool: tool.overridesBuiltInTool,
skipPermission: tool.skipPermission,
+ defer: tool.defer,
})),
canvases: config.canvases?.map((canvas) => canvas.declaration),
requestCanvasRenderer: config.requestCanvasRenderer,
@@ -1293,6 +1294,7 @@ export class CopilotClient {
parameters: toJsonSchema(tool.parameters),
overridesBuiltInTool: tool.overridesBuiltInTool,
skipPermission: tool.skipPermission,
+ defer: tool.defer,
})),
canvases: config.canvases?.map((canvas) => canvas.declaration),
requestCanvasRenderer: config.requestCanvasRenderer,
diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts
index 75aa5159f..647ca79c1 100644
--- a/nodejs/src/types.ts
+++ b/nodejs/src/types.ts
@@ -511,6 +511,13 @@ export interface Tool {
* When true, the tool can execute without a permission prompt.
*/
skipPermission?: boolean;
+ /**
+ * Controls whether the tool may be deferred (loaded lazily via tool search)
+ * rather than always pre-loaded. When `"auto"`, the tool can be deferred and
+ * surfaced through tool search. When `"never"`, the tool is always pre-loaded.
+ * Optional; defaults to `"auto"`.
+ */
+ defer?: "auto" | "never";
}
/**
@@ -525,6 +532,7 @@ export function defineTool(
handler?: ToolHandler;
overridesBuiltInTool?: boolean;
skipPermission?: boolean;
+ defer?: "auto" | "never";
}
): Tool {
return { name, ...config };
diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts
index 9352eb627..ab09f55ac 100644
--- a/nodejs/test/client.test.ts
+++ b/nodejs/test/client.test.ts
@@ -1355,6 +1355,63 @@ describe("CopilotClient", () => {
});
});
+ describe("defer in tool definitions", () => {
+ it("sends defer in tool definition on session.create", async () => {
+ const client = new CopilotClient();
+ await client.start();
+ onTestFinished(() => client.forceStop());
+
+ const spy = vi.spyOn((client as any).connection!, "sendRequest");
+ await client.createSession({
+ onPermissionRequest: approveAll,
+ tools: [
+ {
+ name: "lookup_issue",
+ description: "Fetch issue details",
+ handler: async () => "ok",
+ defer: "auto",
+ },
+ ],
+ });
+
+ const payload = spy.mock.calls.find((c) => c[0] === "session.create")![1] as any;
+ expect(payload.tools).toEqual([
+ expect.objectContaining({ name: "lookup_issue", defer: "auto" }),
+ ]);
+ });
+
+ it("sends defer in tool definition on session.resume", async () => {
+ const client = new CopilotClient();
+ await client.start();
+ onTestFinished(() => client.forceStop());
+
+ const session = await client.createSession({ onPermissionRequest: approveAll });
+ const spy = vi
+ .spyOn((client as any).connection!, "sendRequest")
+ .mockImplementation(async (method: string, params: any) => {
+ if (method === "session.resume") return { sessionId: params.sessionId };
+ throw new Error(`Unexpected method: ${method}`);
+ });
+ await client.resumeSession(session.sessionId, {
+ onPermissionRequest: approveAll,
+ tools: [
+ {
+ name: "lookup_issue",
+ description: "Fetch issue details",
+ handler: async () => "ok",
+ defer: "auto",
+ },
+ ],
+ });
+
+ const payload = spy.mock.calls.find((c) => c[0] === "session.resume")![1] as any;
+ expect(payload.tools).toEqual([
+ expect.objectContaining({ name: "lookup_issue", defer: "auto" }),
+ ]);
+ spy.mockRestore();
+ });
+ });
+
describe("agent parameter in session creation", () => {
it("forwards agent in session.create request", async () => {
const client = new CopilotClient();
diff --git a/python/README.md b/python/README.md
index a916f98ec..e77fcf16c 100644
--- a/python/README.md
+++ b/python/README.md
@@ -319,6 +319,16 @@ async def safe_lookup(params: LookupParams) -> str:
# your logic
```
+#### Deferring Tools
+
+Set `defer` to control whether a tool may be loaded lazily via tool search rather than always pre-loaded. Use `"auto"` to allow the tool to be deferred and surfaced through tool search, or `"never"` to force it to always be pre-loaded. Defaults to `"auto"`.
+
+```python
+@define_tool(name="lookup_issue", description="Fetch issue details", defer="auto")
+async def lookup_issue(params: LookupParams) -> str:
+ # your logic
+```
+
## Image Support
The SDK supports image attachments via the `attachments` parameter. You can attach images by providing their file path, or by passing base64-encoded data directly using a blob attachment:
diff --git a/python/copilot/client.py b/python/copilot/client.py
index 7dcec6e8f..24eec9d72 100644
--- a/python/copilot/client.py
+++ b/python/copilot/client.py
@@ -1750,6 +1750,8 @@ async def create_session(
definition["overridesBuiltInTool"] = True
if tool.skip_permission:
definition["skipPermission"] = True
+ if tool.defer is not None:
+ definition["defer"] = tool.defer
tool_defs.append(definition)
# Empty-mode validation and normalization
@@ -2323,6 +2325,8 @@ async def resume_session(
definition["overridesBuiltInTool"] = True
if tool.skip_permission:
definition["skipPermission"] = True
+ if tool.defer is not None:
+ definition["defer"] = tool.defer
tool_defs.append(definition)
# Empty-mode validation and normalization
diff --git a/python/copilot/tools.py b/python/copilot/tools.py
index c6a29dc61..a82a48b1e 100644
--- a/python/copilot/tools.py
+++ b/python/copilot/tools.py
@@ -62,6 +62,7 @@ class Tool:
parameters: dict[str, Any] | None = None
overrides_built_in_tool: bool = False
skip_permission: bool = False
+ defer: Literal["auto", "never"] | None = None
T = TypeVar("T", bound=BaseModel)
@@ -75,6 +76,7 @@ def define_tool(
description: str | None = None,
overrides_built_in_tool: bool = False,
skip_permission: bool = False,
+ defer: Literal["auto", "never"] | None = None,
) -> Callable[[Callable[..., Any]], Tool]:
pass
@@ -88,6 +90,7 @@ def define_tool(
handler: None = None,
overrides_built_in_tool: bool = False,
skip_permission: bool = False,
+ defer: Literal["auto", "never"] | None = None,
) -> Tool:
pass
@@ -101,6 +104,7 @@ def define_tool(
params_type: type[T],
overrides_built_in_tool: bool = False,
skip_permission: bool = False,
+ defer: Literal["auto", "never"] | None = None,
) -> Tool:
pass
@@ -113,6 +117,7 @@ def define_tool(
params_type: type[BaseModel] | None = None,
overrides_built_in_tool: bool = False,
skip_permission: bool = False,
+ defer: Literal["auto", "never"] | None = None,
) -> Tool | Callable[[Callable[[Any, ToolInvocation], Any]], Tool]:
"""
Define a tool with automatic JSON schema generation from Pydantic models.
@@ -157,6 +162,10 @@ def lookup_issue(params: LookupIssueParams) -> str:
to override a built-in tool of the same name. If not set and the
name clashes with a built-in tool, the runtime will return an error.
skip_permission: When True, the tool can execute without a permission prompt.
+ defer: Controls whether the tool may be deferred (loaded lazily via tool search)
+ rather than always pre-loaded. When "auto", the tool can be deferred
+ and surfaced through tool search. When "never", the tool is always
+ pre-loaded. Optional; defaults to "auto".
Returns:
A Tool instance
@@ -236,6 +245,7 @@ async def wrapped_handler(invocation: ToolInvocation) -> ToolResult:
handler=wrapped_handler,
overrides_built_in_tool=overrides_built_in_tool,
skip_permission=skip_permission,
+ defer=defer,
)
# If handler is provided, call decorator immediately
@@ -254,6 +264,7 @@ async def wrapped_handler(invocation: ToolInvocation) -> ToolResult:
handler=None,
overrides_built_in_tool=overrides_built_in_tool,
skip_permission=skip_permission,
+ defer=defer,
)
# Otherwise return decorator for @define_tool(...) usage
diff --git a/python/test_client.py b/python/test_client.py
index 502d410ab..93db185af 100644
--- a/python/test_client.py
+++ b/python/test_client.py
@@ -441,6 +441,70 @@ def grep(params) -> str:
await client.force_stop()
+class TestDefer:
+ @pytest.mark.asyncio
+ async def test_defer_sent_in_tool_definition(self):
+ client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH))
+ await client.start()
+
+ try:
+ captured = {}
+ original_request = client._client.request
+
+ async def mock_request(method, params, **kwargs):
+ captured[method] = params
+ return await original_request(method, params, **kwargs)
+
+ client._client.request = mock_request
+
+ @define_tool(description="Fetch issue details", defer="auto")
+ def lookup_issue(params) -> str:
+ return "ok"
+
+ await client.create_session(
+ on_permission_request=PermissionHandler.approve_all, tools=[lookup_issue]
+ )
+ tool_defs = captured["session.create"]["tools"]
+ assert len(tool_defs) == 1
+ assert tool_defs[0]["name"] == "lookup_issue"
+ assert tool_defs[0]["defer"] == "auto"
+ finally:
+ await client.force_stop()
+
+ @pytest.mark.asyncio
+ async def test_resume_session_sends_defer(self):
+ client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH))
+ await client.start()
+
+ try:
+ session = await client.create_session(
+ on_permission_request=PermissionHandler.approve_all
+ )
+
+ captured = {}
+
+ async def mock_request(method, params, **kwargs):
+ captured[method] = params
+ return {"sessionId": params["sessionId"]}
+
+ client._client.request = mock_request
+
+ @define_tool(description="Fetch issue details", defer="auto")
+ def lookup_issue(params) -> str:
+ return "ok"
+
+ await client.resume_session(
+ session.session_id,
+ on_permission_request=PermissionHandler.approve_all,
+ tools=[lookup_issue],
+ )
+ tool_defs = captured["session.resume"]["tools"]
+ assert len(tool_defs) == 1
+ assert tool_defs[0]["defer"] == "auto"
+ finally:
+ await client.force_stop()
+
+
class TestInstructionDirectories:
@pytest.mark.asyncio
async def test_create_session_sends_instruction_directories(self):
diff --git a/rust/src/types.rs b/rust/src/types.rs
index 8b9b5960a..63d79e573 100644
--- a/rust/src/types.rs
+++ b/rust/src/types.rs
@@ -337,6 +337,13 @@ pub struct Tool {
/// access control.
#[serde(default, skip_serializing_if = "is_false")]
pub skip_permission: 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
+ /// [`DeferMode::Never`], the tool is always pre-loaded. `None` lets the
+ /// runtime decide.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub defer: Option,
/// Optional runtime implementation. When `Some`, the SDK dispatches
/// matching `external_tool.requested` broadcasts to this handler.
/// When `None`, the tool is declaration-only.
@@ -357,6 +364,17 @@ fn is_false(b: &bool) -> bool {
!*b
}
+/// Controls whether a [`Tool`] may be deferred (loaded lazily via tool search)
+/// rather than always pre-loaded.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum DeferMode {
+ /// The tool can be deferred and surfaced through tool search.
+ Auto,
+ /// The tool is always pre-loaded.
+ Never,
+}
+
impl Tool {
/// Construct a new [`Tool`] with the given name and otherwise default
/// values. The struct is `#[non_exhaustive]`, so external callers
@@ -437,6 +455,14 @@ impl Tool {
self
}
+ /// Set the deferral mode controlling whether the tool may be loaded
+ /// lazily via tool search ([`DeferMode::Auto`]) or always pre-loaded
+ /// ([`DeferMode::Never`]).
+ pub fn with_defer(mut self, defer: DeferMode) -> Self {
+ self.defer = Some(defer);
+ self
+ }
+
/// Attach a runtime implementation. The SDK will dispatch matching
/// `external_tool.requested` broadcasts to `handler` for this tool's
/// name. Without a handler the tool is declaration-only.
@@ -464,6 +490,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("defer", &self.defer)
.field(
"handler",
&self.handler.as_ref().map(|_| "").unwrap_or("None"),
@@ -4300,6 +4327,18 @@ mod tests {
assert!(tool.skip_permission);
}
+ #[test]
+ fn tool_defer_serialization() {
+ let tool = Tool::new("lookup").with_defer(super::DeferMode::Auto);
+ assert_eq!(tool.defer, Some(super::DeferMode::Auto));
+ let value = serde_json::to_value(&tool).unwrap();
+ assert_eq!(value.get("defer").unwrap(), &json!("auto"));
+
+ let plain = Tool::new("plain");
+ let value = serde_json::to_value(&plain).unwrap();
+ assert!(value.get("defer").is_none());
+ }
+
#[test]
fn custom_agent_config_builder_with_model() {
let agent = CustomAgentConfig::new("my-agent", "You are helpful.")