Skip to content
Open
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 @@ -2705,17 +2705,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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious if the [property: JsonPropertyName("metadata")] here makes any difference. I can't see what it would do. Is it possibly redundant?

{
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: please call it "runtime" not "CLI" since we're decoupling from CLI

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also you don't need to distinguish the SDK from the runtime here, so probably don't even need to mention that at all. From the consumer's point of view, this is a library they are using and the way it's split into SDK-vs-runtime is an implementation detail that's not relevant to them here.

Altogether you can just remove the <remarks> section.

/// may recognize to inform host-specific behavior. Unknown keys are preserved.
/// </remarks>
public IDictionary<string, object>? Metadata { get; set; }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can define this type as IDictionary<string, object> because the object won't work with NativeAOT serialization.

A relatively easy fix would be to replace this with:

Suggested change
public IDictionary<string, object>? Metadata { get; set; }
public IDictionary<string, JsonNode?>? 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 @@ -1160,6 +1160,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 @@ -1269,6 +1269,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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment about not mentioning runtime here as if it's a separate thing

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are other places in this PR too but I'll not comment on the others - I'm sure you can find them :)

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
82 changes: 64 additions & 18 deletions java/src/main/java/com/github/copilot/rpc/ToolDefinition.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,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 @@ -83,7 +87,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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like this would be a breaking change.

@edburns What's the recommended way to handle this in Java? Should this be added purely as a setter and not a constructor parameter? Or should it be on a new constructor overload?


/**
* Creates a tool definition with a JSON schema for parameters.
Expand All @@ -103,7 +108,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 @@ -127,7 +132,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 @@ -150,7 +155,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 @@ -176,7 +181,32 @@ 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.7
*/
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 Expand Up @@ -247,7 +277,7 @@ public static List<ToolDefinition> fromClass(Class<?> clazz) {
*/
@CopilotExperimental
public ToolDefinition overridesBuiltInTool(boolean value) {
return new ToolDefinition(name, description, parameters, handler, value, skipPermission, defer);
return new ToolDefinition(name, description, parameters, handler, value, skipPermission, defer, metadata);
}

/**
Expand All @@ -261,7 +291,7 @@ public ToolDefinition overridesBuiltInTool(boolean value) {
*/
@CopilotExperimental
public ToolDefinition skipPermission(boolean value) {
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, value, defer);
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, value, defer, metadata);
}

/**
Expand All @@ -275,7 +305,23 @@ public ToolDefinition skipPermission(boolean value) {
*/
@CopilotExperimental
public ToolDefinition defer(ToolDefer value) {
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, skipPermission, value);
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, skipPermission, value,
metadata);
}

/**
* Returns a copy with the opaque {@code metadata} bag set.
*
* @param value
* the opaque, host-defined metadata forwarded verbatim to the
* runtime; keys are namespaced and not part of the stable public API
* @return a new {@code ToolDefinition} with the metadata applied
* @since 1.0.7
*/
@CopilotExperimental
public ToolDefinition metadata(Map<String, Object> value) {
return new ToolDefinition(name, description, parameters, handler, overridesBuiltInTool, skipPermission, defer,
value);
}

// ------------------------------------------------------------------
Expand Down Expand Up @@ -319,7 +365,7 @@ public static <R> ToolDefinition from(String name, String description, Supplier<
R result = handler.get();
return CompletableFuture.completedFuture(formatResult(result, mapper));
};
return new ToolDefinition(name, description, schema, toolHandler, null, null, null);
return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null);
}

/**
Expand Down Expand Up @@ -361,7 +407,7 @@ public static <T1, R> ToolDefinition from(String name, String description, Param
R result = handler.apply(arg1);
return CompletableFuture.completedFuture(formatResult(result, mapper));
};
return new ToolDefinition(name, description, schema, toolHandler, null, null, null);
return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null);
}

/**
Expand Down Expand Up @@ -409,7 +455,7 @@ public static <T1, T2, R> ToolDefinition from(String name, String description, P
R result = handler.apply(arg1, arg2);
return CompletableFuture.completedFuture(formatResult(result, mapper));
};
return new ToolDefinition(name, description, schema, toolHandler, null, null, null);
return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null);
}

// ------------------------------------------------------------------
Expand Down Expand Up @@ -458,7 +504,7 @@ public static <R> ToolDefinition fromAsync(String name, String description,
}
return future.thenApply(result -> formatResult(result, mapper));
};
return new ToolDefinition(name, description, schema, toolHandler, null, null, null);
return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null);
}

/**
Expand Down Expand Up @@ -506,7 +552,7 @@ public static <T1, R> ToolDefinition fromAsync(String name, String description,
}
return future.thenApply(result -> formatResult(result, mapper));
};
return new ToolDefinition(name, description, schema, toolHandler, null, null, null);
return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null);
}

/**
Expand Down Expand Up @@ -551,7 +597,7 @@ public static <T1, T2, R> ToolDefinition fromAsync(String name, String descripti
}
return future.thenApply(result -> formatResult(result, mapper));
};
return new ToolDefinition(name, description, schema, toolHandler, null, null, null);
return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null);
}

// ------------------------------------------------------------------
Expand Down Expand Up @@ -594,7 +640,7 @@ public static <R> ToolDefinition fromWithToolInvocation(String name, String desc
R result = handler.apply(invocation);
return CompletableFuture.completedFuture(formatResult(result, mapper));
};
return new ToolDefinition(name, description, schema, toolHandler, null, null, null);
return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null);
}

/**
Expand Down Expand Up @@ -640,7 +686,7 @@ public static <T1, R> ToolDefinition fromWithToolInvocation(String name, String
R result = handler.apply(arg1, invocation);
return CompletableFuture.completedFuture(formatResult(result, mapper));
};
return new ToolDefinition(name, description, schema, toolHandler, null, null, null);
return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null);
}

// ------------------------------------------------------------------
Expand Down Expand Up @@ -689,7 +735,7 @@ public static <R> ToolDefinition fromAsyncWithToolInvocation(String name, String
}
return future.thenApply(result -> formatResult(result, mapper));
};
return new ToolDefinition(name, description, schema, toolHandler, null, null, null);
return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null);
}

/**
Expand Down Expand Up @@ -741,7 +787,7 @@ public static <T1, R> ToolDefinition fromAsyncWithToolInvocation(String name, St
}
return future.thenApply(result -> formatResult(result, mapper));
};
return new ToolDefinition(name, description, schema, toolHandler, null, null, null);
return new ToolDefinition(name, description, schema, toolHandler, null, null, null, null);
}

// ------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,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");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also be adding "metadata" to CopilotTool and including it in the output here instead of null? Or is there no way to represent arbitrary dictionaries on a Java annotation?

out.print(" )");
}

Expand Down
Loading
Loading