Skip to content

Commit 0d575bc

Browse files
belaltaher8Copilot
andcommitted
Add opaque metadata passthrough to SDK tool definitions
Add an optional, opaque `metadata` bag to tool definitions across all SDK languages and forward it verbatim over the session.create/resume RPC. This lets hosts attach namespaced metadata to tools without expanding the typed public contract; the runtime may recognize specific keys to inform host-specific behavior. Unknown keys are round-tripped untouched. Languages: nodejs (Tool.metadata + defineTool), python (Tool.metadata + define_tool), go (Tool.Metadata), rust (Tool.metadata + with_metadata), java (ToolDefinition.metadata + createWithMetadata), dotnet (CopilotToolOptions.Metadata + wire ToolDefinition.Metadata). Tests added per language for wire/serialization forwarding and omission when unset. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 5eead9c commit 0d575bc

26 files changed

Lines changed: 394 additions & 47 deletions

dotnet/src/Client.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2705,17 +2705,20 @@ internal record ToolDefinition(
27052705
JsonElement Parameters, /* JSON schema */
27062706
bool? OverridesBuiltInTool = null,
27072707
bool? SkipPermission = null,
2708-
CopilotToolDefer? Defer = null)
2708+
CopilotToolDefer? Defer = null,
2709+
[property: JsonPropertyName("metadata")] IDictionary<string, object>? Metadata = null)
27092710
{
27102711
public static ToolDefinition FromAIFunction(AIFunctionDeclaration function)
27112712
{
27122713
var overrides = function.AdditionalProperties.TryGetValue(CopilotTool.OverridesBuiltInToolKey, out var val) && val is true;
27132714
var skipPerm = function.AdditionalProperties.TryGetValue(CopilotTool.SkipPermissionKey, out var skipVal) && skipVal is true;
27142715
var defer = function.AdditionalProperties.TryGetValue(CopilotTool.DeferKey, out var deferVal) && deferVal is CopilotToolDefer d ? d : (CopilotToolDefer?)null;
2716+
var metadata = function.AdditionalProperties.TryGetValue(CopilotTool.MetadataKey, out var metaVal) && metaVal is IDictionary<string, object> m ? m : null;
27152717
return new ToolDefinition(function.Name, function.Description, function.JsonSchema,
27162718
overrides ? true : null,
27172719
skipPerm ? true : null,
2718-
defer);
2720+
defer,
2721+
metadata);
27192722
}
27202723
}
27212724

dotnet/src/CopilotTool.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ public static class CopilotTool
2020
/// <summary>The key used in <see cref="AITool.AdditionalProperties"/> to carry the tool's <see cref="CopilotToolDefer"/> deferral mode.</summary>
2121
internal const string DeferKey = "defer";
2222

23+
/// <summary>The key used in <see cref="AITool.AdditionalProperties"/> to carry the tool's opaque host-defined metadata.</summary>
24+
internal const string MetadataKey = "metadata";
25+
2326
/// <summary>
2427
/// Defines a tool for use in a <see cref="CopilotSession"/>.
2528
/// </summary>
@@ -87,7 +90,7 @@ static void ApplyToolInvocationBinding(AIFunctionFactoryOptions factoryOptions)
8790

