Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
21 changes: 21 additions & 0 deletions dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,27 @@ var session = await client.CreateSessionAsync(new SessionConfig

When Copilot invokes `lookup_issue`, the client automatically runs your handler and responds to the CLI. Handlers can return any JSON-serializable value (automatically wrapped), or a `ToolResultAIContent` wrapping a `ToolResultObject` for full control over result metadata.

#### Overriding Built-in Tools

If you register a tool with the same name as a built-in CLI tool (e.g. `edit_file`, `read_file`), the SDK will throw an error unless you explicitly opt in by setting `BuiltInToolOverrides` on the session config. This flag signals that you intend to replace the built-in tool with your custom implementation.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to query for all built-in tools?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not currently. Can you say what the use case would be?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if there's a) a way I can proactively determine whether I'm going to get conflicts with built-in tools or if it'll just be "oops, that one conflicts now, better change it" and b) whether I can determine what tools are in use to see whether I might want to replace one with my own logic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK thanks for explaining. I suspect even then it might be tricky to make a programmatic decision since it would depend on understanding the semantics and usage patterns of the built-in tool. But we can certainly consider adding a way to enumerate them if we get demand for it.


```csharp
// Register the custom tool
AIFunctionFactory.Create(
async ([Description("File path")] string path, [Description("New content")] string content) => {
// your logic
},
"edit_file",
"Custom file editor with project-specific validation")

// Opt in to overriding the built-in tool in session config
var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5",
BuiltInToolOverrides = new HashSet<string> { "edit_file" }
});
```

### System Message Customization

Control the system prompt using `SystemMessage` in session config:
Expand Down
14 changes: 9 additions & 5 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,8 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
config.SessionId,
config.ClientName,
config.ReasoningEffort,
config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
config.Tools?.Select(f => ToolDefinition.FromAIFunction(f,
config.BuiltInToolOverrides?.Contains(f.Name) == true)).ToList(),
config.SystemMessage,
config.AvailableTools,
config.ExcludedTools,
Expand Down Expand Up @@ -477,7 +478,8 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
config.ClientName,
config.Model,
config.ReasoningEffort,
config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
config.Tools?.Select(f => ToolDefinition.FromAIFunction(f,
config.BuiltInToolOverrides?.Contains(f.Name) == true)).ToList(),
config.SystemMessage,
config.AvailableTools,
config.ExcludedTools,
Expand Down Expand Up @@ -1416,10 +1418,12 @@ internal record CreateSessionRequest(
internal record ToolDefinition(
string Name,
string? Description,
JsonElement Parameters /* JSON schema */)
JsonElement Parameters, /* JSON schema */
bool? OverridesBuiltInTool = null)
{
public static ToolDefinition FromAIFunction(AIFunction function)
=> new ToolDefinition(function.Name, function.Description, function.JsonSchema);
public static ToolDefinition FromAIFunction(AIFunction function, bool overridesBuiltInTool = false)
=> new ToolDefinition(function.Name, function.Description, function.JsonSchema,
overridesBuiltInTool ? true : null);
}

internal record CreateSessionResponse(
Expand Down
4 changes: 4 additions & 0 deletions dotnet/src/GitHub.Copilot.SDK.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
<IsAotCompatible>true</IsAotCompatible>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="GitHub.Copilot.SDK.Test" />
</ItemGroup>

<ItemGroup>
<None Include="../README.md" Pack="true" PackagePath="/" />
</ItemGroup>
Expand Down
17 changes: 17 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,7 @@ protected SessionConfig(SessionConfig? other)
Streaming = other.Streaming;
SystemMessage = other.SystemMessage;
Tools = other.Tools is not null ? [.. other.Tools] : null;
BuiltInToolOverrides = other.BuiltInToolOverrides is not null ? [.. other.BuiltInToolOverrides] : null;
WorkingDirectory = other.WorkingDirectory;
}

Expand Down Expand Up @@ -802,6 +803,14 @@ protected SessionConfig(SessionConfig? other)
public string? ConfigDir { get; set; }

public ICollection<AIFunction>? Tools { get; set; }

/// <summary>
/// Set of tool names that are intended to override built-in tools of the same name.
/// If a tool name clashes with a built-in tool and is not in this set, the runtime
/// will return an error.
/// </summary>
public HashSet<string>? BuiltInToolOverrides { get; set; }

public SystemMessageConfig? SystemMessage { get; set; }
public List<string>? AvailableTools { get; set; }
public List<string>? ExcludedTools { get; set; }
Expand Down Expand Up @@ -912,6 +921,7 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)
Streaming = other.Streaming;
SystemMessage = other.SystemMessage;
Tools = other.Tools is not null ? [.. other.Tools] : null;
BuiltInToolOverrides = other.BuiltInToolOverrides is not null ? [.. other.BuiltInToolOverrides] : null;
WorkingDirectory = other.WorkingDirectory;
}

