Add opaque metadata passthrough to SDK tool definitions#1864
Add opaque metadata passthrough to SDK tool definitions#1864belaltaher8 wants to merge 3 commits into
metadata passthrough to SDK tool definitions#1864Conversation
68c607d to
6fdc349
Compare
metadata passthrough to SDK tool definitions
There was a problem hiding this comment.
Pull request overview
Adds an optional, opaque metadata bag to tool definitions across the multi-language SDKs and forwards it verbatim over session.create / session.resume, enabling hosts to attach namespaced, runtime-recognized (but SDK-opaque) attributes without expanding the typed contract.
Changes:
- Add
metadatato tool definition types/builders (Rust/Go/Node/Python/.NET/Java) with “omit when unset” behavior where applicable. - Wire
metadatathrough session create/resume payload construction (Node + Python shown here; others rely on serialization of the definition type). - Add/extend unit tests to validate serialization and omission semantics; update Java annotation-processor fixtures for the new constructor arity.
Show a summary per file
| File | Description |
|---|---|
| rust/src/types.rs | Add Tool.metadata, builder, Debug output, and serialization test. |
| python/copilot/tools.py | Add Tool.metadata + define_tool(..., metadata=...) passthrough. |
| python/copilot/client.py | Include metadata when building tool definitions for create/resume RPCs. |
| python/test_client.py | Add test asserting metadata forwarded on create/resume and omitted when unset. |
| nodejs/src/types.ts | Extend Tool / defineTool types to include optional metadata. |
| nodejs/src/client.ts | Forward tool.metadata into session.create / session.resume payloads. |
| nodejs/test/client.test.ts | Add tests for forwarding and omission of metadata. |
| go/types.go | Add Tool.Metadata with omitempty JSON tag. |
| go/client_test.go | Add serialization tests for tool metadata presence/omission. |
| dotnet/src/CopilotTool.cs | Add MetadataKey + option plumbing into AITool.AdditionalProperties. |
| dotnet/src/Client.cs | Include metadata in internal wire ToolDefinition built from AI function declarations. |
| dotnet/test/Unit/CopilotToolTests.cs | Add unit tests ensuring metadata is set/omitted in additional properties. |
| java/src/main/java/com/github/copilot/rpc/ToolDefinition.java | Add metadata record component + factories + fluent modifier. |
| java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java | Emit null metadata argument in generated ToolDefinition(...) calls. |
| java/src/test/java/com/github/copilot/ToolDefinitionTest.java | Add serialization tests for metadata presence/omission. |
| java/src/test/java/com/github/copilot/rpc/fixtures/StaticTools$$CopilotToolMeta.java | Update fixture constructor call to include null metadata arg. |
| java/src/test/java/com/github/copilot/rpc/fixtures/StaticInvocationTools$$CopilotToolMeta.java | Update fixture constructor call to include null metadata arg. |
| java/src/test/java/com/github/copilot/rpc/fixtures/SimpleTools$$CopilotToolMeta.java | Update fixture constructor calls to include null metadata arg. |
| java/src/test/java/com/github/copilot/rpc/fixtures/OverrideTools$$CopilotToolMeta.java | Update fixture constructor call to include null metadata arg. |
| java/src/test/java/com/github/copilot/rpc/fixtures/OptionalParamTools$$CopilotToolMeta.java | Update fixture constructor calls to include null metadata arg. |
| java/src/test/java/com/github/copilot/rpc/fixtures/MultiReturnTools$$CopilotToolMeta.java | Update fixture constructor calls to include null metadata arg. |
| java/src/test/java/com/github/copilot/rpc/fixtures/InvocationAwareTools$$CopilotToolMeta.java | Update fixture constructor calls to include null metadata arg. |
| java/src/test/java/com/github/copilot/rpc/fixtures/DefaultValueTools$$CopilotToolMeta.java | Update fixture constructor call to include null metadata arg. |
| java/src/test/java/com/github/copilot/rpc/fixtures/DateTimeTools$$CopilotToolMeta.java | Update fixture constructor call to include null metadata arg. |
| java/src/test/java/com/github/copilot/rpc/fixtures/ArgCoercionTools$$CopilotToolMeta.java | Update fixture constructor call to include null metadata arg. |
| java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools$$CopilotToolMeta.java | Update fixture constructor calls to include null metadata arg. |
Review details
- Files reviewed: 26/26 changed files
- Comments generated: 1
- Review effort level: Low
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>
6fdc349 to
0d575bc
Compare
| @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) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
after looking into it more yea this would be a breaking change 😭 it seems like overloading the record wth multiple constructors to maintain backwards compatibility is the recommendation from googling, but ill defer to @edburns for guidance 🙇🏼
There was a problem hiding this comment.
Hello @belaltaher8 , thanks for looping me in. Thankfully, even if its a breaking change, it's ok because we have the protection of the @CopilotExperimental annotation on some of the methods. But perhaps the impacted methods do not have that annotation on it. Will evaluate.
There was a problem hiding this comment.
Thanks for doing this, @belaltaher8. From my perspective as the Java SME, these two items are the most important. I realize this may come across as a pain in the ass, but Microsoft has decided that language idiomatic SDK surface area is a key differentiator from our competition. So when we add new features, we need to "localize" them with as much fidelity as possible.
-
ADR-005 gap: annotation-based tools still can’t express metadata
The new property bag is available on
ToolDefinition, but@CopilotToolusers (ToolDefinition.fromObject(...)/fromClass(...)) still can’t set it. Today:-
com.github.copilot.tool.CopilotToolhas no metadata member. -
com.github.copilot.tool.CopilotToolProcessor#writeToolDefinition(...)always emitsnullfor metadata.
So ADR-005 is functionally behind ADR-006 unless users manually post-process definitions.
What to change:
-
java/src/main/java/com/github/copilot/tool/CopilotTool.javaAdd nested annotation types and a new member:
-
MetadataEntry[] metadata() default {}; -
nested
@interface MetadataEntry { String key(); MetadataValue value(); } -
nested
@interface MetadataValue { MetadataFlag[] flags() default {}; boolean bool() default false; String str() default ""; } -
nested
@interface MetadataFlag { String name(); boolean value(); }
This keeps everything in one source file and avoids adding multiple top-level annotation classes.
-
-
java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.javaIn
writeToolDefinition(...), replace hardcodednullmetadata emission with generated map source:-
when
metadata().length == 0emitnull -
otherwise emit
Map.of(...)/ nestedMap.of(...)matching annotation values.
Add helper(s) that convert
CopilotTool.MetadataEntry[]to Java source literals. -
-
java/src/test/java/com/github/copilot/tool/CopilotToolProcessorTest.javaAdd compile-time generation assertions for:
-
metadata present → generated
new ToolDefinition(..., ..., ..., metadataMap)includes expected key(s) and nested flag map -
metadata absent → generated constructor argument is
null. -
Docs/examples
-
Update ADR-005 example snippet (currently old ctor shape) to include metadata position or, better, use stable factory-style examples.
-
-
-
Use an overloaded constructor for compatibility
Adding
metadataas a new record component changed constructor arity. To preserve compatibility for existing direct constructor call sites (including previously generated ADR-005 classes), add a backward-compatible overload.What to change:
java/src/main/java/com/github/copilot/rpc/ToolDefinition.java
Keep canonical 8-arg record constructor and add:
public ToolDefinition(String name, String description, Object parameters, ToolHandler handler, Boolean overridesBuiltInTool, Boolean skipPermission, ToolDefer defer) { this(name, description, parameters, handler, overridesBuiltInTool, skipPermission, defer, null); }
-
Keep PR’s 8-arg updates in factory methods and processor output.
-
Keep fluent copy methods carrying
metadatathrough (overridesBuiltInTool(...),skipPermission(...),defer(...)) and.metadata(...)setter-copy method.
Testing guidance (specific):
-
ToolDefinitionTest -
7-arg overload sets
metadata == null -
8-arg/copy
.metadata(...)serializesmetadata -
chaining flags before/after
.metadata(...)preserves metadata -
CopilotToolProcessorTest -
nested-annotation metadata generates expected
Map.of(...)source -
no metadata generates
nullargument -
combined flags + metadata all appear in generated constructor call
-
Generated fixture coverage
-
update
$$CopilotToolMetafixture files to include the final constructor shape and metadata emission behavior where applicable -
Run
-
cd java && mvn verifyto cover processor tests + runtime tests end-to-end. -
cd java && mvn spotless:applyto ensure formatting rules are kept.
-
I will paste the AI generated sketch of the new inner-annotations to enable @CopilotTool availability for your useful new feature next.
There was a problem hiding this comment.
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
package com.github.copilot.tool;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import com.github.copilot.CopilotExperimental;
import com.github.copilot.rpc.ToolDefer;
/**
* Marks a method as a Copilot tool. The annotated method will be exposed to the
* model as a callable tool during a session.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@CopilotExperimental
public @interface CopilotTool {
/** Tool description (sent to the model). */
String value();
/** Tool name. Defaults to method name converted to snake_case. */
String name() default "";
/** Whether this tool overrides a built-in tool. */
boolean overridesBuiltInTool() default false;
/** Whether to skip permission checks. */
boolean skipPermission() default false;
/** Defer configuration for this tool. */
ToolDefer defer() default ToolDefer.NONE;
/**
* Optional metadata bag for host-defined properties.
*
* Example emitted shape:
* Map.of("github.com/copilot:safeForTelemetry",
* Map.of("name", true, "inputsNames", false))
*/
MetadataEntry[] metadata() default {};
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
@interface MetadataEntry {
String key();
MetadataValue value();
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
@interface MetadataValue {
// Scalar options for simple cases
boolean bool() default false;
String str() default "";
// Object-like value for structured cases
MetadataFlag[] flags() default {};
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
@interface MetadataFlag {
String name();
boolean value();
}
}Example usage:
@CopilotTool(
value = "Reports phase",
metadata = {
@CopilotTool.MetadataEntry(
key = "github.com/copilot:safeForTelemetry",
value = @CopilotTool.MetadataValue(
flags = {
@CopilotTool.MetadataFlag(name = "name", value = true),
@CopilotTool.MetadataFlag(name = "inputsNames", value = false)
}
)
)
}
)
public String reportPhase(String phase) {
return phase;
}| /// forwards them verbatim to the runtime, which may recognize specific | ||
| /// keys to inform host-specific behavior. Unknown keys are preserved and | ||
| /// round-tripped untouched. | ||
| #[serde(default, skip_serializing_if = "HashMap::is_empty")] |
There was a problem hiding this comment.
I think skip_serializing_if = "HashMap::is_empty" might be inconsistent with the other SDKs, which would send empty dictionaries. Is there any situation where it would be important to (or not to) send an empty dictionary? My guess is it's semantically equivalent but just want to check.
There was a problem hiding this comment.
i think we're fine here. An empty metadata object carries no keys, so it decodes to absent in the other languages too. I believe that Go and Rust would omitempty. And the other SDKs default to null/undefined which omits
it'd be {} if someone passes an empty bag
- dotnet: change Metadata value type to IDictionary<string, JsonNode?> for
NativeAOT-safe serialization (CopilotToolOptions + wire ToolDefinition,
FromAIFunction cast, unit test); drop redundant [JsonPropertyName("metadata")]
since the Web camelCase policy already maps it; remove the <remarks> block
from the Metadata property.
- Reword metadata doc comments across nodejs/python/go/rust/java to describe
the bag opaquely without the SDK-vs-runtime/CLI distinction.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: bf2bf0dd-ec9f-42f5-a6e1-4f2a9e562d5f
…-passthrough # Conflicts: # python/test_client.py
edburns
left a comment
There was a problem hiding this comment.
Please see this comment.
Add an optional
_metadatabag to theTooldefinition (anddefineTool) and forward it verbatim over the session.create/resume RPC. This lets hosts attach namespaced, opaque metadata to tools without expanding the typed public contract; unknown keys are round-tripped untouched.