8891
static void ApplyToolOptions(AIFunctionFactoryOptions factoryOptions, CopilotToolOptions? toolOptions)
8992
{
90-
if (toolOptions is not null && (toolOptions.OverridesBuiltInTool || toolOptions.SkipPermission || toolOptions.Defer is not null))
93+
if (toolOptions is not null && (toolOptions.OverridesBuiltInTool || toolOptions.SkipPermission || toolOptions.Defer is not null || toolOptions.Metadata is not null))
9194
{
9295
Dictionary<string, object?> additionalProperties = new(StringComparer.Ordinal);
9396
if (factoryOptions.AdditionalProperties is not null)
@@ -113,6 +116,11 @@ static void ApplyToolOptions(AIFunctionFactoryOptions factoryOptions, CopilotToo
113116
additionalProperties[DeferKey] = defer;
114117
}
115118

119+
if (toolOptions.Metadata is { } metadata)
120+
{
121+
additionalProperties[MetadataKey] = metadata;
122+
}
123+
116124
factoryOptions.AdditionalProperties = additionalProperties;
117125
}
118126
}
@@ -151,6 +159,16 @@ public sealed class CopilotToolOptions
151159
/// SDK forwards it to the CLI as the tool's <c>defer</c> mode. Defaults to "auto".
152160
/// </remarks>
153161
public CopilotToolDefer? Defer { get; set; }
162+
163+
/// <summary>
164+
/// Gets or sets opaque, host-defined metadata associated with the tool definition.
165+
/// </summary>
166+
/// <remarks>
167+
/// Keys are namespaced and not part of the stable public API. When set, the SDK forwards
168+
/// the metadata verbatim to the CLI as the tool's <c>metadata</c> object, which the runtime
169+
/// may recognize to inform host-specific behavior. Unknown keys are preserved.
170+
/// </remarks>
171+
public IDictionary<string, object>? Metadata { get; set; }
154172
}
155173

156174
/// <summary>

dotnet/test/Unit/CopilotToolTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,34 @@ public void DefineTool_Omits_Copilot_Metadata_When_Flags_Are_False()
4343
Assert.False(function.AdditionalProperties.ContainsKey("defer"));
4444
}
4545

46+
[Fact]
47+
public void DefineTool_Sets_Metadata_In_Additional_Properties()
48+
{
49+
var metadata = new Dictionary<string, object>
50+
{
51+
["github.com/copilot:safeForTelemetry"] = new Dictionary<string, object>
52+
{
53+
["name"] = true,
54+
["inputsNames"] = false
55+
}
56+
};
57+
58+
var function = CopilotTool.DefineTool(
59+
ReturnsOk,
60+
new CopilotToolOptions { Metadata = metadata });
61+
62+
Assert.True(function.AdditionalProperties.TryGetValue("metadata", out var value));
63+
Assert.Same(metadata, value);
64+
}
65+
66+
[Fact]
67+
public void DefineTool_Omits_Metadata_When_Unset()
68+
{
69+
var function = CopilotTool.DefineTool(ReturnsOk);
70+
71+
Assert.False(function.AdditionalProperties.ContainsKey("metadata"));
72+
}
73+
4674
[Fact]
4775
public void DefineTool_Accepts_Lambda_Handlers_Without_Casts()
4876
{

go/client_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,6 +1160,53 @@ func TestToolDefer(t *testing.T) {
11601160
})
11611161
}
11621162

