Skip to content

[Java] @CopilotTool ergonomics 4.3: Annotation processor (CopilotToolProcessor) #1760

Description

@edburns

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

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

Files to modify

  1. java/src/main/resources/META-INF/services/javax.annotation.processing.Processor — add com.github.copilot.tool.CopilotToolProcessor
  2. 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)

  1. Tool name: If @CopilotTool(name="...") is specified, use it. Otherwise, convert the method name to snake_case (e.g., setCurrentPhaseset_current_phase).

  2. Access level enforcement: Emit a compile error (Messager.printMessage(ERROR, ...)) if the annotated method is private. Package-private, protected, and public are all acceptable.

  3. 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(...)))
  4. 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).
  5. 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.
  6. 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.

  7. overridesBuiltInTool support: If true, use ToolDefinition.createOverride(...) instead of ToolDefinition.create(...).

  8. ObjectMapper source: Use com.github.copilot.rpc.RpcMapper.INSTANCE as a private static final field.

  9. 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:

  1. 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.

  2. 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.
  3. Compile error tests: Verify the processor emits compile errors for:

    • private methods annotated with @CopilotTool.
    • @Param(required=true, defaultValue="something") (conflicting attributes).
  4. 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.

  5. 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).

  6. 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.

  7. snake_case conversion test: Verify setCurrentPhaseset_current_phase, searchItemssearch_items, grepgrep (already snake_case).

  8. 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.

  9. Spotless format check: mvn spotless:check passes.

  10. 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).

Metadata

Metadata

Labels

No labels
No labels

Type

No fields configured for Task.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions