feat: Support overriding built-in tools#636
Conversation
Auto-add user-registered tool names to excludedTools in session.create/resume RPC payloads so that SDK-registered tools override CLI built-in tools. - Node.js: mergeExcludedTools() helper + createSession/resumeSession updates - Python: inline merge logic in create_session/resume_session - Go: mergeExcludedTools() helper + CreateSession/ResumeSessionWithOptions updates - .NET: MergeExcludedTools() helper + CreateSessionAsync/ResumeSessionAsync updates - Tests added for all 4 SDKs - All 4 READMEs updated with "Overriding Built-in Tools" documentation Co-authored-by: patniko <26906478+patniko@users.noreply.github.com>
Add E2E tests across all 4 SDKs verifying that registering a custom tool with the same name as a built-in tool (e.g., 'grep') causes the custom tool to be invoked instead of the built-in. This validates the mergeExcludedTools feature end-to-end. - Add 'overrides built-in tool with custom tool' test to Node, Python, Go, .NET - Add YAML snapshot for the replay proxy - Add test/scenarios/tools/tool-overrides/ with all 4 language implementations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix Python tests: add missing required argument to create_session() calls - Fix Go: separate misplaced buildUnsupportedToolResult doc comment from mergeExcludedTools - Fix Go sample: whitespace alignment from merge Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Instead of merging SDK tool names into excludedTools (which semantically abuses the exclude mechanism), each tool now declares overridesBuiltInTool when it intentionally replaces a built-in tool. The runtime uses this flag to detect accidental name clashes (error) vs intentional overrides. Changes across all 4 SDKs (Node.js, Python, Go, .NET): - Add overridesBuiltInTool field to tool definitions - Remove mergeExcludedTools / inline merge logic - Pass overridesBuiltInTool through wire protocol to runtime - Update tests and documentation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove TestMergeExcludedTools from go/client_test.go (tests deleted function) - Revert excluded_tools handling in python/copilot/client.py to 'is not None' - Rename MergeExcludedToolsTests.cs to OverridesBuiltInToolTests.cs to match class Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
dotnet/README.md
Outdated
|
|
||
| #### 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. |
There was a problem hiding this comment.
Is there a way to query for all built-in tools?
There was a problem hiding this comment.
Not currently. Can you say what the use case would be?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Pull request overview
Adds first-class support across the SDKs for intentionally overriding Copilot CLI built-in tools by name, using an explicit opt-in flag/setting so accidental name clashes become errors while intentional overrides work.
Changes:
- Add
overridesBuiltInTool(or equivalent) to tool definitions and forward it over the JSON-RPC wire protocol. - Update SDK READMEs plus add scenario samples demonstrating a custom tool overriding a built-in tool.
- Add unit + E2E coverage (including a new replay snapshot) for the override behavior.
Reviewed changes
Copilot reviewed 32 out of 33 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/snapshots/tools/overrides_built_in_tool_with_custom_tool.yaml | New replay snapshot covering a custom grep tool override flow. |
| test/scenarios/tools/tool-overrides/verify.sh | Scenario verification script that builds/runs all 4 language samples and checks for override output. |
| test/scenarios/tools/tool-overrides/typescript/src/index.ts | TypeScript scenario showing overridesBuiltInTool: true on a custom grep tool. |
| test/scenarios/tools/tool-overrides/typescript/package.json | Scenario package wiring to local Node SDK build. |
| test/scenarios/tools/tool-overrides/python/requirements.txt | Scenario requirements pointing at editable local Python SDK. |
| test/scenarios/tools/tool-overrides/python/main.py | Python scenario showing overrides_built_in_tool=True on a custom grep tool. |
| test/scenarios/tools/tool-overrides/go/main.go | Go scenario showing OverridesBuiltInTool = true on a custom grep tool. |
| test/scenarios/tools/tool-overrides/go/go.mod | Go scenario module with local replace to repo Go SDK. |
| test/scenarios/tools/tool-overrides/go/go.sum | Go scenario dependency checksums. |
| test/scenarios/tools/tool-overrides/csharp/csharp.csproj | C# scenario project referencing the local .NET SDK project. |
| test/scenarios/tools/tool-overrides/csharp/Program.cs | C# scenario demonstrating override via session config BuiltInToolOverrides. |
| test/scenarios/tools/tool-overrides/README.md | Scenario documentation for running/verifying tool overrides. |
| python/test_client.py | Python unit tests validating overridesBuiltInTool serialization behavior. |
| python/e2e/test_tools.py | Python E2E test for overriding a built-in tool with a custom tool. |
| python/copilot/types.py | Adds overrides_built_in_tool to the Python Tool dataclass. |
| python/copilot/tools.py | Extends define_tool to accept overrides_built_in_tool. |
| python/copilot/client.py | Forwards overridesBuiltInTool in tool definitions over the wire protocol. |
| python/README.md | Documents how to opt into overriding built-in tools in Python. |
| nodejs/test/e2e/tools.test.ts | Node E2E test for built-in tool override behavior. |
| nodejs/test/client.test.ts | Node unit tests verifying overridesBuiltInTool is sent and excluded tools aren’t auto-mutated. |
| nodejs/src/types.ts | Adds overridesBuiltInTool?: boolean to Tool / defineTool types. |
| nodejs/src/client.ts | Passes overridesBuiltInTool through to session.create / session.resume. |
| nodejs/README.md | Documents how to opt into overriding built-in tools in Node/TS. |
| go/types.go | Adds OverridesBuiltInTool to Go Tool struct for JSON serialization. |
| go/internal/e2e/tools_test.go | Go E2E test for overriding a built-in tool with a custom tool. |
| go/client_test.go | Go unit tests for tool serialization plus additional excluded-tools merging tests. |
| go/README.md | Documents how to opt into overriding built-in tools in Go. |
| dotnet/test/ToolsTests.cs | .NET E2E-style test verifying custom tool override works. |
| dotnet/test/MergeExcludedToolsTests.cs | .NET unit tests for override flag plumbing + session config surface area. |
| dotnet/src/Types.cs | Adds BuiltInToolOverrides to session configs and copy constructors. |
| dotnet/src/GitHub.Copilot.SDK.csproj | Adds InternalsVisibleTo for SDK test assembly access. |
| dotnet/src/Client.cs | Sets OverridesBuiltInTool on serialized tool definitions based on session config. |
| dotnet/README.md | Documents how to opt into overriding built-in tools in .NET. |
Comments suppressed due to low confidence (3)
python/copilot/client.py:503
excluded_toolsis alist[str]in SessionConfig; converting vialist(cfg.get("excluded_tools") or [])changes semantics (explicit[]becomes omitted) and can also accidentally turn a string into a list of characters. Prefer preserving the original value and using anis not Nonecheck (matchingavailable_tools) or validating the type before serializing.
This issue also appears on line 681 of the same file.
excluded_tools = cfg.get("excluded_tools")
if excluded_tools is not None:
payload["excludedTools"] = excluded_tools
python/copilot/client.py:683
- Same issue as in
create_session:excluded_tools = list(cfg.get("excluded_tools") or [])can change caller intent and coerce unexpected types. Preserve the provided list (including empty) and only omit the field when the key is truly absent/None.
excluded_tools = cfg.get("excluded_tools")
if excluded_tools is not None:
payload["excludedTools"] = excluded_tools
go/client_test.go:577
- The new tests call
mergeExcludedTools(...), but no such function exists in the Go package (searching the repo shows only these call sites). This will fail to compile the test suite; either add the helper to the production code (if intended) or remove/adjust these tests to target the actual behavior.
| @overload | ||
| def define_tool( | ||
| name: str | None = None, | ||
| *, | ||
| description: str | None = None, | ||
| ) -> Callable[[Callable[..., Any]], Tool]: ... | ||
|
|
||
|
|
||
| @overload | ||
| def define_tool( | ||
| name: str, | ||
| *, | ||
| description: str | None = None, | ||
| handler: Callable[[T, ToolInvocation], R], | ||
| params_type: type[T], | ||
| ) -> Tool: ... | ||
|
|
||
|
|
||
| def define_tool( | ||
| name: str | None = None, | ||
| *, | ||
| description: str | None = None, | ||
| handler: Callable[[Any, ToolInvocation], Any] | None = None, | ||
| params_type: type[BaseModel] | None = None, | ||
| overrides_built_in_tool: bool = False, | ||
| ) -> Tool | Callable[[Callable[[Any, ToolInvocation], Any]], Tool]: |
There was a problem hiding this comment.
define_tool’s overload signatures don’t include the new overrides_built_in_tool keyword-only parameter, so type checkers will flag valid calls (and docs/tests added in this PR already use it). Update both overloads to accept overrides_built_in_tool: bool = False.
| | Option | Value | Effect | | ||
| |--------|-------|--------| | ||
| | `tools` | Custom `grep` tool | Provides a custom grep implementation | | ||
| | `overridesBuiltInTool` | `true` | Tells the SDK to disable the built-in `grep` in favor of the custom one | | ||
|
|
There was a problem hiding this comment.
Markdown table rows start with || which adds an unintended empty column / breaks typical table formatting. Use a single leading | for the header and separator rows.
…ove stale tests - .NET: Read is_override from AIFunction.AdditionalProperties instead of BuiltInToolOverrides on session config. Remove BuiltInToolOverrides from SessionConfig and ResumeSessionConfig. - Remove negative tests asserting old mergeExcludedTools behavior is gone (Node: 'does not merge tool names', 'preserves user-specified excludedTools'; Python: test_does_not_merge_tool_names, test_preserves_user_excluded_tools) - Update README and scenario examples - Run formatters (gofmt, ruff, prettier, dotnet format) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…d signatures - Remove OverridesBuiltInToolTests.cs (E2E test in ToolsTests.cs covers this) - Remove InternalsVisibleTo from csproj (no longer needed) - Add overrides_built_in_tool param to both define_tool overload signatures Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
✅ Cross-SDK Consistency Review: PASSEDI've completed a comprehensive consistency review of this PR across all 4 SDK implementations (Node.js/TypeScript, Python, Go, and .NET). Review SummaryThis PR adds support for overriding built-in tools with the ✅ Feature ImplementationAll 4 SDKs implement the feature following their language-specific conventions:
✅ Wire Protocol ConsistencyAll SDKs correctly serialize to the same JSON field: ✅ DocumentationAll 4 READMEs include an "Overriding Built-in Tools" section with:
✅ Test Coverage
ConclusionNo consistency issues found. This PR is a model example of maintaining feature parity across multi-language SDK implementations. The author has:
Great work! 🎉
|
Summary
Adds support for SDK-registered tools to override CLI built-in tools of the same name (Issue #411).
Each tool can now declare
overridesBuiltInTool: trueto intentionally replace a built-in tool. The runtime uses this flag to detect accidental name clashes (error) vs intentional overrides.Changes across all 4 SDKs (Node.js, Python, Go, .NET)
overridesBuiltInToolfield to tool definitionsoverridesBuiltInToolthrough wire protocol to runtimeCloses #411