1163+
func TestToolMetadata(t *testing.T) {
1164+
t.Run("Metadata is serialized in tool definition", func(t *testing.T) {
1165+
tool := Tool{
1166+
Name: "my_tool",
1167+
Description: "A custom tool",
1168+
Metadata: map[string]any{
1169+
"github.com/copilot:safeForTelemetry": map[string]any{"name": true, "inputsNames": false},
1170+
},
1171+
Handler: func(_ ToolInvocation) (ToolResult, error) { return ToolResult{}, nil },
1172+
}
1173+
data, err := json.Marshal(tool)
1174+
if err != nil {
1175+
t.Fatalf("failed to marshal: %v", err)
1176+
}
1177+
var m map[string]any
1178+
if err := json.Unmarshal(data, &m); err != nil {
1179+
t.Fatalf("failed to unmarshal: %v", err)
1180+
}
1181+
meta, ok := m["metadata"].(map[string]any)
1182+
if !ok {
1183+
t.Fatalf("expected metadata object, got %v", m)
1184+
}
1185+
if _, ok := meta["github.com/copilot:safeForTelemetry"]; !ok {
1186+
t.Errorf("expected namespaced key preserved, got %v", meta)
1187+
}
1188+
})
1189+
1190+
t.Run("Metadata omitted when unset", func(t *testing.T) {
1191+
tool := Tool{
1192+
Name: "custom_tool",
1193+
Description: "A custom tool",
1194+
Handler: func(_ ToolInvocation) (ToolResult, error) { return ToolResult{}, nil },
1195+
}
1196+
data, err := json.Marshal(tool)
1197+
if err != nil {
1198+
t.Fatalf("failed to marshal: %v", err)
1199+
}
1200+
var m map[string]any
1201+
if err := json.Unmarshal(data, &m); err != nil {
1202+
t.Fatalf("failed to unmarshal: %v", err)
1203+
}
1204+
if _, ok := m["metadata"]; ok {
1205+
t.Errorf("expected metadata to be omitted, got %v", m)
1206+
}
1207+
})
1208+
}
1209+
11631210
func TestClient_CreateSession_AllowsMissingPermissionHandler(t *testing.T) {
11641211
t.Run("accepts nil config before connection validation", func(t *testing.T) {
11651212
client := NewClient(&ClientOptions{Connection: StdioConnection{Path: "/__nonexistent_copilot_binary__"}})

go/types.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1269,6 +1269,12 @@ type Tool struct {
12691269
// Defer controls whether the tool may be deferred (loaded lazily via tool
12701270
// search) rather than always pre-loaded. When empty, the runtime decides.
12711271
Defer ToolDefer `json:"defer,omitempty"`
1272+
// Metadata is opaque, host-defined metadata associated with the tool
1273+
// definition. Keys are namespaced and not part of the stable public API;
1274+
// the SDK forwards them verbatim to the runtime, which may recognize
1275+
// specific keys to inform host-specific behavior. Unknown keys are
1276+
// preserved and round-tripped untouched.
1277+
Metadata map[string]any `json:"metadata,omitempty"`
12721278
// Handler is optional. When nil, the SDK exposes the tool declaration but does
12731279
// not automatically invoke it.
12741280
Handler ToolHandler `json:"-"`

java/src/main/java/com/github/copilot/rpc/ToolDefinition.java

Lines changed: 64 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@
7575
* controls whether the tool may be deferred (loaded lazily via tool
7676
* search) rather than always pre-loaded; {@code null} lets the
7777
* runtime decide
78+
* @param metadata
79+
* opaque, host-defined metadata forwarded verbatim to the runtime;
80+
* keys are namespaced and not part of the stable public API;
81+
* {@code null} when unset
7882
* @see SessionConfig#setTools(java.util.List)
7983
* @see ToolHandler
8084
* @since 1.0.0
@@ -83,7 +87,8 @@
8387
public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("description") String description,
8488
@JsonProperty("parameters") Object parameters, @JsonIgnore ToolHandler handler,
8589
@JsonProperty("overridesBuiltInTool") Boolean overridesBuiltInTool,
86-
@JsonProperty("skipPermission") Boolean skipPermission, @JsonProperty("defer") ToolDefer defer) {
90+
@JsonProperty("skipPermission") Boolean skipPermission, @JsonProperty("defer") ToolDefer defer,
91+
@JsonProperty("metadata") Map<String, Object> metadata) {
8792

8893
/**
8994
* Creates a tool definition with a JSON schema for parameters.
@@ -103,7 +108,7 @@ public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("d
103108
*/
104109
public static ToolDefinition create(String name, String description, Map<String, Object> schema,
105110
ToolHandler handler) {
106-
return new ToolDefinition(name, description, schema, handler, null, null, null);
111+
return new ToolDefinition(name, description, schema, handler, null, null, null, null);
107112
}
108113

109114
/**
@@ -127,7 +132,7 @@ public static ToolDefinition create(String name, String description, Map<String,
127132
*/
128133
public static ToolDefinition createOverride(String name, String description, Map<String, Object> schema,
129134
ToolHandler handler) {
130-
return new ToolDefinition(name, description, schema, handler, true, null, null);
135+
return new ToolDefinition(name, description, schema, handler, true, null, null, null);
131136
}
132137

133138
/**
@@ -150,7 +155,7 @@ public static ToolDefinition createOverride(String name, String description, Map
150155
*/
151156
public static ToolDefinition createSkipPermission(String name, String description, Map<String, Object> schema,
152157
ToolHandler handler) {
153-
return new ToolDefinition(name, description, schema, handler, null, true, null);
158+
return new ToolDefinition(name, description, schema, handler, null, true, null, null);
154159
}
155160

156161
/**
@@ -176,7 +181,32 @@ public static ToolDefinition createSkipPermission(String name, String descriptio
176181
*/
177182
public static ToolDefinition createWithDefer(String name, String description, Map<String, Object> schema,
178183
ToolHandler handler, ToolDefer defer) {
179-
return new ToolDefinition(name, description, schema, handler, null, null, defer);
184+
return new ToolDefinition(name, description, schema, handler, null, null, defer, null);
185+
}
186+
187+
/**
188+
* Creates a tool definition with opaque, host-defined metadata.
189+
* <p>
190+
* Use this factory method to attach namespaced metadata that the SDK forwards
191+
* verbatim to the runtime. The keys are not part of the stable public API; the
192+
* runtime may recognize specific keys to inform host-specific behavior.
193+
*
194+
* @param name
195+
* the unique name of the tool
196+
* @param description
197+
* a description of what the tool does
198+
* @param schema
199+
* the JSON Schema as a {@code Map}
200+
* @param handler
201+
* the handler function to execute when invoked
202+
* @param metadata
203+
* the opaque metadata map forwarded to the runtime
204+
* @return a new tool definition with the metadata set
205+
* @since 1.0.7
206+
*/
207+
public static ToolDefinition createWithMetadata(String name, String description, Map<String, Object> schema,
208+
ToolHandler handler, Map<String, Object> metadata) {
209+
return new ToolDefinition(name, description, schema, handler, null, null, null, metadata);
180210
}
181211

182212
/**
@@ -247,7 +277,7 @@ public static List<ToolDefinition> fromClass(Class<?> clazz) {
247277
*/
248278
@CopilotExperimental
249279
public ToolDefinition overridesBuiltInTool(boolean value) {
250-
return new ToolDefinition(name, description, parameters, handler, value, skipPermission, defer);
280+
return new ToolDefinition(name, description, parameters, handler, value, skipPermission, defer, metadata);
251281
}
252282

253283
/**
@@ -261,7 +291,7 @@ public ToolDefinition overridesBuiltInTool(boolean value) {
261291
*/
262292
@CopilotExperimental
263293
public ToolDefinition skipPermission(boolean value) {
264-
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, value, defer);
294+
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, value, defer, metadata);
265295
}
266296

267297
/**
@@ -275,7 +305,23 @@ public ToolDefinition skipPermission(boolean value) {
275305
*/
276306
@CopilotExperimental
277307
public ToolDefinition defer(ToolDefer value) {
278-
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, skipPermission, value);
308+
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, skipPermission, value,
309+
metadata);
310+
}
311+
312+
/**
313+
* Returns a copy with the opaque {@code metadata} bag set.
314+
*
315+
* @param value
316+
* the opaque, host-defined metadata forwarded verbatim to the
317+
* runtime; keys are namespaced and not part of the stable public API
318+
* @return a new {@code ToolDefinition} with the metadata applied
319+
* @since 1.0.7
320+
*/
321+
@CopilotExperimental
322+
public ToolDefinition metadata(Map<String, Object> value) {
323+
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, skipPermission, defer,
324+
value);
279325
}
280326

