Skip to content

Commit e82a114

Browse files
CopilotedburnsCopilot
authored
Rename Param annotation to CopilotToolParam in Java SDK (#1838)
* Initial plan * Rename Param annotation to CopilotToolParam across Java SDK Rename the class `com.github.copilot.tool.Param` to `com.github.copilot.tool.CopilotToolParam` to free the `Param` name for the tool-as-lambda API variant. Updates all imports, usages (@Param → @CopilotToolParam), reflection references (Param.class → CopilotToolParam.class), error messages in CopilotToolProcessor, Javadoc, test fixtures, README, and ADR documentation. Closes #1837 Co-authored-by: edburns <75821+edburns@users.noreply.github.com> * Fix remaining Param type declarations to CopilotToolParam Co-authored-by: edburns <75821+edburns@users.noreply.github.com> * Spotless apply On branch edburns/review-copilot-pr-1838 modified: src/main/java/com/github/copilot/tool/CopilotTool.java modified: src/main/java/com/github/copilot/tool/CopilotToolParam.java modified: src/main/java/com/github/copilot/tool/CopilotToolProcessor.java modified: src/test/java/com/github/copilot/rpc/fixtures/OptionalParamTools.java modified: src/test/java/com/github/copilot/rpc/fixtures/SimpleTools.java modified: src/test/java/com/github/copilot/tool/CopilotToolAnnotationTest.java * Fix ADR-005 Option 2 example: @CopilotToolParam cannot target record components The Option 2 code example incorrectly placed @CopilotToolParam on a record component. Since @CopilotToolParam targets ElementType.PARAMETER only (method parameters), this is not valid Java. - Remove @CopilotToolParam from the PhaseArgs record component - Update the prose to explain the PARAMETER-only target constraint - Add a Drawbacks bullet noting per-field descriptions are unsupported in Option 2 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: edburns <75821+edburns@users.noreply.github.com> Co-authored-by: Ed Burns <edburns@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c90f47b commit e82a114

18 files changed

Lines changed: 150 additions & 141 deletions

java/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,12 @@ When you define tools with `@CopilotTool`, parameters of type `ToolInvocation` a
140140
```java
141141
import com.github.copilot.rpc.ToolInvocation;
142142
import com.github.copilot.tool.CopilotTool;
143-
import com.github.copilot.tool.Param;
143+
import com.github.copilot.tool.CopilotToolParam;
144144

145145
class ProgressTools {
146146
@CopilotTool("Reports the current phase and session")
147147
public String reportProgress(
148-
@Param("Current phase") String phase,
148+
@CopilotToolParam("Current phase") String phase,
149149
ToolInvocation invocation) {
150150
return "phase=" + phase + ", sessionId=" + invocation.getSessionId();
151151
}
@@ -156,13 +156,13 @@ Position examples:
156156

157157
```java
158158
@CopilotTool("Invocation first")
159-
public String report(ToolInvocation invocation, @Param("Phase") String phase) { ... }
159+
public String report(ToolInvocation invocation, @CopilotToolParam("Phase") String phase) { ... }
160160

161161
@CopilotTool("Invocation only")
162162
public String onlyContext(ToolInvocation invocation) { ... }
163163

164164
@CopilotTool("Invocation middle")
165-
public String report(@Param("Phase") String phase, ToolInvocation invocation, @Param("Limit") int limit) { ... }
165+
public String report(@CopilotToolParam("Phase") String phase, ToolInvocation invocation, @CopilotToolParam("Limit") int limit) { ... }
166166
```
167167

168168
## Memory

java/docs/adr/adr-005-tool-definition.md

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ Explicit `ToolDefinition.create(name, description, schema, handler)` with a hand
5151

5252
### Option 2: Record-as-schema with generic factory
5353

54-
Define a record for the tool's arguments, annotate its components with `@Param`, and use a generic factory method to auto-generate the schema from the record's `RecordComponent[]` metadata:
54+
Define a record for the tool's arguments and use a generic factory method to auto-generate the schema from the record's `RecordComponent[]` metadata. Because `@CopilotToolParam` targets `ElementType.PARAMETER` (method parameters only), it cannot be placed on record components; per-field descriptions are not supported in this option:
5555

5656
```java
57-
record PhaseArgs(@Param("The phase to transition to") Phase phase) {}
57+
record PhaseArgs(Phase phase) {}
5858

5959
ToolDefinition.define("set_current_phase",
6060
"Sets the current phase of the agent.",
@@ -76,26 +76,27 @@ ToolDefinition.define("set_current_phase",
7676
- Tool name and description are still explicit string arguments.
7777
- Requires a separate record class for every tool's args (even trivial single-param tools).
7878
- The handler is still an explicit lambda — the "tool" is not the method itself.
79+
- Per-field descriptions cannot be provided: `@CopilotToolParam` targets method parameters only, not record components.
7980
- Nested or complex schemas (arrays of objects, polymorphic types) need additional mapping logic.
8081
- No analog in the broader Java ecosystem; Java developers are not accustomed to defining a record per function call.
8182

8283
### Option 3: Annotation-on-method (langchain4j-style)
8384

84-
Annotate existing Java methods with `@Tool` (or a Copilot-specific equivalent) and annotate parameters with `@P`/`@Param`. The framework discovers tools by scanning methods on a given object, auto-generates `ToolSpecification` / `ToolDefinition` from the method signature, and dispatches invocations directly to the annotated method.
85+
Annotate existing Java methods with `@Tool` (or a Copilot-specific equivalent) and annotate parameters with `@P`/`@CopilotToolParam`. The framework discovers tools by scanning methods on a given object, auto-generates `ToolSpecification` / `ToolDefinition` from the method signature, and dispatches invocations directly to the annotated method.
8586

8687
```java
8788
class MyTools {
8889

8990
@CopilotTool("Sets the current phase of the agent. Use this to report progress.")
90-
String setCurrentPhase(@Param("The phase to transition to") Phase phase) {
91+
String setCurrentPhase(@CopilotToolParam("The phase to transition to") Phase phase) {
9192
this.phase = phase;
9293
updateUi();
9394
return "Phase set to " + phase;
9495
}
9596

9697
@CopilotTool(name = "report_intent", value = "Reports the agent's intent",
9798
overridesBuiltInTool = true)
98-
String reportIntent(@Param("The intent") String intent) {
99+
String reportIntent(@CopilotToolParam("The intent") String intent) {
99100
// ...
100101
}
101102
}
@@ -110,7 +111,7 @@ This is the approach used by [langchain4j](https://github.com/langchain4j/langch
110111
**What the framework does automatically:**
111112
1. **Name** — derived from `@CopilotTool(name=...)` or the method name (converted to snake_case).
112113
2. **Description** — from `@CopilotTool("...")` or `@CopilotTool(value="...")`.
113-
3. **Parameter schema** — generated by reflecting on method parameters: types map to JSON Schema types; `@Param` provides descriptions; `Optional<T>` or `@Param(required=false)` marks optional params.
114+
3. **Parameter schema** — generated by reflecting on method parameters: types map to JSON Schema types; `@CopilotToolParam` provides descriptions; `Optional<T>` or `@CopilotToolParam(required=false)` marks optional params.
114115
4. **Handler** — the method itself. The framework deserializes JSON arguments into the method's parameter types and invokes the method reflectively. The return value is serialized back to a string result.
115116

116117
**Advantages:**
@@ -127,8 +128,8 @@ This is the approach used by [langchain4j](https://github.com/langchain4j/langch
127128
- One-time scanning cost at registration time (negligible for typical tool counts).
128129
- Return type handling needs a policy: `String` → sent as-is; `void` → "Success"; other types → JSON-serialized.
129130
- Async story: methods could return `CompletableFuture<T>` for async tools, or the framework could invoke synchronous methods on a configurable executor.
130-
- New annotation(s) added to the public API surface (`@CopilotTool`, `@Param`).
131-
- Requires `-parameters` javac flag for parameter name preservation (or explicit `@Param(name=...)` — same constraint as langchain4j).
131+
- New annotation(s) added to the public API surface (`@CopilotTool`, `@CopilotToolParam`).
132+
- Requires `-parameters` javac flag for parameter name preservation (or explicit `@CopilotToolParam(name=...)` — same constraint as langchain4j).
132133

133134
## Decision Outcome
134135

@@ -141,7 +142,7 @@ This is the approach used by [langchain4j](https://github.com/langchain4j/langch
141142
2. **Minimum viable tool is one annotated method.** With Option 3, the absolute minimum code to define a tool is:
142143
```java
143144
@CopilotTool("Gets the weather")
144-
String getWeather(@Param("City") String city) { return weatherApi.get(city); }
145+
String getWeather(@CopilotToolParam("City") String city) { return weatherApi.get(city); }
145146
```
146147
With Option 2, you need a record class *and* a lambda. With Option 1, you need a record class, a Map schema, *and* a lambda.
147148

@@ -191,7 +192,7 @@ At runtime, `ToolDefinition.fromObject(myTools)` loads the generated `$$CopilotT
191192
### Compile-time validation
192193

193194
Because the processor has full access to the source AST, it can emit compile errors for:
194-
- Missing `@Param` on parameters (when descriptions are required by policy).
195+
- Missing `@CopilotToolParam` on parameters (when descriptions are required by policy).
195196
- Unsupported parameter types (types without a clear JSON Schema mapping).
196197
- Duplicate tool names within the same class hierarchy.
197198
- Invalid annotation combinations (e.g., `overridesBuiltInTool` on a tool with `skipPermission`).
@@ -217,14 +218,14 @@ Because the processor has full access to the source AST, it can emit compile err
217218

218219
## Consequences
219220

220-
- New public annotations: `@CopilotTool` and `@Param` (in `com.github.copilot.rpc` or a new `com.github.copilot.tool` package).
221+
- New public annotations: `@CopilotTool` and `@CopilotToolParam` (in `com.github.copilot.rpc` or a new `com.github.copilot.tool` package).
221222
- New JSR 269 annotation processor that generates `$$CopilotToolMeta` companion classes at compile time.
222223
- New utility: `ToolDefinition.fromObject(Object)` / `ToolDefinition.fromClass(Class<?>)` that loads the generated metadata class (falling back to runtime reflection if the processor was not run).
223224
- The existing `ToolDefinition.create(...)` / `ToolDefinition.createOverride(...)` APIs remain unchanged — they become the "low-level" path.
224225
- No `-parameters` javac flag requirement for users who run the annotation processor (which happens automatically when the SDK is on the compile classpath).
225226
- Async support: methods returning `CompletableFuture<T>` are handled natively; synchronous methods are wrapped in `CompletableFuture.completedFuture(...)` (or dispatched to an executor, TBD).
226227
- GraalVM native-image compatibility without additional reflection configuration.
227-
- **Experimental designation:** `@CopilotTool`, `@Param`, `ToolDefinition.fromObject(Object)`, and `ToolDefinition.fromClass(Class<?>)` will all be annotated with `@CopilotExperimental`. This gates adoption behind an explicit opt-in (`-Acopilot.experimental.allowed=true`) until the API surface stabilizes, consistent with the policy established in ADR-004.
228+
- **Experimental designation:** `@CopilotTool`, `@CopilotToolParam`, `ToolDefinition.fromObject(Object)`, and `ToolDefinition.fromClass(Class<?>)` will all be annotated with `@CopilotExperimental`. This gates adoption behind an explicit opt-in (`-Acopilot.experimental.allowed=true`) until the API surface stabilizes, consistent with the policy established in ADR-004.
228229

229230
## Related work items
230231

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
*
2323
* <pre>
2424
* &#64;CopilotTool("Get weather for a location")
25-
* public CompletableFuture&lt;String&gt; getWeather(&#64;Param(value = "City name", required = true) String location) {
25+
* public CompletableFuture&lt;String&gt; getWeather(
26+
* &#64;CopilotToolParam(value = "City name", required = true) String location) {
2627
* return CompletableFuture.completedFuture("Sunny in " + location);
2728
* }
2829
* </pre>

java/src/main/java/com/github/copilot/tool/Param.java renamed to java/src/main/java/com/github/copilot/tool/CopilotToolParam.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@
2121
*
2222
* <pre>
2323
* &#64;CopilotTool("Search for issues")
24-
* public CompletableFuture&lt;String&gt; searchIssues(&#64;Param(value = "Search query", required = true) String query,
25-
* &#64;Param(value = "Max results", required = false, defaultValue = "10") int limit) {
24+
* public CompletableFuture&lt;String&gt; searchIssues(
25+
* &#64;CopilotToolParam(value = "Search query", required = true) String query,
26+
* &#64;CopilotToolParam(value = "Max results", required = false, defaultValue = "10") int limit) {
2627
* // ...
2728
* }
2829
* </pre>
@@ -33,7 +34,7 @@
3334
@Retention(RetentionPolicy.RUNTIME)
3435
@Target(ElementType.PARAMETER)
3536
@CopilotExperimental
36-
public @interface Param {
37+
public @interface CopilotToolParam {
3738

3839
/** Parameter description (sent to the model). */
3940
String value() default "";

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

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -68,23 +68,23 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
6868
continue;
6969
}
7070

71-
// Validate @Param conflicts
71+
// Validate @CopilotToolParam conflicts
7272
int toolInvocationParamCount = 0;
7373
for (VariableElement param : method.getParameters()) {
7474
if (isToolInvocationType(param.asType())) {
7575
toolInvocationParamCount++;
76-
if (param.getAnnotation(Param.class) != null) {
76+
if (param.getAnnotation(CopilotToolParam.class) != null) {
7777
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
78-
"@Param is not supported on ToolInvocation parameters because ToolInvocation is injected runtime context and not part of the tool schema",
78+
"@CopilotToolParam is not supported on ToolInvocation parameters because ToolInvocation is injected runtime context and not part of the tool schema",
7979
param);
8080
}
8181
continue;
8282
}
83-
Param paramAnnotation = param.getAnnotation(Param.class);
83+
CopilotToolParam paramAnnotation = param.getAnnotation(CopilotToolParam.class);
8484
if (paramAnnotation != null && paramAnnotation.required()
8585
&& !paramAnnotation.defaultValue().isEmpty()) {
8686
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
87-
"@Param cannot have both required=true and a non-empty defaultValue", param);
87+
"@CopilotToolParam cannot have both required=true and a non-empty defaultValue", param);
8888
}
8989
if (paramAnnotation != null && !paramAnnotation.defaultValue().isEmpty()) {
9090
String defaultValidationError = validateDefaultValueCompatibility(param.asType(),
@@ -96,7 +96,7 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
9696
if (paramAnnotation != null && !paramAnnotation.required() && paramAnnotation.defaultValue().isEmpty()
9797
&& param.asType().getKind().isPrimitive()) {
9898
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
99-
"@Param(required=false) primitive parameters must provide defaultValue or use a boxed/Optional type",
99+
"@CopilotToolParam(required=false) primitive parameters must provide defaultValue or use a boxed/Optional type",
100100
param);
101101
}
102102
}
@@ -111,17 +111,17 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
111111
if (schemaParameters.size() == 1) {
112112
VariableElement singleParam = schemaParameters.get(0);
113113
if (isRecord(singleParam.asType())) {
114-
Param paramAnnotation = singleParam.getAnnotation(Param.class);
114+
CopilotToolParam paramAnnotation = singleParam.getAnnotation(CopilotToolParam.class);
115115
if (paramAnnotation != null) {
116116
if (!paramAnnotation.defaultValue().isEmpty()) {
117117
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
118-
"@Param(defaultValue=...) is not supported on single-record tool parameters; use record component defaults or a non-record parameter",
118+
"@CopilotToolParam(defaultValue=...) is not supported on single-record tool parameters; use record component defaults or a non-record parameter",
119119
singleParam);
120120
}
121121
if (!paramAnnotation.name().isEmpty() || !paramAnnotation.value().isEmpty()
122122
|| !paramAnnotation.required()) {
123123
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
124-
"@Param name/value/required are not supported on single-record tool parameters; annotate record components instead",
124+
"@CopilotToolParam name/value/required are not supported on single-record tool parameters; annotate record components instead",
125125
singleParam);
126126
}
127127
}
@@ -235,7 +235,7 @@ private void writeMetaClass(PrintWriter out, String packageName, String simpleCl
235235
private boolean needsWithMetaHelper(List<ExecutableElement> methods) {
236236
for (ExecutableElement method : methods) {
237237
for (VariableElement param : method.getParameters()) {
238-
Param paramAnnotation = param.getAnnotation(Param.class);
238+
CopilotToolParam paramAnnotation = param.getAnnotation(CopilotToolParam.class);
239239
if (paramAnnotation != null
240240
&& (!paramAnnotation.value().isEmpty() || !paramAnnotation.defaultValue().isEmpty())) {
241241
return true;
@@ -255,7 +255,8 @@ private void writeToolDefinition(PrintWriter out, ExecutableElement method) {
255255
boolean skipPermission = annotation.skipPermission();
256256
com.github.copilot.rpc.ToolDefer defer = annotation.defer();
257257

258-
// Generate schema with @Param metadata (descriptions, names, defaults)
258+
// Generate schema with @CopilotToolParam metadata (descriptions, names,
259+
// defaults)
259260
String schemaSource = generateSchemaWithParamMetadata(method.getParameters());
260261

261262
// Generate invocation lambda
@@ -296,7 +297,7 @@ private String generateSchemaWithParamMetadata(List<? extends VariableElement> p
296297
for (VariableElement param : schemaParameters) {
297298
String paramName = getParamName(param);
298299
TypeMirror paramType = param.asType();
299-
Param paramAnnotation = param.getAnnotation(Param.class);
300+
CopilotToolParam paramAnnotation = param.getAnnotation(CopilotToolParam.class);
300301

301302
// Generate the type schema for this parameter
302303
String typeSchema = schemaGenerator.generateSchemaSource(paramType, processingEnv.getTypeUtils(),
@@ -338,7 +339,7 @@ private boolean isToolInvocationType(TypeMirror type) {
338339
return TOOL_INVOCATION_TYPE.equals(processingEnv.getTypeUtils().erasure(type).toString());
339340
}
340341

341-
private String buildPropertySchema(String typeSchema, Param paramAnnotation, TypeMirror paramType) {
342+
private String buildPropertySchema(String typeSchema, CopilotToolParam paramAnnotation, TypeMirror paramType) {
342343
if (paramAnnotation == null) {
343344
return typeSchema;
344345
}
@@ -382,7 +383,7 @@ private String generateLambdaBody(ExecutableElement method) {
382383
TypeMirror paramType = param.asType();
383384

384385
// Handle default values
385-
Param paramAnnotation = param.getAnnotation(Param.class);
386+
CopilotToolParam paramAnnotation = param.getAnnotation(CopilotToolParam.class);
386387
boolean hasDefault = paramAnnotation != null && !paramAnnotation.defaultValue().isEmpty();
387388

388389
if (hasDefault) {
@@ -695,7 +696,7 @@ private String validatePrimitiveDefault(TypeKind kind, String defaultValue) {
695696
return null;
696697
}
697698
} catch (NumberFormatException ex) {
698-
return "@Param defaultValue '" + defaultValue + "' is not valid for " + kind.name().toLowerCase()
699+
return "@CopilotToolParam defaultValue '" + defaultValue + "' is not valid for " + kind.name().toLowerCase()
699700
+ " parameters";
700701
}
701702
}
@@ -704,13 +705,13 @@ private String validateBooleanDefault(String defaultValue) {
704705
if ("true".equalsIgnoreCase(defaultValue) || "false".equalsIgnoreCase(defaultValue)) {
705706
return null;
706707
}
707-
return "@Param defaultValue '" + defaultValue + "' is not valid for boolean parameters";
708+
return "@CopilotToolParam defaultValue '" + defaultValue + "' is not valid for boolean parameters";
708709
}
709710

710711
private String validateCharacterDefault(String defaultValue) {
711712
return defaultValue != null && defaultValue.length() == 1
712713
? null
713-
: "@Param defaultValue '" + defaultValue + "' is not valid for char parameters";
714+
: "@CopilotToolParam defaultValue '" + defaultValue + "' is not valid for char parameters";
714715
}
715716

716717
private TypeKind boxedTypeKind(String qualifiedName) {
@@ -733,7 +734,7 @@ private TypeKind boxedTypeKind(String qualifiedName) {
733734
}
734735

735736
private String getParamName(VariableElement param) {
736-
Param paramAnnotation = param.getAnnotation(Param.class);
737+
CopilotToolParam paramAnnotation = param.getAnnotation(CopilotToolParam.class);
737738
if (paramAnnotation != null && !paramAnnotation.name().isEmpty()) {
738739
return paramAnnotation.name();
739740
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ public String generateParametersSchemaSource(List<? extends VariableElement> par
9090
propertyEntries.add("Map.entry(\"" + paramName + "\", " + schema + ")");
9191

9292
if (!isOptional) {
93-
Param paramAnnotation = param.getAnnotation(Param.class);
93+
CopilotToolParam paramAnnotation = param.getAnnotation(CopilotToolParam.class);
9494
if (paramAnnotation == null || paramAnnotation.required()) {
9595
requiredNames.add("\"" + paramName + "\"");
9696
}

0 commit comments

Comments
 (0)