Skip to content

Commit f843c80

Browse files
Support overriding built-in tools (#636)
1 parent bd98e3a commit f843c80

File tree

31 files changed

+782
-9
lines changed

31 files changed

+782
-9
lines changed

dotnet/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,30 @@ var session = await client.CreateSessionAsync(new SessionConfig
415415

416416
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.
417417

418+
#### Overriding Built-in Tools
419+
420+
If you register a tool with the same name as a built-in CLI tool (e.g. `edit_file`, `read_file`), the runtime will return an error unless you explicitly opt in by setting `is_override` in the tool's `AdditionalProperties`. This flag signals that you intend to replace the built-in tool with your custom implementation.
421+
422+
```csharp
423+
var editFile = AIFunctionFactory.Create(
424+
async ([Description("File path")] string path, [Description("New content")] string content) => {
425+
// your logic
426+
},
427+
"edit_file",
428+
"Custom file editor with project-specific validation",
429+
new AIFunctionFactoryOptions
430+
{
431+
AdditionalProperties = new ReadOnlyDictionary<string, object?>(
432+
new Dictionary<string, object?> { ["is_override"] = true })
433+
});
434+
435+
var session = await client.CreateSessionAsync(new SessionConfig
436+
{
437+
Model = "gpt-5",
438+
Tools = [editFile],
439+
});
440+
```
441+
418442
### System Message Customization
419443

420444
Control the system prompt using `SystemMessage` in session config:

dotnet/src/Client.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,10 +1416,15 @@ internal record CreateSessionRequest(
14161416
internal record ToolDefinition(
14171417
string Name,
14181418
string? Description,
1419-
JsonElement Parameters /* JSON schema */)
1419+
JsonElement Parameters, /* JSON schema */
1420+
bool? OverridesBuiltInTool = null)
14201421
{
14211422
public static ToolDefinition FromAIFunction(AIFunction function)
1422-
=> new ToolDefinition(function.Name, function.Description, function.JsonSchema);
1423+
{
1424+
var overrides = function.AdditionalProperties.TryGetValue("is_override", out var val) && val is true;
1425+
return new ToolDefinition(function.Name, function.Description, function.JsonSchema,
1426+
overrides ? true : null);
1427+
}
14231428
}
14241429

14251430
internal record CreateSessionResponse(

dotnet/src/Types.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,7 @@ protected SessionConfig(SessionConfig? other)
802802
public string? ConfigDir { get; set; }
803803

804804
public ICollection<AIFunction>? Tools { get; set; }
805+
805806
public SystemMessageConfig? SystemMessage { get; set; }
806807
public List<string>? AvailableTools { get; set; }
807808
public List<string>? ExcludedTools { get; set; }

dotnet/test/ToolsTests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using GitHub.Copilot.SDK.Test.Harness;
66
using Microsoft.Extensions.AI;
7+
using System.Collections.ObjectModel;
78
using System.ComponentModel;
89
using System.Linq;
910
using System.Text.Json;
@@ -152,6 +153,34 @@ record City(int CountryId, string CityName, int Population);
152153
[JsonSerializable(typeof(JsonElement))]
153154
private partial class ToolsTestsJsonContext : JsonSerializerContext;
154155

156+
[Fact]
157+
public async Task Overrides_Built_In_Tool_With_Custom_Tool()
158+
{
159+
var session = await CreateSessionAsync(new SessionConfig
160+
{
161+
Tools = [AIFunctionFactory.Create((Delegate)CustomGrep, new AIFunctionFactoryOptions
162+
{
163+
Name = "grep",
164+
AdditionalProperties = new ReadOnlyDictionary<string, object?>(
165+
new Dictionary<string, object?> { ["is_override"] = true })
166+
})],
167+
OnPermissionRequest = PermissionHandler.ApproveAll,
168+
});
169+
170+
await session.SendAsync(new MessageOptions
171+
{
172+
Prompt = "Use grep to search for the word 'hello'"
173+
});
174+
175+
var assistantMessage = await TestHelper.GetFinalAssistantMessageAsync(session);
176+
Assert.NotNull(assistantMessage);
177+
Assert.Contains("CUSTOM_GREP_RESULT", assistantMessage!.Data.Content ?? string.Empty);
178+
179+
[Description("A custom grep implementation that overrides the built-in")]
180+
static string CustomGrep([Description("Search query")] string query)
181+
=> $"CUSTOM_GREP_RESULT: {query}";
182+
}
183+
155184
[Fact(Skip = "Behaves as if no content was in the result. Likely that binary results aren't fully implemented yet.")]
156185
public async Task Can_Return_Binary_Result()
157186
{

go/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,18 @@ session, _ := client.CreateSession(context.Background(), &copilot.SessionConfig{
267267

268268
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.
269269

270+
#### Overriding Built-in Tools
271+
272+
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.
273+
274+
```go
275+
editFile := copilot.DefineTool("edit_file", "Custom file editor with project-specific validation",
276+
func(params EditFileParams, inv copilot.ToolInvocation) (any, error) {
277+
// your logic
278+
})
279+
editFile.OverridesBuiltInTool = true
280+
```
281+
270282
## Streaming
271283

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

451+
func TestOverridesBuiltInTool(t *testing.T) {
452+
t.Run("OverridesBuiltInTool is serialized in tool definition", func(t *testing.T) {
453+
tool := Tool{
454+
Name: "grep",
455+
Description: "Custom grep",
456+
OverridesBuiltInTool: true,
457+
Handler: func(_ ToolInvocation) (ToolResult, error) { return ToolResult{}, nil },
458+
}
459+
data, err := json.Marshal(tool)
460+
if err != nil {
461+
t.Fatalf("failed to marshal: %v", err)
462+
}
463+
var m map[string]any
464+
if err := json.Unmarshal(data, &m); err != nil {
465+
t.Fatalf("failed to unmarshal: %v", err)
466+
}
467+
if v, ok := m["overridesBuiltInTool"]; !ok || v != true {
468+
t.Errorf("expected overridesBuiltInTool=true, got %v", m)
469+
}
470+
})
471+
472+
t.Run("OverridesBuiltInTool omitted when false", func(t *testing.T) {
473+
tool := Tool{
474+
Name: "custom_tool",
475+
Description: "A custom tool",
476+
Handler: func(_ ToolInvocation) (ToolResult, error) { return ToolResult{}, nil },
477+
}
478+
data, err := json.Marshal(tool)
479+
if err != nil {
480+
t.Fatalf("failed to marshal: %v", err)
481+
}
482+
var m map[string]any
483+
if err := json.Unmarshal(data, &m); err != nil {
484+
t.Fatalf("failed to unmarshal: %v", err)
485+
}
486+
if _, ok := m["overridesBuiltInTool"]; ok {
487+
t.Errorf("expected overridesBuiltInTool to be omitted, got %v", m)
488+
}
489+
})
490+
}
491+
451492
func TestClient_CreateSession_RequiresPermissionHandler(t *testing.T) {
452493
t.Run("returns error when config is nil", func(t *testing.T) {
453494
client := NewClient(nil)

go/internal/e2e/tools_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,44 @@ func TestTools(t *testing.T) {
264264
}
265265
})
266266

267+
t.Run("overrides built-in tool with custom tool", func(t *testing.T) {
268+
ctx.ConfigureForTest(t)
269+
270+
type GrepParams struct {
271+
Query string `json:"query" jsonschema:"Search query"`
272+
}
273+
274+
grepTool := copilot.DefineTool("grep", "A custom grep implementation that overrides the built-in",
275+
func(params GrepParams, inv copilot.ToolInvocation) (string, error) {
276+
return "CUSTOM_GREP_RESULT: " + params.Query, nil
277+
})
278+
grepTool.OverridesBuiltInTool = true
279+
280+
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
281+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
282+
Tools: []copilot.Tool{
283+
grepTool,
284+
},
285+
})
286+
if err != nil {
287+
t.Fatalf("Failed to create session: %v", err)
288+
}
289+
290+
_, err = session.Send(t.Context(), copilot.MessageOptions{Prompt: "Use grep to search for the word 'hello'"})
291+
if err != nil {
292+
t.Fatalf("Failed to send message: %v", err)
293+
}
294+
295+
answer, err := testharness.GetFinalAssistantMessage(t.Context(), session)
296+
if err != nil {
297+
t.Fatalf("Failed to get assistant message: %v", err)
298+
}
299+
300+
if answer.Data.Content == nil || !strings.Contains(*answer.Data.Content, "CUSTOM_GREP_RESULT") {
301+
t.Errorf("Expected answer to contain 'CUSTOM_GREP_RESULT', got %v", answer.Data.Content)
302+
}
303+
})
304+
267305
t.Run("invokes custom tool with permission handler", func(t *testing.T) {
268306
ctx.ConfigureForTest(t)
269307

go/types.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -410,10 +410,11 @@ type SessionConfig struct {
410410

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

419420
// ToolInvocation describes a tool call initiated by Copilot

nodejs/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,19 @@ const session = await client.createSession({
402402

403403
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.
404404

405+
#### Overriding Built-in Tools
406+
407+
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.
408+
409+
```ts
410+
defineTool("edit_file", {
411+
description: "Custom file editor with project-specific validation",
412+
parameters: z.object({ path: z.string(), content: z.string() }),
413+
overridesBuiltInTool: true,
414+
handler: async ({ path, content }) => { /* your logic */ },
415+
})
416+
```
417+
405418
### System Message Customization
406419

407420
Control the system prompt using `systemMessage` in session config:

nodejs/src/client.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,7 @@ export class CopilotClient {
533533
name: tool.name,
534534
description: tool.description,
535535
parameters: toJsonSchema(tool.parameters),
536+
overridesBuiltInTool: tool.overridesBuiltInTool,
536537
})),
537538
systemMessage: config.systemMessage,
538539
availableTools: config.availableTools,
@@ -621,6 +622,7 @@ export class CopilotClient {
621622
name: tool.name,
622623
description: tool.description,
623624
parameters: toJsonSchema(tool.parameters),
625+
overridesBuiltInTool: tool.overridesBuiltInTool,
624626
})),
625627
provider: config.provider,
626628
requestPermission: true,

0 commit comments

Comments
 (0)