Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
7 changes: 5 additions & 2 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2485,17 +2485,20 @@ internal record ToolDefinition(
JsonElement Parameters, /* JSON schema */
bool? OverridesBuiltInTool = null,
bool? SkipPermission = null,
CopilotToolDefer? Defer = null)
CopilotToolDefer? Defer = null,
[property: JsonPropertyName("metadata")] IDictionary<string, object>? Metadata = null)
{
public static ToolDefinition FromAIFunction(AIFunctionDeclaration function)
{
var overrides = function.AdditionalProperties.TryGetValue(CopilotTool.OverridesBuiltInToolKey, out var val) && val is true;
var skipPerm = function.AdditionalProperties.TryGetValue(CopilotTool.SkipPermissionKey, out var skipVal) && skipVal is true;
var defer = function.AdditionalProperties.TryGetValue(CopilotTool.DeferKey, out var deferVal) && deferVal is CopilotToolDefer d ? d : (CopilotToolDefer?)null;
var metadata = function.AdditionalProperties.TryGetValue(CopilotTool.MetadataKey, out var metaVal) && metaVal is IDictionary<string, object> m ? m : null;
return new ToolDefinition(function.Name, function.Description, function.JsonSchema,
overrides ? true : null,
skipPerm ? true : null,
defer);
defer,
metadata);
}
}

Expand Down
20 changes: 19 additions & 1 deletion dotnet/src/CopilotTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ public static class CopilotTool
/// <summary>The key used in <see cref="AITool.AdditionalProperties"/> to carry the tool's <see cref="CopilotToolDefer"/> deferral mode.</summary>
internal const string DeferKey = "defer";

/// <summary>The key used in <see cref="AITool.AdditionalProperties"/> to carry the tool's opaque host-defined metadata.</summary>
internal const string MetadataKey = "metadata";

/// <summary>
/// Defines a tool for use in a <see cref="CopilotSession"/>.
/// </summary>
Expand Down Expand Up @@ -87,7 +90,7 @@ static void ApplyToolInvocationBinding(AIFunctionFactoryOptions factoryOptions)

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

if (toolOptions.Metadata is { } metadata)
{
additionalProperties[MetadataKey] = metadata;
}

factoryOptions.AdditionalProperties = additionalProperties;
}
}
Expand Down Expand Up @@ -151,6 +159,16 @@ public sealed class CopilotToolOptions
/// SDK forwards it to the CLI as the tool's <c>defer</c> mode. Defaults to "auto".
/// </remarks>
public CopilotToolDefer? Defer { get; set; }

/// <summary>
/// Gets or sets opaque, host-defined metadata associated with the tool definition.
/// </summary>
/// <remarks>
/// Keys are namespaced and not part of the stable public API. When set, the SDK forwards
/// the metadata verbatim to the CLI as the tool's <c>metadata</c> object, which the runtime
/// may recognize to inform host-specific behavior. Unknown keys are preserved.
/// </remarks>
public IDictionary<string, object>? Metadata { get; set; }
}

/// <summary>
Expand Down
28 changes: 28 additions & 0 deletions dotnet/test/Unit/CopilotToolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,34 @@ public void DefineTool_Omits_Copilot_Metadata_When_Flags_Are_False()
Assert.False(function.AdditionalProperties.ContainsKey("defer"));
}

[Fact]
public void DefineTool_Sets_Metadata_In_Additional_Properties()
{
var metadata = new Dictionary<string, object>
{
["github.com/copilot:safeForTelemetry"] = new Dictionary<string, object>
{
["name"] = true,
["inputsNames"] = false
}
};

var function = CopilotTool.DefineTool(
ReturnsOk,
new CopilotToolOptions { Metadata = metadata });

Assert.True(function.AdditionalProperties.TryGetValue("metadata", out var value));
Assert.Same(metadata, value);
}

[Fact]
public void DefineTool_Omits_Metadata_When_Unset()
{
var function = CopilotTool.DefineTool(ReturnsOk);

Assert.False(function.AdditionalProperties.ContainsKey("metadata"));
}

