Skip to content

Commit 348f9e7

Browse files
authored
Tool search configuration support (#1933)
* Tool search configuration support * Fix ResumeSessionConfig * Fix spotless check * Fix Copilot comments * Fetch available tools for tool search * Addressing CI and alerts * Addressing review comments * Fix formatting * Fix spotless check
1 parent edbe6c6 commit 348f9e7

34 files changed

Lines changed: 990 additions & 20 deletions

dotnet/src/Client.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,6 +1137,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
11371137
InstructionDirectories: config.InstructionDirectories,
11381138
PluginDirectories: config.PluginDirectories,
11391139
LargeOutput: config.LargeOutput,
1140+
ToolSearch: config.ToolSearch,
11401141
Memory: config.Memory,
11411142
Canvases: config.Canvases,
11421143
RequestCanvasRenderer: config.RequestCanvasRenderer,
@@ -1348,6 +1349,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
13481349
InstructionDirectories: config.InstructionDirectories,
13491350
PluginDirectories: config.PluginDirectories,
13501351
LargeOutput: config.LargeOutput,
1352+
ToolSearch: config.ToolSearch,
13511353
Memory: config.Memory,
13521354
Canvases: config.Canvases,
13531355
RequestCanvasRenderer: config.RequestCanvasRenderer,
@@ -2689,6 +2691,7 @@ internal record CreateSessionRequest(
26892691
IList<string>? InstructionDirectories = null,
26902692
IList<string>? PluginDirectories = null,
26912693
LargeToolOutputConfig? LargeOutput = null,
2694+
ToolSearchConfig? ToolSearch = null,
26922695
MemoryConfiguration? Memory = null,
26932696
#pragma warning disable GHCP001
26942697
IList<CanvasDeclaration>? Canvases = null,
@@ -2790,6 +2793,7 @@ internal record ResumeSessionRequest(
27902793
IList<string>? InstructionDirectories = null,
27912794
IList<string>? PluginDirectories = null,
27922795
LargeToolOutputConfig? LargeOutput = null,
2796+
ToolSearchConfig? ToolSearch = null,
27932797
MemoryConfiguration? Memory = null,
27942798
#pragma warning disable GHCP001
27952799
IList<CanvasDeclaration>? Canvases = null,

dotnet/src/Session.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,13 @@ private sealed record EventSubscription(Type EventType, Action<SessionEvent> Han
9090
private readonly Channel<SessionEvent> _eventChannel = Channel.CreateUnbounded<SessionEvent>(
9191
new() { SingleReader = true });
9292

93+
/// <summary>
94+
/// Fixed name of the runtime's built-in tool-search tool. A client can
95+
/// replace its behavior by registering a tool with this exact name and
96+
/// <c>OverridesBuiltInTool</c> set to <c>true</c>.
97+
/// </summary>
98+
private const string ToolSearchToolName = "tool_search_tool";
99+
93100
/// <summary>
94101
/// Gets the unique identifier for this session.
95102
/// </summary>
@@ -841,6 +848,26 @@ private async Task ExecuteToolAndRespondAsync(string requestId, string toolName,
841848
Arguments = arguments
842849
};
843850

851+
// The built-in tool-search tool receives a snapshot of the session's
852+
// currently initialized tools so an override can filter the live
853+
// catalog without issuing its own RPC. Fetch it only for that tool
854+
// to avoid a round-trip on every tool call; a failed fetch leaves
855+
// the snapshot null rather than failing the tool.
856+
if (toolName == ToolSearchToolName)
857+
{
858+
try
859+
{
860+
var metadata = await Rpc.Tools.GetCurrentMetadataAsync();
861+
invocation.AvailableTools = metadata.Tools;
862+
}
863+
catch (Exception ex) when (ex is RemoteRpcException or IOException or ObjectDisposedException or JsonException)
864+
{
865+
// A failed metadata fetch is non-fatal: leave AvailableTools
866+
// null so the tool still runs without the snapshot.
867+
LogToolMetadataFetchFailed(ex, toolName);
868+
}
869+
}
870+
844871
var aiFunctionArgs = new AIFunctionArguments
845872
{
846873
Context = new Dictionary<object, object?>
@@ -1907,6 +1934,9 @@ await InvokeRpcAsync<object>(
19071934
[LoggerMessage(Level = LogLevel.Error, Message = "Unhandled exception in session event handler")]
19081935
private partial void LogEventHandlerError(Exception exception);
19091936

1937+
[LoggerMessage(Level = LogLevel.Debug, Message = "Failed to fetch tool metadata for {toolName}")]
1938+
private partial void LogToolMetadataFetchFailed(Exception exception, string toolName);
1939+
19101940
internal record SendMessageRequest
19111941
{
19121942
public string SessionId { get; init; } = string.Empty;

dotnet/src/Types.cs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,12 @@ public sealed class ToolResultObject
697697
[JsonPropertyName("toolTelemetry")]
698698
public IDictionary<string, object>? ToolTelemetry { get; set; }
699699

700+
/// <summary>
701+
/// Names of tools returned by a tool-search tool.
702+
/// </summary>
703+
[JsonPropertyName("toolReferences")]
704+
public IList<string>? ToolReferences { get; set; }
705+
700706
/// <summary>
701707
/// Converts the result of an <see cref="AIFunction"/> invocation into a
702708
/// <see cref="ToolResultObject"/>. Handles <see cref="ToolResultAIContent"/>,
@@ -808,6 +814,14 @@ public sealed class ToolInvocation
808814
/// Arguments passed to the tool by the language model.
809815
/// </summary>
810816
public JsonElement? Arguments { get; set; }
817+
/// <summary>
818+
/// Snapshot of the session's currently initialized tools. The SDK populates
819+
/// this only when the invocation targets the built-in tool-search tool
820+
/// (<c>tool_search_tool</c>), so a tool-search override can rank/filter the
821+
/// live catalog — including MCP tools configured in settings — without
822+
/// issuing its own RPC. <c>null</c> for every other tool invocation.
823+
/// </summary>
824+
public IList<CurrentToolMetadata>? AvailableTools { get; set; }
811825
}
812826

813827
/// <summary>
@@ -2721,6 +2735,30 @@ public sealed class LargeToolOutputConfig
27212735
public string? OutputDirectory { get; set; }
27222736
}
27232737

2738+
/// <summary>
2739+
/// Overrides the runtime's built-in tool-search behavior.
2740+
/// Defers tools to keep the model's active tool set small.
2741+
/// To override the tool-search tool's implementation, register a tool
2742+
/// named "tool_search_tool" with <c>OverridesBuiltInTool</c> set to
2743+
/// <see langword="true"/>.
2744+
/// </summary>
2745+
public sealed class ToolSearchConfig
2746+
{
2747+
/// <summary>
2748+
/// Enable or disable tool search.
2749+
/// </summary>
2750+
[JsonPropertyName("enabled")]
2751+
public bool? Enabled { get; set; }
2752+
2753+
/// <summary>
2754+
/// The tool count above which MCP and external tools are deferred behind
2755+
/// tool search. When <see langword="null"/>, the runtime default (30)
2756+
/// applies.
2757+
/// </summary>
2758+
[JsonPropertyName("deferThreshold")]
2759+
public int? DeferThreshold { get; set; }
2760+
}
2761+
27242762
/// <summary>
27252763
/// Configuration for session memory.
27262764
/// </summary>
@@ -2829,6 +2867,7 @@ protected SessionConfigBase(SessionConfigBase? other)
28292867
Hooks = other.Hooks;
28302868
InfiniteSessions = other.InfiniteSessions;
28312869
LargeOutput = other.LargeOutput;
2870+
ToolSearch = other.ToolSearch;
28322871
Memory = other.Memory;
28332872
McpServers = other.McpServers is not null
28342873
? (other.McpServers is Dictionary<string, McpServerConfig> dict
@@ -3235,6 +3274,13 @@ protected SessionConfigBase(SessionConfigBase? other)
32353274
/// </summary>
32363275
public LargeToolOutputConfig? LargeOutput { get; set; }
32373276

3277+
/// <summary>
3278+
/// Overrides the runtime's built-in tool-search behavior.
3279+
/// Tool search defers tools to keep the model's active tool set small. When <see langword="null"/>,
3280+
/// the runtime default applies.
3281+
/// </summary>
3282+
public ToolSearchConfig? ToolSearch { get; set; }
3283+
32383284
/// <summary>
32393285
/// Configuration for session memory. When set, controls whether the
32403286
/// session can read and write persistent memory.

dotnet/test/Unit/SerializationTests.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,48 @@ public void HooksInvokeResponse_SerializesNullOutput_AsEmptyOrNoOutputProperty()
856856
// else: property omitted, which is fine (runtime treats undefined output as no-op)
857857
}
858858

859+
[Fact]
860+
public void ToolResultObject_SerializesToolReferences_WithSdkOptions()
861+
{
862+
var options = GetSerializerOptions();
863+
var original = new ToolResultObject
864+
{
865+
TextResultForLlm = "found 2 tools",
866+
ResultType = "success",
867+
ToolReferences = ["get_weather", "check_status"],
868+
};
869+
870+
var json = JsonSerializer.Serialize(original, options);
871+
using var document = JsonDocument.Parse(json);
872+
var root = document.RootElement;
873+
Assert.Equal("found 2 tools", root.GetProperty("textResultForLlm").GetString());
874+
var refs = root.GetProperty("toolReferences");
875+
Assert.Equal(JsonValueKind.Array, refs.ValueKind);
876+
Assert.Equal(2, refs.GetArrayLength());
877+
Assert.Equal("get_weather", refs[0].GetString());
878+
Assert.Equal("check_status", refs[1].GetString());
879+
880+
var deserialized = JsonSerializer.Deserialize<ToolResultObject>(json, options);
881+
Assert.NotNull(deserialized);
882+
string[] expectedReferences = ["get_weather", "check_status"];
883+
Assert.Equal(expectedReferences, deserialized!.ToolReferences);
884+
}
885+
886+
[Fact]
887+
public void ToolResultObject_OmitsToolReferences_WhenNull_WithSdkOptions()
888+
{
889+
var options = GetSerializerOptions();
890+
var original = new ToolResultObject
891+
{
892+
TextResultForLlm = "ok",
893+
ResultType = "success",
894+
};
895+
896+
var json = JsonSerializer.Serialize(original, options);
897+
using var document = JsonDocument.Parse(json);
898+
Assert.False(document.RootElement.TryGetProperty("toolReferences", out _));
899+
}
900+
859901
private static JsonSerializerOptions GetSerializerOptions()
860902
{
861903
var prop = typeof(CopilotClient)

go/client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,7 @@ func (c *Client) CreateSession(ctx context.Context, config *SessionConfig) (*Ses
722722
req.DisabledSkills = config.DisabledSkills
723723
req.InfiniteSessions = config.InfiniteSessions
724724
req.LargeOutput = config.LargeOutput
725+
req.ToolSearch = config.ToolSearch
725726
req.Memory = config.Memory
726727
req.GitHubToken = config.GitHubToken
727728
req.RemoteSession = config.RemoteSession
@@ -1088,6 +1089,7 @@ func (c *Client) ResumeSessionWithOptions(ctx context.Context, sessionID string,
10881089
req.DisabledSkills = config.DisabledSkills
10891090
req.InfiniteSessions = config.InfiniteSessions
10901091
req.LargeOutput = config.LargeOutput
1092+
req.ToolSearch = config.ToolSearch
10911093
req.Memory = config.Memory
10921094
req.GitHubToken = config.GitHubToken
10931095
req.RemoteSession = config.RemoteSession

go/session.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ import (
1313
"github.com/github/copilot-sdk/go/rpc"
1414
)
1515

16+
// toolSearchToolName is the fixed name of the runtime's built-in tool-search
17+
// tool. A client can replace its behavior by registering a [Tool] with this
18+
// exact name and OverridesBuiltInTool set to true.
19+
const toolSearchToolName = "tool_search_tool"
20+
1621
type sessionHandler struct {
1722
id uint64
1823
fn SessionEventHandler
@@ -1513,6 +1518,17 @@ func (s *Session) executeToolAndRespond(requestID, toolName, toolCallID string,
15131518
TraceContext: ctx,
15141519
}
15151520

1521+
// The built-in tool-search tool receives a snapshot of the session's
1522+
// currently initialized tools so an override can filter the live catalog
1523+
// without issuing its own RPC. Fetch it only for that tool to avoid a
1524+
// round-trip on every tool call; a failed fetch leaves the snapshot nil
1525+
// rather than failing the tool.
1526+
if toolName == toolSearchToolName {
1527+
if metadata, mErr := s.RPC.Tools.GetCurrentMetadata(ctx); mErr == nil && metadata != nil {
1528+
invocation.AvailableTools = metadata.Tools
1529+
}
1530+
}
1531+
15161532
result, err := handler(invocation)
15171533
if err != nil {
15181534
errMsg := err.Error()
@@ -1542,6 +1558,7 @@ func (s *Session) executeToolAndRespond(requestID, toolName, toolCallID string,
15421558
TextResultForLlm: textResultForLLM,
15431559
ToolTelemetry: result.ToolTelemetry,
15441560
ResultType: &effectiveResultType,
1561+
ToolReferences: result.ToolReferences,
15451562
}
15461563
if result.Error != "" {
15471564
rpcResult.Error = &result.Error

go/types.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -948,6 +948,18 @@ type LargeToolOutputConfig struct {
948948
OutputDirectory string `json:"outputDir,omitempty"`
949949
}
950950

951+
// ToolSearchConfig allows to configure tool search behavior.
952+
// Tool search defers tools to keep the model's active tool set small.
953+
// To override the tool-search tool's implementation, register a
954+
// [Tool] named "tool_search_tool" with OverridesBuiltInTool set to true.
955+
type ToolSearchConfig struct {
956+
// Controls whether tool search is enabled.
957+
Enabled *bool `json:"enabled,omitempty"`
958+
// DeferThreshold is the tool count above which MCP and external tools are
959+
// deferred behind tool search. When nil, the runtime default (30) applies.
960+
DeferThreshold *int `json:"deferThreshold,omitempty"`
961+
}
962+
951963
// SessionFSCapabilities declares optional provider capabilities.
952964
type SessionFSCapabilities struct {
953965
// Sqlite indicates whether the provider supports SQLite query/exists operations.
@@ -1153,6 +1165,10 @@ type SessionConfig struct {
11531165
// output exceeding the configured size, the output is written to a temp file
11541166
// and a reference is returned to the model instead of the full payload.
11551167
LargeOutput *LargeToolOutputConfig
1168+
// ToolSearch overrides the runtime's built-in tool-search behavior, which
1169+
// defers rarely used tools behind a searchable index. When nil, the runtime
1170+
// default applies.
1171+
ToolSearch *ToolSearchConfig
11561172
// Memory configures the memory feature for the session. When omitted, the
11571173
// runtime default applies.
11581174
Memory *MemoryConfiguration
@@ -1290,6 +1306,14 @@ type ToolInvocation struct {
12901306
ToolName string
12911307
Arguments any
12921308

1309+
// AvailableTools is a snapshot of the session's currently initialized
1310+
// tools. The SDK populates it only when this invocation targets the
1311+
// built-in tool-search tool ("tool_search_tool"), so a tool-search
1312+
// override can rank/filter the live catalog -- including MCP tools
1313+
// configured in settings -- without issuing its own RPC. It is nil for
1314+
// every other tool invocation.
1315+
AvailableTools []rpc.CurrentToolMetadata
1316+
12931317
// TraceContext carries the W3C Trace Context propagated from the CLI's
12941318
// execute_tool span. Pass this to OpenTelemetry-aware code so that
12951319
// child spans created inside the handler are parented to the CLI span.
@@ -1309,6 +1333,8 @@ type ToolResult struct {
13091333
Error string `json:"error,omitempty"`
13101334
SessionLog string `json:"sessionLog,omitempty"`
13111335
ToolTelemetry map[string]any `json:"toolTelemetry,omitempty"`
1336+
// ToolReferences lists names of tools returned by a tool-search tool.
1337+
ToolReferences []string `json:"toolReferences,omitempty"`
13121338
}
13131339

13141340
// CommandContext provides context about a slash-command invocation.
@@ -1605,6 +1631,10 @@ type ResumeSessionConfig struct {
16051631
// output exceeding the configured size, the output is written to a temp file
16061632
// and a reference is returned to the model instead of the full payload.
16071633
LargeOutput *LargeToolOutputConfig
1634+
// ToolSearch overrides the runtime's built-in tool-search behavior, which
1635+
// defers rarely used tools behind a searchable index. When nil, the runtime
1636+
// default applies.
1637+
ToolSearch *ToolSearchConfig
16081638
// Memory configures the memory feature for the session. When omitted, the
16091639
// runtime default applies.
16101640
Memory *MemoryConfiguration
@@ -2125,6 +2155,7 @@ type createSessionRequest struct {
21252155
DisabledSkills []string `json:"disabledSkills,omitempty"`
21262156
InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"`
21272157
LargeOutput *LargeToolOutputConfig `json:"largeOutput,omitempty"`
2158+
ToolSearch *ToolSearchConfig `json:"toolSearch,omitempty"`
21282159
Memory *MemoryConfiguration `json:"memory,omitempty"`
21292160
Commands []wireCommand `json:"commands,omitempty"`
21302161
RequestElicitation *bool `json:"requestElicitation,omitempty"`
@@ -2216,6 +2247,7 @@ type resumeSessionRequest struct {
22162247
DisabledSkills []string `json:"disabledSkills,omitempty"`
22172248
InfiniteSessions *InfiniteSessionConfig `json:"infiniteSessions,omitempty"`
22182249
LargeOutput *LargeToolOutputConfig `json:"largeOutput,omitempty"`
2250+
ToolSearch *ToolSearchConfig `json:"toolSearch,omitempty"`
22192251
Memory *MemoryConfiguration `json:"memory,omitempty"`
22202252
Commands []wireCommand `json:"commands,omitempty"`
22212253
RequestElicitation *bool `json:"requestElicitation,omitempty"`

0 commit comments

Comments
 (0)