feat(java): Add CopilotToolProcessor annotation processor (task 4.3)#1777
Conversation
Your branch is up to date with 'upstream/edburns/1682-java-tool-ergonomics'. Changes to be committed: (use "git restore --staged <file>..." to unstage) new file: 1682-java-tool-ergonomics-prompts-remove-before-merge/20260618-prompts.md Signed-off-by: Ed Burns <edburns@microsoft.com>
- Add NONE constant to ToolDefer enum for annotation default value - Create com.github.copilot.tool.CopilotTool annotation - Create com.github.copilot.tool.Param annotation - Export com.github.copilot.tool package in module-info.java - Add CopilotToolAnnotationTest verifying retention, targets, defaults Closes #1758
NONE is an annotation-only sentinel for @copilotTool(defer=...) defaults. Its @jsonvalue now returns null so @JsonInclude(NON_NULL) omits it from the JSON-RPC payload, matching the nullable/optional semantics used by all other SDKs (.NET CopilotToolDefer?, Node defer?, Go omitempty, Python | None, Rust Option<DeferMode>).
* WIP Phase 4.1 * Remove prompts, pre-merge * fix(java): correct ToolDefer.NONE Javadoc on @jsonvalue null semantics Clarify that @jsonvalue returning null does not cause field omission by @JsonInclude(NON_NULL) — it only changes the leak from "" to null. The primary protection is mapping NONE to a null field reference before constructing ToolDefinition (responsibility of the annotation processor and ToolDefinition.fromObject()). * fix(java): address three review comments Co-authored-by: edburns <75821+edburns@users.noreply.github.com> * Revert "Remove prompts, pre-merge" This reverts commit a4fe9b2. --------- Co-authored-by: Ed Burns <edburns@microsoft.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: edburns <75821+edburns@users.noreply.github.com>
…ity (#1766) * Initial plan * feat(java): add SchemaGenerator compile-time type-to-JSON-Schema utility Creates SchemaGenerator.java that maps javax.lang.model TypeMirror instances to JSON Schema source code literals (Map.of(...) expressions). Implements all 24 type mappings from the specification including: - Primitives and boxed types (int/Integer, long/Long, etc.) - String, UUID, OffsetDateTime - Collections (List<T>, Collection<T>, Set<T>) - Maps (Map<String, V> with typed values) - Arrays (String[]) - Enums (with constant enumeration) - Records and POJOs (with properties/required) - Optional<T>, OptionalInt, OptionalDouble - Sealed interfaces (oneOf) - JsonNode and Object (any) Also adds SchemaGeneratorTest using compilation-testing approach with javax.tools.JavaCompiler to exercise the generator at compile time. Closes #1759 * fix: address code review - remove unused param, handle all primitive types * fix(java): correct SimpleJavaFileObject override - getCharContent not getContent Co-authored-by: edburns <75821+edburns@users.noreply.github.com> * spotless * Remove .class files generated by test * spotless * fix: use Map.ofEntries for properties to avoid Map.of 10-entry limit Address review comment r3461777483: Map.of() only supports up to 10 key-value pairs. Switch properties maps in SchemaGenerator to use Map.ofEntries(Map.entry(...), ...) so records/POJOs/methods with >10 fields won't cause generated source compilation failures. Update SchemaGeneratorTest expectations to match the new format. * fix: add missing Byte/Short/Character boxed type mappings Address review comment r3461777428: Byte and Short now map to "integer", Character maps to "string", matching their primitive equivalents. Add tests for all three. * fix: add missing OptionalLong mapping in generateDeclaredTypeSchema Address review comment r3461777459: OptionalLong was handled in isOptionalType/unwrapOptional but missing from generateDeclaredTypeSchema, causing it to fall through to POJO introspection when used as a direct return type. Add the mapping and tests for OptionalInt, OptionalLong, and OptionalDouble. * fix: correct misleading @JsonSubTypes comment on sealed interface handling Address review comment r3461777579: the implementation uses getPermittedSubclasses() (Java sealed types), not Jackson annotations. * test: add sealed interface test for oneOf schema generation Address review comment r3461777685: the processor had special handling for TestSealed* types but no test exercised generateSealedSchema(). Add a test with a sealed interface (TestSealedShape) and two record permits (Circle, Rect) verifying the oneOf schema output. * test: add >10-field record test proving Map.ofEntries compiles Address review comment r3461777706: add a test with an 11-component record that verifies the generated Map.ofEntries(...) expression actually compiles, proving the Map.of 10-entry limit fix works end-to-end. --------- 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>
Implements JSR 269 annotation processor that finds @CopilotTool-annotated methods and generates $$CopilotToolMeta companion classes containing tool definitions, JSON Schema, and invocation lambdas. Key features: - snake_case tool name conversion from camelCase method names - Access level enforcement (compile error for private methods) - Return type handling (String, void, CompletableFuture<String>, etc.) - Argument deserialization (direct cast for primitives/String, convertValue for complex) - @Param description and defaultValue support in schema - ToolDefer support (NONE maps to null/regular create) - overridesBuiltInTool and skipPermission support Also includes comprehensive test suite using javax.tools.JavaCompiler programmatic compilation. Closes #1760 Co-authored-by: edburns <75821+edburns@users.noreply.github.com>
- Use fully qualified type names in generated code for type safety - Fix Files.walk() resource leak in test with try-with-resources - Rename exception variables for clarity Co-authored-by: edburns <75821+edburns@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
edburns
left a comment
There was a problem hiding this comment.
Both the JDK 25 and JDK 17 tests failed.
https://github.com/github/copilot-sdk/actions/runs/28055309594/
Please investigate, fix, run-tests locally, and then push commits.
- Remove unused Collections import
- Reformat boolean expressions: && at start of continuation lines
- Reformat ternary: ? at start of continuation line
- Reformat .replace() chain with one call per line
- Fix hasErrorContaining stream method chain formatting
- Fix resolveClasspath() to use System.getProperty("java.class.path")
first, ensuring Jackson and all test deps are available when compiling
generated $$CopilotToolMeta code
This comment has been minimized.
This comment has been minimized.
- Merge propertyEntries.add() onto one line per formatter requirement - Fix sb.append() chain formatting to match Eclipse formatter output - Revert escapeJava to original line-breaking style (formatter preference) - Fix resolveClasspath() to combine system classpath with CodeSource paths from key classes (SDK, Jackson, RPC types) ensuring all dependencies are available for javac in the annotation processor test
The generated 6342CopilotToolMeta code uses ObjectMapper which requires jackson-core (Versioned, JsonFactory) and jackson-annotations at compile time. Add these transitive dependencies to the key classes list so their CodeSource paths are included in the javac classpath.
Investigated and fixed both CI failures across multiple iterations: JDK 25 (Spotless formatting): Applied all Eclipse formatter-required changes to JDK 17 (test classpath): The Latest commit: |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Generated by SDK Consistency Review Agent for issue #1777 · sonnet46 1.8M
There was a problem hiding this comment.
Pull request overview
Adds a Java JSR-269 annotation processor to generate $$CopilotToolMeta companion classes for @CopilotTool methods (compile-time tool metadata + invocation wiring), and wires it into the Java module and service loader with compiler-based tests.
Changes:
- Implement
CopilotToolProcessorto discover@CopilotToolmethods, validate invalid usages, and generate per-class$$CopilotToolMetasources. - Register the processor via
META-INF/servicesandmodule-info.java. - Add
JavaCompiler-driven tests validating generation, errors, and key codegen behaviors.
Show a summary per file
| File | Description |
|---|---|
| java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java | Implements the annotation processor and code generation for $$CopilotToolMeta. |
| java/src/main/resources/META-INF/services/javax.annotation.processing.Processor | Registers the new processor for service loading. |
| java/src/main/java/module-info.java | Adds the processor to the module provides clause. |
| java/src/test/java/com/github/copilot/tool/CopilotToolProcessorTest.java | Programmatic compiler tests for generation and validation behaviors. |
Copilot's findings
- Files reviewed: 4/4 changed files
- Comments generated: 4
403ac7d to
d7097f2
Compare
…eta contract Address PR #1777 review comment (r3463252393): the generated $$CopilotToolMeta class was using `new ObjectMapper()`, which lacks the SDK Jackson configuration (JavaTimeModule, NON_NULL inclusion, lenient unknown-properties). This would break tool argument coercion and return serialization at runtime for java.time.* and other types. Instead of embedding a bare or configured ObjectMapper in the generated code, change the generated `definitions()` method signature from: definitions(MyTools instance) to: definitions(MyTools instance, ObjectMapper mapper) This establishes an internal contract: the caller (the future ToolDefinition.fromObject() in issue #1761) is responsible for supplying a properly configured mapper via reflective invocation. The generated code uses `mapper` for all convertValue() and writeValueAsString() calls. Benefits: - No DRY violation (mapper config stays canonical in JsonRpcClient) - No new public API exposing ObjectMapper - No package-visibility workarounds - Clean separation: generated code declares its needs, caller supplies Issue #1761 description has been updated to document this contract so the implementing agent knows to pass ObjectMapper as the second argument when reflectively invoking definitions().
This comment has been minimized.
This comment has been minimized.
Address review comment on PR #1777: the isRecordOrPojo heuristic incorrectly triggered for JDK container types (List, Map, etc.) when used as a single tool parameter. For example, a tool with parameter List<String> would attempt to deserialize the entire arguments object as a List, failing at runtime. Replace the heuristic with a deterministic check: only Java records qualify for the getArgumentsAs() shortcut. Records are immutable data carriers with compiler-guaranteed component lists, making them safe for whole-object deserialization. POJOs and all other class types now fall through to the per-field extraction path, which always works correctly. Removed isSimpleType() helper which was only used by the old heuristic.
Address review comment on PR #1777: @Param(defaultValue=...) was always emitted as a JSON string in the generated schema's 'default' field, making numeric and boolean defaults the wrong type (e.g., "10" instead of 10, "true" instead of true). Changes: - withMeta helper: String defaultValue -> Object defaultValue - buildPropertySchema: reuse generateDefaultLiteral() to emit typed Java literals (int, boolean, etc.) instead of always quoting - Add test emitsTypedDefaultValuesInSchema verifying int -> 10, boolean -> true, String -> "hello" in generated code
Address review comment on PR #1777: getGeneratedSource() fallback search appended 61059CopilotToolMeta to a simpleName that already contained it, producing MyTools$$CopilotToolMeta$$CopilotToolMeta. Simplify to just match on 'class <simpleName>'.
Address SDK Consistency Review on PR #1777: the if/else if chain in writeToolDefinition silently dropped combined annotation flags (e.g., overridesBuiltInTool + skipPermission + defer). All other SDKs support combining these flags simultaneously. Replace the factory method dispatch with a direct call to the ToolDefinition record constructor, which accepts all seven fields independently. Each flag is now emitted as its own argument: Boolean.TRUE or null for overridesBuiltInTool/skipPermission, ToolDefer.X or null for defer. Add test generatesCombinedFlags verifying all three flags appear in generated code when set together.
Cross-SDK Consistency Review ✅This PR adds a Java-specific annotation processor (
Feature parity ✅The three tool option attributes introduced by
|
Fixes #1760 .
Implements the JSR 269 annotation processor that finds
@CopilotTool-annotated methods and generates$$CopilotToolMetacompanion classes at compile time — zero reflection, zero-parametersflag requirement.Changes
CopilotToolProcessor.java— The processor that:@CopilotToolmethods by enclosing class, generates one$$CopilotToolMetaper class in the same packageprivatemethods andrequired=true+defaultValueconflictssnake_case(or uses explicit@CopilotTool(name=...))SchemaGeneratorfor type→JSON Schema mapping, adds@Paramdescription/default viawithMetahelperToolDefinition.create/createOverride/createSkipPermission/createWithDeferbased on annotation attributesToolDefer.NONE→null(regularcreate), non-NONE →createWithDeferMETA-INF/services/javax.annotation.processing.Processor— RegistersCopilotToolProcessormodule-info.java— Adds toprovidesclauseCopilotToolProcessorTest.java— Programmaticjavax.tools.JavaCompilertests covering generation, error cases, return types, arg coercion, schema output, and service registrationGenerated code shape