281327
// ------------------------------------------------------------------
@@ -319,7 +365,7 @@ public static <R> ToolDefinition from(String name, String description, Supplier<
319365
R result = handler.get();
320366
return CompletableFuture.completedFuture(formatResult(result, mapper));
321367
};
322-
return new ToolDefinition(name, description, schema, toolHandler, null, null, null);
368+
return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null);
323369
}
324370

325371
/**
@@ -361,7 +407,7 @@ public static <T1, R> ToolDefinition from(String name, String description, Param
361407
R result = handler.apply(arg1);
362408
return CompletableFuture.completedFuture(formatResult(result, mapper));
363409
};
364-
return new ToolDefinition(name, description, schema, toolHandler, null, null, null);
410+
return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null);
365411
}
366412

367413
/**
@@ -409,7 +455,7 @@ public static <T1, T2, R> ToolDefinition from(String name, String description, P
409455
R result = handler.apply(arg1, arg2);
410456
return CompletableFuture.completedFuture(formatResult(result, mapper));
411457
};
412-
return new ToolDefinition(name, description, schema, toolHandler, null, null, null);
458+
return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null);
413459
}
414460

415461
// ------------------------------------------------------------------
@@ -458,7 +504,7 @@ public static <R> ToolDefinition fromAsync(String name, String description,
458504
}
459505
return future.thenApply(result -> formatResult(result, mapper));
460506
};
461-
return new ToolDefinition(name, description, schema, toolHandler, null, null, null);
507+
return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null);
462508
}
463509

464510
/**
@@ -506,7 +552,7 @@ public static <T1, R> ToolDefinition fromAsync(String name, String description,
506552
}
507553
return future.thenApply(result -> formatResult(result, mapper));
508554
};
509-
return new ToolDefinition(name, description, schema, toolHandler, null, null, null);
555+
return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null);
510556
}
511557

512558
/**
@@ -551,7 +597,7 @@ public static <T1, T2, R> ToolDefinition fromAsync(String name, String descripti
551597
}
552598
return future.thenApply(result -> formatResult(result, mapper));
553599
};
554-
return new ToolDefinition(name, description, schema, toolHandler, null, null, null);
600+
return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null);
555601
}
556602

557603
// ------------------------------------------------------------------
@@ -594,7 +640,7 @@ public static <R> ToolDefinition fromWithToolInvocation(String name, String desc
594640
R result = handler.apply(invocation);
595641
return CompletableFuture.completedFuture(formatResult(result, mapper));
596642
};
597-
return new ToolDefinition(name, description, schema, toolHandler, null, null, null);
643+
return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null);
598644
}
599645

600646
/**
@@ -640,7 +686,7 @@ public static <T1, R> ToolDefinition fromWithToolInvocation(String name, String
640686
R result = handler.apply(arg1, invocation);
641687
return CompletableFuture.completedFuture(formatResult(result, mapper));
642688
};
643-
return new ToolDefinition(name, description, schema, toolHandler, null, null, null);
689+
return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null);
644690
}
645691

646692
// ------------------------------------------------------------------
@@ -689,7 +735,7 @@ public static <R> ToolDefinition fromAsyncWithToolInvocation(String name, String
689735
}
690736
return future.thenApply(result -> formatResult(result, mapper));
691737
};
692-
return new ToolDefinition(name, description, schema, toolHandler, null, null, null);
738+
return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null);
693739
}
694740

695741
/**
@@ -741,7 +787,7 @@ public static <T1, R> ToolDefinition fromAsyncWithToolInvocation(String name, St
741787
}
742788
return future.thenApply(result -> formatResult(result, mapper));
743789
};
744-
return new ToolDefinition(name, description, schema, toolHandler, null, null, null);
790+
return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null);
745791
}
746792

747793
// ------------------------------------------------------------------

java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,8 @@ private void writeToolDefinition(PrintWriter out, ExecutableElement method) {
276276
out.println(" },");
277277
out.println(" " + overridesArg + ",");
278278
out.println(" " + skipPermArg + ",");
279-
out.println(" " + deferArg);
279+
out.println(" " + deferArg + ",");
280+
out.println(" null");
280281
out.print(" )");
281282
}
282283

0 commit comments

Comments
 (0)