[Fact]
public void DefineTool_Accepts_Lambda_Handlers_Without_Casts()
{
Expand Down
47 changes: 47 additions & 0 deletions go/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,53 @@ func TestToolDefer(t *testing.T) {
})
}

func TestToolMetadata(t *testing.T) {
t.Run("Metadata is serialized in tool definition", func(t *testing.T) {
tool := Tool{
Name: "my_tool",
Description: "A custom tool",
Metadata: map[string]any{
"github.com/copilot:safeForTelemetry": map[string]any{"name": true, "inputsNames": false},
},
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)
}
meta, ok := m["metadata"].(map[string]any)
if !ok {
t.Fatalf("expected metadata object, got %v", m)
}
if _, ok := meta["github.com/copilot:safeForTelemetry"]; !ok {
t.Errorf("expected namespaced key preserved, got %v", meta)
}
})

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

func TestClient_CreateSession_AllowsMissingPermissionHandler(t *testing.T) {
t.Run("accepts nil config before connection validation", func(t *testing.T) {
client := NewClient(&ClientOptions{Connection: StdioConnection{Path: "/__nonexistent_copilot_binary__"}})
Expand Down
6 changes: 6 additions & 0 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,12 @@ type Tool struct {
// Defer controls whether the tool may be deferred (loaded lazily via tool
// search) rather than always pre-loaded. When empty, the runtime decides.
Defer ToolDefer `json:"defer,omitempty"`
// Metadata is opaque, host-defined metadata associated with the tool
// definition. Keys are namespaced and not part of the stable public API;
// the SDK forwards them verbatim to the runtime, which may recognize
// specific keys to inform host-specific behavior. Unknown keys are
// preserved and round-tripped untouched.
Metadata map[string]any `json:"metadata,omitempty"`
// Handler is optional. When nil, the SDK exposes the tool declaration but does
// not automatically invoke it.
Handler ToolHandler `json:"-"`
Expand Down
41 changes: 36 additions & 5 deletions java/src/main/java/com/github/copilot/rpc/ToolDefinition.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@
* controls whether the tool may be deferred (loaded lazily via tool
* search) rather than always pre-loaded; {@code null} lets the
* runtime decide
* @param metadata
* opaque, host-defined metadata forwarded verbatim to the runtime;
* keys are namespaced and not part of the stable public API;
* {@code null} when unset
* @see SessionConfig#setTools(java.util.List)
* @see ToolHandler
* @since 1.0.0
Expand All @@ -78,7 +82,8 @@
public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("description") String description,
@JsonProperty("parameters") Object parameters, @JsonIgnore ToolHandler handler,
@JsonProperty("overridesBuiltInTool") Boolean overridesBuiltInTool,
@JsonProperty("skipPermission") Boolean skipPermission, @JsonProperty("defer") ToolDefer defer) {
@JsonProperty("skipPermission") Boolean skipPermission, @JsonProperty("defer") ToolDefer defer,
@JsonProperty("metadata") Map<String, Object> metadata) {

/**
* Creates a tool definition with a JSON schema for parameters.
Expand All @@ -98,7 +103,7 @@ public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("d
*/
public static ToolDefinition create(String name, String description, Map<String, Object> schema,
ToolHandler handler) {
return new ToolDefinition(name, description, schema, handler, null, null, null);
return new ToolDefinition(name, description, schema, handler, null, null, null, null);
}

/**
Expand All @@ -122,7 +127,7 @@ public static ToolDefinition create(String name, String description, Map<String,
*/
public static ToolDefinition createOverride(String name, String description, Map<String, Object> schema,
ToolHandler handler) {
return new ToolDefinition(name, description, schema, handler, true, null, null);
return new ToolDefinition(name, description, schema, handler, true, null, null, null);
}

/**
Expand All @@ -145,7 +150,7 @@ public static ToolDefinition createOverride(String name, String description, Map
*/
public static ToolDefinition createSkipPermission(String name, String description, Map<String, Object> schema,
ToolHandler handler) {
return new ToolDefinition(name, description, schema, handler, null, true, null);
return new ToolDefinition(name, description, schema, handler, null, true, null, null);
}

/**
Expand All @@ -171,7 +176,33 @@ public static ToolDefinition createSkipPermission(String name, String descriptio
*/
public static ToolDefinition createWithDefer(String name, String description, Map<String, Object> schema,
ToolHandler handler, ToolDefer defer) {
return new ToolDefinition(name, description, schema, handler, null, null, defer);
return new ToolDefinition(name, description, schema, handler, null, null, defer, null);
}

/**
* Creates a tool definition with opaque, host-defined metadata.
* <p>
* Use this factory method to attach namespaced metadata that the SDK
* forwards verbatim to the runtime. The keys are not part of the stable
* public API; the runtime may recognize specific keys to inform
* host-specific behavior.
*
* @param name
* the unique name of the tool
* @param description
* a description of what the tool does
* @param schema
* the JSON Schema as a {@code Map}
* @param handler
* the handler function to execute when invoked
* @param metadata
* the opaque metadata map forwarded to the runtime
* @return a new tool definition with the metadata set
* @since 1.0.0
*/
public static ToolDefinition createWithMetadata(String name, String description, Map<String, Object> schema,
ToolHandler handler, Map<String, Object> metadata) {
return new ToolDefinition(name, description, schema, handler, null, null, null, metadata);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,8 @@ private void writeToolDefinition(PrintWriter out, ExecutableElement method) {
out.println(" },");
out.println(" " + overridesArg + ",");
out.println(" " + skipPermArg + ",");
out.println(" " + deferArg);
out.println(" " + deferArg + ",");
out.println(" null");
out.print(" )");
}

Expand Down
23 changes: 23 additions & 0 deletions java/src/test/java/com/github/copilot/ToolDefinitionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,27 @@ void testDeferNeverIsSerialized() throws Exception {

assertEquals("never", json.get("defer").asText());
}

@Test
void testMetadataIsSerialized() throws Exception {
Map<String, Object> metadata = Map.of("github.com/copilot:safeForTelemetry",
Map.of("name", true, "inputsNames", false));
ToolDefinition tool = ToolDefinition.createWithMetadata("my_tool", "A tool", schema(),
invocation -> CompletableFuture.completedFuture("ok"), metadata);

ObjectNode json = (ObjectNode) MAPPER.readTree(MAPPER.writeValueAsString(tool));

assertTrue(json.has("metadata"));
assertTrue(json.get("metadata").has("github.com/copilot:safeForTelemetry"));
}

@Test
void testMetadataOmittedWhenNull() throws Exception {
ToolDefinition tool = ToolDefinition.create("my_tool", "A tool", schema(),
invocation -> CompletableFuture.completedFuture("ok"));

ObjectNode json = (ObjectNode) MAPPER.readTree(MAPPER.writeValueAsString(tool));

assertFalse(json.has("metadata"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public List<ToolDefinition> definitions(ErgonomicTestTools instance, ObjectMappe
Map<String, Object> args = invocation.getArguments();
String phase = (String) args.get("phase");
return CompletableFuture.completedFuture(instance.setCurrentPhase(phase));
}, null, null, null),
}, null, null, null, null),
new ToolDefinition(
"search_items", "Search for items by keyword", Map
.of("type", "object", "properties",
Expand All @@ -44,6 +44,6 @@ public List<ToolDefinition> definitions(ErgonomicTestTools instance, ObjectMappe
Map<String, Object> args = invocation.getArguments();
String keyword = (String) args.get("keyword");
return CompletableFuture.completedFuture(instance.searchItems(keyword));
}, null, null, null));
}, null, null, null, null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,6 @@ public List<ToolDefinition> definitions(ArgCoercionTools instance, ObjectMapper
boolean flag = (Boolean) args.get("flag");
ArgCoercionTools.Color color = ArgCoercionTools.Color.valueOf((String) args.get("color"));
return CompletableFuture.completedFuture(instance.mixedArgs(text, count, flag, color));
}, null, null, null));
}, null, null, null, null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ public List<ToolDefinition> definitions(DateTimeTools instance, ObjectMapper map
Map<String, Object> args = invocation.getArguments();
LocalDateTime when = mapper.convertValue(args.get("when"), LocalDateTime.class);
return CompletableFuture.completedFuture(instance.scheduleEvent(when));
}, null, null, null));
}, null, null, null, null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,6 @@ public List<ToolDefinition> definitions(DefaultValueTools instance, ObjectMapper
Object countRaw = args.containsKey("count") ? args.get("count") : 42;
int count = ((Number) countRaw).intValue();
return CompletableFuture.completedFuture(instance.withDefault(label, count));
}, null, null, null));
}, null, null, null, null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ public List<ToolDefinition> definitions(MultiReturnTools instance, ObjectMapper
return List.of(new ToolDefinition("string_method", "Returns a string",
Map.of("type", "object", "properties", Map.of(), "required", List.of()), invocation -> {
return CompletableFuture.completedFuture(instance.stringMethod());
}, null, null, null), new ToolDefinition("void_method", "Void method",
}, null, null, null, null), new ToolDefinition("void_method", "Void method",
Map.of("type", "object", "properties", Map.of(), "required", List.of()), invocation -> {
instance.voidMethod();
return CompletableFuture.completedFuture("Success");
}, null, null, null),
}, null, null, null, null),
new ToolDefinition("async_method", "Async method",
Map.of("type", "object", "properties", Map.of(), "required", List.of()), invocation -> {
return instance.asyncMethod().thenApply(r -> (Object) r);
}, null, null, null));
}, null, null, null, null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public List<ToolDefinition> definitions(OptionalParamTools instance, ObjectMappe
Object titleRaw = args.get("title");
Optional<String> title = titleRaw != null ? Optional.of((String) titleRaw) : Optional.empty();
return CompletableFuture.completedFuture(instance.greetWithTitle(name, title));
}, null, null, null),
}, null, null, null, null),
new ToolDefinition("multiply", "Multiply with optional factor",
Map.of("type", "object", "properties",
Map.ofEntries(
Expand All @@ -58,7 +58,7 @@ public List<ToolDefinition> definitions(OptionalParamTools instance, ObjectMappe
? OptionalInt.of(((Number) factorRaw).intValue())
: OptionalInt.empty();
return CompletableFuture.completedFuture(instance.multiply(base, factor));
}, null, null, null),
}, null, null, null, null),
new ToolDefinition("scale", "Scale with optional ratio",
Map.of("type", "object", "properties",
Map.ofEntries(
Expand All @@ -77,7 +77,7 @@ public List<ToolDefinition> definitions(OptionalParamTools instance, ObjectMappe
? OptionalDouble.of(((Number) ratioRaw).doubleValue())
: OptionalDouble.empty();
return CompletableFuture.completedFuture(instance.scale(value, ratio));
}, null, null, null),
}, null, null, null, null),
new ToolDefinition("offset", "Offset with optional delta",
Map.of("type", "object", "properties",
Map.ofEntries(
Expand All @@ -96,6 +96,6 @@ public List<ToolDefinition> definitions(OptionalParamTools instance, ObjectMappe
? OptionalLong.of(((Number) deltaRaw).longValue())
: OptionalLong.empty();
return CompletableFuture.completedFuture(instance.offset(base, delta));
}, null, null, null));
}, null, null, null, null));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,6 @@ public List<ToolDefinition> definitions(OverrideTools instance, ObjectMapper map
Map<String, Object> args = invocation.getArguments();
String pattern = (String) args.get("pattern");
return CompletableFuture.completedFuture(instance.customGrep(pattern));
}, Boolean.TRUE, null, null));
}, Boolean.TRUE, null, null, null));
}
}
Loading