Expand All @@ -928,6 +938,13 @@ protected ResumeSessionConfig(ResumeSessionConfig? other)

public ICollection<AIFunction>? Tools { get; set; }

/// <summary>
/// Set of tool names that are intended to override built-in tools of the same name.
/// If a tool name clashes with a built-in tool and is not in this set, the runtime
/// will return an error.
/// </summary>
public HashSet<string>? BuiltInToolOverrides { get; set; }

/// <summary>
/// System message configuration.
/// </summary>
Expand Down
61 changes: 61 additions & 0 deletions dotnet/test/OverridesBuiltInToolTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

using Microsoft.Extensions.AI;
using System.ComponentModel;
using System.Text.Json;
using Xunit;

namespace GitHub.Copilot.SDK.Test;

public class OverridesBuiltInToolTests
{
[Fact]
public void ToolDefinition_FromAIFunction_Sets_OverridesBuiltInTool()
{
var fn = AIFunctionFactory.Create(Noop, "grep");
var def = CopilotClient.ToolDefinition.FromAIFunction(fn, overridesBuiltInTool: true);

Assert.Equal("grep", def.Name);
Assert.True(def.OverridesBuiltInTool);
}

[Fact]
public void ToolDefinition_FromAIFunction_Omits_OverridesBuiltInTool_When_False()
{
var fn = AIFunctionFactory.Create(Noop, "custom_tool");
var def = CopilotClient.ToolDefinition.FromAIFunction(fn, overridesBuiltInTool: false);

Assert.Equal("custom_tool", def.Name);
Assert.Null(def.OverridesBuiltInTool);
}

[Fact]
public void SessionConfig_BuiltInToolOverrides_Is_Used()
{
var config = new SessionConfig
{
Tools = new List<AIFunction> { AIFunctionFactory.Create(Noop, "grep") },
BuiltInToolOverrides = new HashSet<string> { "grep" },
};

Assert.Contains("grep", config.BuiltInToolOverrides);
}

[Fact]
public void ResumeSessionConfig_BuiltInToolOverrides_Is_Used()
{
var config = new ResumeSessionConfig
{
Tools = new List<AIFunction> { AIFunctionFactory.Create(Noop, "grep") },
BuiltInToolOverrides = new HashSet<string> { "grep" },
};

Assert.NotNull(config.BuiltInToolOverrides);
Assert.Contains("grep", config.BuiltInToolOverrides!);
}

[Description("No-op")]
static string Noop() => "";
}
24 changes: 24 additions & 0 deletions dotnet/test/ToolsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,30 @@ record City(int CountryId, string CityName, int Population);
[JsonSerializable(typeof(JsonElement))]
private partial class ToolsTestsJsonContext : JsonSerializerContext;

[Fact]
public async Task Overrides_Built_In_Tool_With_Custom_Tool()
{
var session = await CreateSessionAsync(new SessionConfig
{
Tools = [AIFunctionFactory.Create(CustomGrep, "grep")],
BuiltInToolOverrides = ["grep"],
OnPermissionRequest = PermissionHandler.ApproveAll,
});

await session.SendAsync(new MessageOptions
{
Prompt = "Use grep to search for the word 'hello'"
});

var assistantMessage = await TestHelper.GetFinalAssistantMessageAsync(session);
Assert.NotNull(assistantMessage);
Assert.Contains("CUSTOM_GREP_RESULT", assistantMessage!.Data.Content ?? string.Empty);

[Description("A custom grep implementation that overrides the built-in")]
static string CustomGrep([Description("Search query")] string query)
=> $"CUSTOM_GREP_RESULT: {query}";
}

[Fact(Skip = "Behaves as if no content was in the result. Likely that binary results aren't fully implemented yet.")]
public async Task Can_Return_Binary_Result()
{
Expand Down
12 changes: 12 additions & 0 deletions go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,18 @@ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{

When the model selects a tool, the SDK automatically runs your handler (in parallel with other calls) and responds to the CLI's `tool.call` with the handler's result.

#### Overriding Built-in Tools

If you register a tool with the same name as a built-in CLI tool (e.g. `edit_file`, `read_file`), the SDK will throw an error unless you explicitly opt in by setting `OverridesBuiltInTool = true`. This flag signals that you intend to replace the built-in tool with your custom implementation.

```go
editFile := copilot.DefineTool("edit_file", "Custom file editor with project-specific validation",
func(params EditFileParams, inv copilot.ToolInvocation) (any, error) {
// your logic
})
editFile.OverridesBuiltInTool = true
```

## 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 @@ -448,6 +448,47 @@ func TestResumeSessionRequest_ClientName(t *testing.T) {
})
}

