Overview
Create the JSR 269 annotation processor (CopilotToolProcessor) that finds @CopilotTool-annotated methods and generates $$CopilotToolMeta companion classes containing tool definitions, JSON Schema, and invocation lambdas.
Branch: edburns/1682-java-tool-ergonomics on upstream (⚠️ NOT main — PRs must target this branch)
Prerequisites
- Task 4.1 (annotations) and Task 4.2 (SchemaGenerator) must be complete and merged to the branch.
- Before writing any code, read the entire implementation plan at:
1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md
Relevant plan sections to carefully re-read
- Section 3.2 —
@CopilotTool annotation design (Resolution: RUNTIME retention, ToolDefer support)
- Section 3.3 —
@Param annotation design (Resolution: defaultValue semantics and validation rules)
- Section 3.5 — Generated code shape (Resolution: access levels, return type handling, argument deserialization patterns, ObjectMapper source)
- Section 3.7 —
module-info.java impact (Resolution: no issues, generated class in same package)
- Section 3.8 — Processor registration (Resolution: standard JSR 269 multi-processor registration)
- Section 4.3 — Annotation processor (
CopilotToolProcessor) (the primary task description)
Deliverables
Files to create
java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java
Files to modify
java/src/main/resources/META-INF/services/javax.annotation.processing.Processor — add com.github.copilot.tool.CopilotToolProcessor
java/src/main/java/module-info.java — add provides javax.annotation.processing.Processor with ..., com.github.copilot.tool.CopilotToolProcessor;
Generated code specification
For a class com.example.MyTools containing @CopilotTool methods, the processor generates com.example.MyTools$$CopilotToolMeta in the same package:
// GENERATED by CopilotToolProcessor — do not edit
package com.example;
import com.github.copilot.rpc.ToolDefinition;
import com.github.copilot.rpc.RpcMapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.*;
import java.util.concurrent.CompletableFuture;
final class MyTools$$CopilotToolMeta {
private static final ObjectMapper objectMapper = RpcMapper.INSTANCE;
static List<ToolDefinition> definitions(MyTools instance) {
return List.of(
ToolDefinition.create(
"tool_name",
"Tool description",
Map.of("type", "object",
"properties", Map.of("param1", Map.of("type", "string", "description", "...")),
"required", List.of("param1")),
invocation -> {
String param1 = (String) invocation.getArguments().get("param1");
return CompletableFuture.completedFuture(instance.myMethod(param1));
},
/* overridesBuiltInTool */ false,
/* skipPermission */ false,
/* defer */ null
)
);
}
}
Processor behavior rules (from resolutions)
-
Tool name: If @CopilotTool(name="...") is specified, use it. Otherwise, convert the method name to snake_case (e.g., setCurrentPhase → set_current_phase).
-
Access level enforcement: Emit a compile error (Messager.printMessage(ERROR, ...)) if the annotated method is private. Package-private, protected, and public are all acceptable.
-
Return type handling:
| Method return type |
Generated invocation code |
String |
CompletableFuture.completedFuture(instance.method(...)) |
CompletableFuture<String> |
instance.method(...) (use as-is) |
void |
instance.method(...); return CompletableFuture.completedFuture("Success") |
CompletableFuture<T> (non-String) |
instance.method(...).thenApply(r -> objectMapper.writeValueAsString(r)) |
Other T |
CompletableFuture.completedFuture(objectMapper.writeValueAsString(instance.method(...))) |
-
Argument deserialization:
- Simple types (
String, primitives, boxed): direct cast from invocation.getArguments().get("name") (e.g., (String) args.get("city"), ((Number) args.get("count")).intValue()).
- Complex types (enums, records, POJOs):
objectMapper.convertValue(invocation.getArguments().get("name"), TargetType.class).
- Single-record-parameter shortcut: When a method has exactly one parameter that is a record/POJO matching the full argument set, generate
invocation.getArgumentsAs(RecordType.class).
-
defaultValue handling:
- Emit JSON Schema
"default" key in the property schema.
- Generate code to apply the default at invocation time when the argument key is missing.
- Emit a compile error if
required=true AND defaultValue is non-empty.
-
ToolDefer support: If @CopilotTool(defer=ToolDefer.SOME_VALUE), pass it to ToolDefinition.create(...) or ToolDefinition.createOverride(...). Critical: when defer is ToolDefer.NONE (the annotation default), the generated code must pass null for the defer parameter — not ToolDefer.NONE itself. A non-null NONE reference would leak "defer": null onto the JSON-RPC wire payload. See the Javadoc on ToolDefer.NONE for details.
-
overridesBuiltInTool support: If true, use ToolDefinition.createOverride(...) instead of ToolDefinition.create(...).
-
ObjectMapper source: Use com.github.copilot.rpc.RpcMapper.INSTANCE as a private static final field.
-
Schema generation: Delegate to SchemaGenerator (from task 4.2) for type-to-schema mapping.
Gating tests and criteria
All of the following must pass before this task is considered complete:
-
Compilation test class: Create java/src/test/java/com/github/copilot/tool/CopilotToolProcessorTest.java using javax.tools.JavaCompiler programmatically to compile test sources with the processor and verify output.
-
Basic generation test: Compile a test class with 2-3 @CopilotTool methods. Verify:
$$CopilotToolMeta.java is generated.
- Generated file contains correct tool names (including snake_case conversion).
- Generated file contains correct schema maps matching expected JSON Schema.
- Generated file compiles without errors.
-
Compile error tests: Verify the processor emits compile errors for:
private methods annotated with @CopilotTool.
@Param(required=true, defaultValue="something") (conflicting attributes).
-
Return type tests: Compile test methods with each return type (String, void, CompletableFuture<String>, CompletableFuture<SomeRecord>, int) and verify the generated invocation code matches the expected pattern for each.
-
Argument coercion tests: Compile test methods with parameters of type String, int, boolean, an enum, and a record. Verify the generated argument extraction code uses the correct pattern (direct cast vs. objectMapper.convertValue).
-
Jackson round-trip test: Load the generated $$CopilotToolMeta class, call definitions(instance) to obtain the List<ToolDefinition>, then for each definition: serialize its inputSchema (Map<String, Object>) to JSON via RpcMapper.INSTANCE.writeValueAsString(...), verify the JSON contains the expected JSON Schema keys ("type", "properties", "required"), and deserialize back via readValue(...) to a Map and assert equality with the original. This proves the Map.of(...) literals the processor emits are wire-compatible with the Copilot JSON-RPC protocol.
-
snake_case conversion test: Verify setCurrentPhase → set_current_phase, searchItems → search_items, grep → grep (already snake_case).
-
Processor registration test: Verify the processor is correctly registered in META-INF/services/javax.annotation.processing.Processor and that it processes @CopilotTool annotations during normal mvn compile.
-
Spotless format check: mvn spotless:check passes.
-
Full test suite: mvn clean verify passes (existing tests and tasks 4.1–4.2 tests not broken).
Constraints
-
✅✅ YOU MUST run mvn spotless:apply before every commit.
-
The generated class must be in the same package as the annotated class (for package-private access).
-
Do NOT use java.lang.reflect in the processor itself — it operates on javax.lang.model types.
-
Do NOT modify any files outside the java/ directory (except the files listed in "Files to modify").
-
Follow existing code style (4-space indent, Javadoc on public APIs).
-
The processor must be incremental-safe (do not rely on processing order between rounds).
Overview
Create the JSR 269 annotation processor (
CopilotToolProcessor) that finds@CopilotTool-annotated methods and generates$$CopilotToolMetacompanion classes containing tool definitions, JSON Schema, and invocation lambdas.Branch:⚠️ NOT
edburns/1682-java-tool-ergonomicsonupstream(main— PRs must target this branch)Prerequisites
1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.mdRelevant plan sections to carefully re-read
@CopilotToolannotation design (Resolution: RUNTIME retention, ToolDefer support)@Paramannotation design (Resolution: defaultValue semantics and validation rules)module-info.javaimpact (Resolution: no issues, generated class in same package)CopilotToolProcessor) (the primary task description)Deliverables
Files to create
java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.javaFiles to modify
java/src/main/resources/META-INF/services/javax.annotation.processing.Processor— addcom.github.copilot.tool.CopilotToolProcessorjava/src/main/java/module-info.java— addprovides javax.annotation.processing.Processor with ..., com.github.copilot.tool.CopilotToolProcessor;Generated code specification
For a class
com.example.MyToolscontaining@CopilotToolmethods, the processor generatescom.example.MyTools$$CopilotToolMetain the same package:Processor behavior rules (from resolutions)
Tool name: If
@CopilotTool(name="...")is specified, use it. Otherwise, convert the method name tosnake_case(e.g.,setCurrentPhase→set_current_phase).Access level enforcement: Emit a compile error (
Messager.printMessage(ERROR, ...)) if the annotated method isprivate. Package-private, protected, and public are all acceptable.Return type handling:
StringCompletableFuture.completedFuture(instance.method(...))CompletableFuture<String>instance.method(...)(use as-is)voidinstance.method(...); return CompletableFuture.completedFuture("Success")CompletableFuture<T>(non-String)instance.method(...).thenApply(r -> objectMapper.writeValueAsString(r))TCompletableFuture.completedFuture(objectMapper.writeValueAsString(instance.method(...)))Argument deserialization:
String, primitives, boxed): direct cast frominvocation.getArguments().get("name")(e.g.,(String) args.get("city"),((Number) args.get("count")).intValue()).objectMapper.convertValue(invocation.getArguments().get("name"), TargetType.class).invocation.getArgumentsAs(RecordType.class).defaultValuehandling:"default"key in the property schema.required=trueANDdefaultValueis non-empty.ToolDefersupport: If@CopilotTool(defer=ToolDefer.SOME_VALUE), pass it toToolDefinition.create(...)orToolDefinition.createOverride(...). Critical: whendeferisToolDefer.NONE(the annotation default), the generated code must passnullfor the defer parameter — notToolDefer.NONEitself. A non-nullNONEreference would leak"defer": nullonto the JSON-RPC wire payload. See the Javadoc onToolDefer.NONEfor details.overridesBuiltInToolsupport: If true, useToolDefinition.createOverride(...)instead ofToolDefinition.create(...).ObjectMapper source: Use
com.github.copilot.rpc.RpcMapper.INSTANCEas aprivate static finalfield.Schema generation: Delegate to
SchemaGenerator(from task 4.2) for type-to-schema mapping.Gating tests and criteria
All of the following must pass before this task is considered complete:
Compilation test class: Create
java/src/test/java/com/github/copilot/tool/CopilotToolProcessorTest.javausingjavax.tools.JavaCompilerprogrammatically to compile test sources with the processor and verify output.Basic generation test: Compile a test class with 2-3
@CopilotToolmethods. Verify:$$CopilotToolMeta.javais generated.Compile error tests: Verify the processor emits compile errors for:
privatemethods annotated with@CopilotTool.@Param(required=true, defaultValue="something")(conflicting attributes).Return type tests: Compile test methods with each return type (
String,void,CompletableFuture<String>,CompletableFuture<SomeRecord>,int) and verify the generated invocation code matches the expected pattern for each.Argument coercion tests: Compile test methods with parameters of type
String,int,boolean, an enum, and a record. Verify the generated argument extraction code uses the correct pattern (direct cast vs.objectMapper.convertValue).Jackson round-trip test: Load the generated
$$CopilotToolMetaclass, calldefinitions(instance)to obtain theList<ToolDefinition>, then for each definition: serialize itsinputSchema(Map<String, Object>) to JSON viaRpcMapper.INSTANCE.writeValueAsString(...), verify the JSON contains the expected JSON Schema keys ("type","properties","required"), and deserialize back viareadValue(...)to aMapand assert equality with the original. This proves theMap.of(...)literals the processor emits are wire-compatible with the Copilot JSON-RPC protocol.snake_case conversion test: Verify
setCurrentPhase→set_current_phase,searchItems→search_items,grep→grep(already snake_case).Processor registration test: Verify the processor is correctly registered in
META-INF/services/javax.annotation.processing.Processorand that it processes@CopilotToolannotations during normalmvn compile.Spotless format check:
mvn spotless:checkpasses.Full test suite:
mvn clean verifypasses (existing tests and tasks 4.1–4.2 tests not broken).Constraints
✅✅ YOU MUST run
mvn spotless:applybefore every commit.The generated class must be in the same package as the annotated class (for package-private access).
Do NOT use
java.lang.reflectin the processor itself — it operates onjavax.lang.modeltypes.Do NOT modify any files outside the
java/directory (except the files listed in "Files to modify").Follow existing code style (4-space indent, Javadoc on public APIs).
The processor must be incremental-safe (do not rely on processing order between rounds).