Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,26 @@ var safeLookup = CopilotTool.DefineTool(

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

#### Deferring Tools

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`.

```csharp
var lookupIssue = CopilotTool.DefineTool(
async ([Description("Issue ID")] string id) => {
// your logic
},
toolOptions: new CopilotToolOptions
{
Defer = CopilotToolDefer.Auto
},
factoryOptions: new AIFunctionFactoryOptions
{
Name = "lookup_issue",
Description = "Fetch issue details",
});
```

## 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.
Expand Down
8 changes: 6 additions & 2 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2336,15 +2336,18 @@ internal record ToolDefinition(
string? Description,
JsonElement Parameters, /* JSON schema */
bool? OverridesBuiltInTool = null,
bool? SkipPermission = null)
bool? SkipPermission = null,
CopilotToolDefer? Defer = null)
{
public static ToolDefinition FromAIFunction(AIFunctionDeclaration function)
{
var overrides = function.AdditionalProperties.TryGetValue(CopilotTool.OverridesBuiltInToolKey, out var val) && val is true;
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;
return new ToolDefinition(function.Name, function.Description, function.JsonSchema,
overrides ? true : null,
skipPerm ? true : null);
skipPerm ? true : null,
defer);
}
}

Expand Down Expand Up @@ -2501,6 +2504,7 @@ internal record HooksInvokeResponse(
[JsonSerializable(typeof(SystemMessageTransformRpcResponse))]
[JsonSerializable(typeof(CommandWireDefinition))]
[JsonSerializable(typeof(ToolDefinition))]
[JsonSerializable(typeof(CopilotToolDefer))]
[JsonSerializable(typeof(ToolResultAIContent))]
[JsonSerializable(typeof(ToolResultObject))]
[JsonSerializable(typeof(UserInputRequestResponse))]
Expand Down
38 changes: 35 additions & 3 deletions dotnet/src/CopilotTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public static class CopilotTool
/// <summary>The key used in <see cref="AITool.AdditionalProperties"/> to indicate that a tool can execute without a permission prompt.</summary>
internal const string SkipPermissionKey = "skip_permission";

/// <summary>The key used in <see cref="AITool.AdditionalProperties"/> to carry the tool's <see cref="CopilotToolDefer"/> deferral mode.</summary>
internal const string DeferKey = "defer";

/// <summary>
/// Defines a tool for use in a <see cref="CopilotSession"/>.
/// </summary>
Expand Down Expand Up @@ -84,7 +87,7 @@ static void ApplyToolInvocationBinding(AIFunctionFactoryOptions factoryOptions)

static void ApplyToolOptions(AIFunctionFactoryOptions factoryOptions, CopilotToolOptions? toolOptions)
{
if (toolOptions is not null && (toolOptions.OverridesBuiltInTool || toolOptions.SkipPermission))
if (toolOptions is not null && (toolOptions.OverridesBuiltInTool || toolOptions.SkipPermission || toolOptions.Defer is not null))
{
Dictionary<string, object?> additionalProperties = new(StringComparer.Ordinal);
if (factoryOptions.AdditionalProperties is not null)
Expand All @@ -105,6 +108,11 @@ static void ApplyToolOptions(AIFunctionFactoryOptions factoryOptions, CopilotToo
additionalProperties[SkipPermissionKey] = true;
}

if (toolOptions.Defer is { } defer)
{
additionalProperties[DeferKey] = defer;
}

factoryOptions.AdditionalProperties = additionalProperties;
}
}
Expand All @@ -121,7 +129,7 @@ public sealed class CopilotToolOptions
/// Gets or sets a value indicating whether this tool intentionally overrides a built-in Copilot tool with the same name.
/// </summary>
/// <remarks>
/// When a <see cref="CopilotToolOptions"/> with <see cref="OverridesBuiltInTool"/> set to true is used to define a tool,
/// When a <see cref="CopilotToolOptions"/> with <see cref="OverridesBuiltInTool"/> set to true is used to define a tool,
/// the resulting <see cref="AIFunction"/> will include "is_override": true in its <see cref="AITool.AdditionalProperties"/>.
/// </remarks>
public bool OverridesBuiltInTool { get; set; }
Expand All @@ -130,8 +138,32 @@ public sealed class CopilotToolOptions
/// Gets or sets a value indicating whether this tool can execute without a permission prompt.
/// </summary>
/// <remarks>
/// When a <see cref="CopilotToolOptions"/> with <see cref="SkipPermission"/> set to true is used to define a tool,
/// When a <see cref="CopilotToolOptions"/> with <see cref="SkipPermission"/> set to true is used to define a tool,
/// the resulting <see cref="AIFunction"/> will include "skip_permission": true in its <see cref="AITool.AdditionalProperties"/>.
/// </remarks>
public bool SkipPermission { get; set; }

/// <summary>
/// Gets or sets a value controlling whether this tool may be deferred (loaded lazily via tool search) rather than always pre-loaded.
/// </summary>
/// <remarks>
/// When set, the resulting <see cref="AIFunction"/> carries the value in its <see cref="AITool.AdditionalProperties"/> and the
/// SDK forwards it to the CLI as the tool's <c>defer</c> mode. Defaults to "auto".
/// </remarks>
public CopilotToolDefer? Defer { get; set; }
}

/// <summary>
/// Controls whether a tool may be deferred (loaded lazily via tool search) rather than always pre-loaded.
/// </summary>
[System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter<CopilotToolDefer>))]
public enum CopilotToolDefer
{
/// <summary>The tool can be deferred and surfaced through tool search.</summary>
[System.Text.Json.Serialization.JsonStringEnumMemberName("auto")]
Auto,

/// <summary>The tool is always pre-loaded.</summary>
[System.Text.Json.Serialization.JsonStringEnumMemberName("never")]
Never
}
6 changes: 5 additions & 1 deletion dotnet/test/Unit/CopilotToolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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]
Expand All @@ -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]
Expand Down
12 changes: 12 additions & 0 deletions go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
41 changes: 41 additions & 0 deletions go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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__"}})
Expand Down
15 changes: 15 additions & 0 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:"-"`
Expand Down
69 changes: 69 additions & 0 deletions java/src/main/java/com/github/copilot/rpc/ToolDefer.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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);
}
}
38 changes: 34 additions & 4 deletions java/src/main/java/com/github/copilot/rpc/ToolDefinition.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Comment thread
almaleksia marked this conversation as resolved.

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

/**
Expand All @@ -108,7 +112,7 @@ public static ToolDefinition create(String name, String description, Map<String,
*/
public static ToolDefinition createOverride(String name, String description, Map<String, Object> schema,
ToolHandler handler) {
return new ToolDefinition(name, description, schema, handler, true, null);
return new ToolDefinition(name, description, schema, handler, true, null, null);
}

/**
Expand All @@ -131,6 +135,32 @@ public static ToolDefinition createOverride(String name, String description, Map
*/
public static ToolDefinition createSkipPermission(String name, String description, Map<String, Object> 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.
* <p>
* 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<String, Object> schema,
ToolHandler handler, ToolDefer defer) {
return new ToolDefinition(name, description, schema, handler, null, null, defer);
}
}
Loading
Loading