func TestOverridesBuiltInTool(t *testing.T) {
t.Run("OverridesBuiltInTool is serialized in tool definition", func(t *testing.T) {
tool := Tool{
Name: "grep",
Description: "Custom grep",
OverridesBuiltInTool: 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 v, ok := m["overridesBuiltInTool"]; !ok || v != true {
t.Errorf("expected overridesBuiltInTool=true, got %v", m)
}
})

t.Run("OverridesBuiltInTool omitted when false", 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["overridesBuiltInTool"]; ok {
t.Errorf("expected overridesBuiltInTool to be omitted, got %v", m)
}
})
}

func TestClient_CreateSession_RequiresPermissionHandler(t *testing.T) {
t.Run("returns error when config is nil", func(t *testing.T) {
client := NewClient(nil)
Expand Down
38 changes: 38 additions & 0 deletions go/internal/e2e/tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,44 @@ func TestTools(t *testing.T) {
}
})

t.Run("overrides built-in tool with custom tool", func(t *testing.T) {
ctx.ConfigureForTest(t)

type GrepParams struct {
Query string `json:"query" jsonschema:"Search query"`
}

grepTool := copilot.DefineTool("grep", "A custom grep implementation that overrides the built-in",
func(params GrepParams, inv copilot.ToolInvocation) (string, error) {
return "CUSTOM_GREP_RESULT: " + params.Query, nil
})
grepTool.OverridesBuiltInTool = true

session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
Tools: []copilot.Tool{
grepTool,
},
})
if err != nil {
t.Fatalf("Failed to create session: %v", err)
}

_, err = session.Send(t.Context(), copilot.MessageOptions{Prompt: "Use grep to search for the word 'hello'"})
if err != nil {
t.Fatalf("Failed to send message: %v", err)
}

answer, err := testharness.GetFinalAssistantMessage(t.Context(), session)
if err != nil {
t.Fatalf("Failed to get assistant message: %v", err)
}

if answer.Data.Content == nil || !strings.Contains(*answer.Data.Content, "CUSTOM_GREP_RESULT") {
t.Errorf("Expected answer to contain 'CUSTOM_GREP_RESULT', got %v", answer.Data.Content)
}
})

t.Run("invokes custom tool with permission handler", func(t *testing.T) {
ctx.ConfigureForTest(t)

Expand Down
9 changes: 5 additions & 4 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,10 +410,11 @@ type SessionConfig struct {

// Tool describes a caller-implemented tool that can be invoked by Copilot
type Tool struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
Parameters map[string]any `json:"parameters,omitempty"`
Handler ToolHandler `json:"-"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Parameters map[string]any `json:"parameters,omitempty"`
OverridesBuiltInTool bool `json:"overridesBuiltInTool,omitempty"`
Handler ToolHandler `json:"-"`
}

// ToolInvocation describes a tool call initiated by Copilot
Expand Down
13 changes: 13 additions & 0 deletions nodejs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,19 @@ const session = await client.createSession({

When Copilot invokes `lookup_issue`, the client automatically runs your handler and responds to the CLI. Handlers can return any JSON-serializable value (automatically wrapped), a simple string, or a `ToolResultObject` for full control over result metadata. Raw JSON schemas are also supported if Zod isn't desired.

#### Overriding Built-in Tools

If you register a tool with the same name as a built-in CLI tool (e.g. `edit_file`, `read_file`), the SDK will throw an error unless you explicitly opt in by setting `overridesBuiltInTool: true`. This flag signals that you intend to replace the built-in tool with your custom implementation.

```ts
defineTool("edit_file", {
description: "Custom file editor with project-specific validation",
parameters: z.object({ path: z.string(), content: z.string() }),
overridesBuiltInTool: true,
handler: async ({ path, content }) => { /* your logic */ },
})
```

### System Message Customization

Control the system prompt using `systemMessage` in session config:
Expand Down
2 changes: 2 additions & 0 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ export class CopilotClient {
name: tool.name,
description: tool.description,
parameters: toJsonSchema(tool.parameters),
overridesBuiltInTool: tool.overridesBuiltInTool,
})),
systemMessage: config.systemMessage,
availableTools: config.availableTools,
Expand Down Expand Up @@ -621,6 +622,7 @@ export class CopilotClient {
name: tool.name,
description: tool.description,
parameters: toJsonSchema(tool.parameters),
overridesBuiltInTool: tool.overridesBuiltInTool,
})),
provider: config.provider,
requestPermission: true,
Expand Down
7 changes: 7 additions & 0 deletions nodejs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ export interface Tool<TArgs = unknown> {
description?: string;
parameters?: ZodSchema<TArgs> | Record<string, unknown>;
handler: ToolHandler<TArgs>;
/**
* When true, explicitly indicates this tool is intended 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.
*/
overridesBuiltInTool?: boolean;
}

/**
Expand All @@ -158,6 +164,7 @@ export function defineTool<T = unknown>(
description?: string;
parameters?: ZodSchema<T> | Record<string, unknown>;
handler: ToolHandler<T>;
overridesBuiltInTool?: boolean;
}
): Tool<T> {
return { name, ...config };
Expand Down
Loading
Loading