Skip to content

Commit 783aa69

Browse files
edburnsCopilotCopilotCopilot
authored
Java: Implement @CopilotTool ergonomics (#1792)
* Resume 1682 iterating * Phase 03 answer questions * On branch edburns/1682-java-tool-ergonomics 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> * WIP: Phase 3. Question 3.4 * WIP: Phase 3. Question 3.6 * WIP: Phase 3. Question 3.6: Answer * Answer 3.7 * Resolve 3.8 * Initial plan * feat(java): create @copilotTool and @Param annotations with tests - 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 * spotless * fix(java): make ToolDefer.NONE serialize as null to prevent wire leak 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 * feat(java): create @copilotTool and @Param annotations (#1763) * 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> * Initial plan * feat(java): add SchemaGenerator compile-time type-to-JSON-Schema utility (#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> * WIP 4.3 * feat(java): Add CopilotToolProcessor annotation processor (task 4.3) (#1777) * Resume 1682 iterating * Phase 03 answer questions * On branch edburns/1682-java-tool-ergonomics 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> * WIP: Phase 3. Question 3.4 * WIP: Phase 3. Question 3.6 * WIP: Phase 3. Question 3.6: Answer * Answer 3.7 * Resolve 3.8 * Initial plan * feat(java): create @copilotTool and @Param annotations with tests - 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 * spotless * fix(java): make ToolDefer.NONE serialize as null to prevent wire leak 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 * feat(java): create @copilotTool and @Param annotations (#1763) * 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> * Initial plan * feat(java): add SchemaGenerator compile-time type-to-JSON-Schema utility (#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> * WIP 4.3 * Initial plan * feat(java): Add CopilotToolProcessor annotation processor (task 4.3) 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> * fix: Address code review feedback - 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> * fix: Fix Spotless formatting and test classpath for JDK 17 - 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 * fix: Fix remaining Spotless violations and test classpath resolution - 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 * fix: Add jackson-core and jackson-annotations to test classpath 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. * fix: Fix Spotless formatting for keyClasses array initializer * fix(java): Pass ObjectMapper as parameter in generated $$CopilotToolMeta 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(). * fix(java): restrict single-param shortcut to records only 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. * fix(java): emit typed default values in JSON Schema 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 * fix(java): fix double 61059CopilotToolMeta suffix in test helper 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>'. * fix(java): use record constructor for independent flag combination 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. --------- Signed-off-by: Ed Burns <edburns@microsoft.com> 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> * Give us this day our daily prompts * feat(java): Add ToolDefinition.fromObject() and fromClass() registration API (#1779) * Initial plan * feat(java): Add ToolDefinition.fromObject() and fromClass() static methods Adds static methods that load processor-generated $$CopilotToolMeta classes and return List<ToolDefinition> with fully working tool definitions (schema + invocation handlers). - fromObject(Object): discovers tools from an instance with @copilotTool methods - fromClass(Class<?>): discovers tools from a class with static @copilotTool methods - Private getConfiguredMapper(): provides ObjectMapper matching JsonRpcClient config - Throws IllegalStateException with helpful message if generated class not found - Both methods annotated with @CopilotExperimental Includes comprehensive test suite (ToolDefinitionFromObjectTest) covering: - Basic discovery and schema verification - Handler invocation for String, void, and CompletableFuture returns - Argument coercion with primitives, String, boolean, and enums - Default value handling when arguments are omitted - Error case for missing generated class - java.time argument deserialization (validates JavaTimeModule contract) - Override tool flag propagation - ToolDefer.NONE → null mapping (defer absent from JSON output) Closes #1761 Co-authored-by: edburns <75821+edburns@users.noreply.github.com> * fix: replace misleading generated-file comment in test fixtures The $$CopilotToolMeta test fixtures are hand-written, not processor- generated. Update the header comment to say so accurately. Also fix Spotless formatting in CopilotToolProcessor.java. Addresses PR review comment about test Javadoc inaccuracy. * fix: introduce CopilotToolMetadataProvider interface to eliminate setAccessible Replace reflective Method.invoke + setAccessible(true) in ToolDefinition.loadDefinitions() with a typed interface cast. Generated $$CopilotToolMeta classes now implement CopilotToolMetadataProvider<T>, making them JPMS-safe and removing the InaccessibleObjectException risk. Addresses review comment r3468393716. * fix: validate fromClass() rejects instance @copilotTool methods fromClass() now scans for non-static @copilotTool methods and throws IllegalArgumentException with an actionable message listing the offending methods and directing users to fromObject() instead. Prevents hard-to-diagnose NullPointerException at invocation time. Addresses review comment r3468393764. * fix: use parsed JSON tree for defer-absence assertion Replace raw json.contains("defer") substring search with ObjectNode.has("defer") to avoid false positives if another field ever contains the substring. Addresses review comment r3468393829. --------- 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> * Give us this day our daily prompts * Add E2E integration test for ergonomic @copilotTool + ToolDefinition.fromObject() API (#1787) * Initial plan * Initial plan * Initial plan * Add E2E integration test for ergonomic @copilotTool + ToolDefinition.fromObject() API Create ErgonomicToolDefinitionIT that proves the ergonomic annotation-based API produces identical wire behavior to the low-level ToolDefinition.create() API, tested against the replay proxy. Files added: - test/snapshots/tools/ergonomic_tool_definition.yaml (identical to low_level_tool_definition.yaml since wire format is the same) - java/src/test/java/com/github/copilot/e2e/ErgonomicToolDefinitionIT.java - java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools.java - java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools$$CopilotToolMeta.java Closes #1762 * spotless * fix: use passed ObjectMapper for record-parameter conversion The single-record-parameter shortcut in CopilotToolProcessor generated invocation.getArgumentsAs() which uses an unconfigured ObjectMapper internally (no JavaTimeModule, no SDK settings). Switch to mapper.convertValue(args, RecordType.class) which uses the SDK-configured mapper passed to the definitions() method. Addresses review comment r3469523760. * fix: exclude Optional types from required list in generated schema CopilotToolProcessor.generateSchemaWithParamMetadata() now checks if a parameter type is Optional/OptionalInt/OptionalLong/OptionalDouble before adding it to the JSON Schema required list. This aligns with SchemaGenerator which already treats these types as optional. Addresses review comment r3469523801. * fix: correct misleading Javadoc in ToolDefinitionFromObjectTest The class-level Javadoc incorrectly stated that the annotation processor generates $$CopilotToolMeta fixtures during test compilation. In reality, the module has <proc>none</proc> and these fixtures are hand-written classes under com.github.copilot.rpc.fixtures. Addresses review comment r3469523833. * fix: remove unused grep override tool from E2E test The ErgonomicToolDefinitionIT snapshot only exercises set_current_phase and search_items. The grep tool (with overridesBuiltInTool=true) was never invoked, making it dead code that contradicted the PR description. Addresses review comment r3469523851. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Ed Burns <edburns@microsoft.com> * Give us this day our daily prompts * Remove before merge * Remove unused parameters flagged by CodeQL - CopilotToolProcessor.writeMetaClass: remove unused 'classElement' param - SchemaGenerator.isOptionalType: remove unused 'typeUtils' and 'elementUtils' - SchemaGenerator.unwrapOptional: remove unused 'elementUtils' - ErgonomicTestTools.searchItems: use 'keyword' param in return value * Update ergonomic_tool_definition snapshot to match searchItems output The searchItems tool now includes the keyword in its response, so update the replay proxy snapshot to expect the new format. * Generate qualified class name for static @copilotTool method calls For static methods, the processor now generates ClassName.method(...) instead of instance.method(...), making the generated code clearer and avoiding compiler warnings about accessing static members via instance references. Adds StaticTools fixture and fromClass_staticToolInvocation test. * Add JSON Schema format hints for all java.time types - LocalDateTime, Instant, ZonedDateTime → format: date-time - LocalDate → format: date - LocalTime → format: time These hints tell the LLM what string format to produce for date/time parameters. Previously only OffsetDateTime was mapped. Adds SchemaGeneratorTest cases for each new type mapping. * Fix Optional parameter extraction in generated tool code The processor now generates null-check + wrapping code for Optional, OptionalInt, OptionalLong, and OptionalDouble parameters instead of incorrectly calling mapper.convertValue(..., Optional.class). For Optional<T>, extracts the inner value using type-appropriate coercion then wraps with Optional.of()/Optional.empty(). For OptionalInt/Long/Double, uses the primitive Number extraction then wraps with the corresponding OptionalX.of()/empty(). Adds CopilotToolProcessorTest for generated code verification and ToolDefinitionFromObjectTest for end-to-end handler invocation with both present and absent optional values. * Fix Java tool-processor test generation and stabilize session-id test (#1799) * Fix Java tool-processor test generation and stabilize session-id test Address the Java test failures observed in the Java 17 surefire/failsafe run by fixing how annotation-processing output is discovered in CopilotToolProcessor tests and by hardening one timing-sensitive session test. Changes included: - CopilotToolProcessor: resolve @copilotTool elements via TypeElement lookup and reuse that element list through validation and generation passes, making annotation discovery robust across compiler/module contexts. - CopilotToolProcessorTest: force annotation processing in the in-memory compile harness (-proc:full, explicit processor), close the file manager with try-with-resources, and add a collecting forwarding file manager that captures generated source content from getJavaFileForOutput to avoid missing generated CopilotToolMeta classes in tests. - CopilotSessionTest#testShouldGetLastSessionId: add bounded retry for session creation (including timeout and execution-timeout-cause handling) to absorb transient startup delays while preserving failure behavior on persistent errors. Result: - CopilotToolProcessorTest now consistently observes generated CopilotToolMeta output and passes. - The full requested Maven workflow (jacoco prepare/report + surefire + failsafe under Java 17, with prior Java 25 compile) completes successfully. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Spotless * Avoid leaking session. The retry on session creation uses `future.get(timeout)` but does not cancel the in-flight `createSession` future when a timeout occurs. If attempt 1 eventually completes after attempt 2 starts, it can leave an orphaned session registered in the client (and potentially race `getLastSessionId` persistence), reintroducing flakiness and leaking resources. Capture the future and cancel it on timeout before retrying. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Add abort-session snapshot variant for interrupted tool calls Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * On branch edburns/1682-java-tool-ergonomics-review-draft-01 modified: java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java modified: java/src/main/java/com/github/copilot/tool/Param.java modified: java/src/main/java/com/github/copilot/tool/SchemaGenerator.java - Add `CopilotExperimental` more liberally * Reject optional primitive @Param without default Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix single-record tool schema and binding alignment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Preserve generic param types in generated tool binding Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add array parameter compile failure regression test Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Assert array parameters compile with TypeReference Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Reject mismatched numeric defaults for integral params Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * spotless --------- Signed-off-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> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 1f3e1d7 commit 783aa69

35 files changed

Lines changed: 4440 additions & 5 deletions

java/mvnw

100644100755
File mode changed.

java/src/main/java/com/github/copilot/rpc/ToolDefer.java

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,27 @@
2121
*/
2222
public enum ToolDefer {
2323

24+
/**
25+
* No deferral preference set. This is an <b>annotation-only sentinel</b> used
26+
* as the default for {@code @CopilotTool(defer = ToolDefer.NONE)}.
27+
* <p>
28+
* This constant must <b>not</b> be passed to {@link ToolDefinition} factory
29+
* methods. The annotation processor and {@code ToolDefinition.fromObject()}
30+
* must map {@code NONE} to a {@code null} field reference so that
31+
* {@code @JsonInclude(NON_NULL)} on {@link ToolDefinition} omits the
32+
* {@code defer} key from the JSON-RPC wire payload entirely (matching the
33+
* nullable/optional semantics used by all other SDKs).
34+
* <p>
35+
* As a secondary safety net, {@link #getValue()} returns {@code null} for this
36+
* constant. Note that this alone does <b>not</b> cause field omission: if a
37+
* non-null {@code NONE} reference reaches a {@link ToolDefinition} field,
38+
* Jackson's {@code @JsonInclude(NON_NULL)} will still emit the field (as
39+
* {@code "defer": null}) because the field reference itself is not null. The
40+
* primary protection is mapping {@code NONE} to a null field reference before
41+
* constructing the {@link ToolDefinition}.
42+
*/
43+
NONE(""),
44+
2445
/** The tool can be deferred and surfaced through tool search. */
2546
AUTO("auto"),
2647

@@ -35,12 +56,18 @@ public enum ToolDefer {
3556

3657
/**
3758
* Returns the JSON value for this deferral mode.
59+
* <p>
60+
* Returns {@code null} for {@link #NONE} to avoid emitting an empty string
61+
* ({@code "defer": ""}) if this sentinel accidentally reaches serialization.
62+
* With {@code null}, the worst-case leak becomes {@code "defer": null} rather
63+
* than an invalid empty string.
3864
*
39-
* @return the string value used in JSON serialization
65+
* @return the string value used in JSON serialization, or {@code null} for
66+
* {@link #NONE}
4067
*/
4168
@JsonValue
4269
public String getValue() {
43-
return value;
70+
return this == NONE ? null : value;
4471
}
4572

4673
/**

java/src/main/java/com/github/copilot/rpc/ToolDefinition.java

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,21 @@
44

55
package com.github.copilot.rpc;
66

7+
import java.lang.reflect.Method;
8+
import java.lang.reflect.Modifier;
9+
import java.util.Arrays;
10+
import java.util.List;
711
import java.util.Map;
12+
import java.util.stream.Collectors;
813

914
import com.fasterxml.jackson.annotation.JsonIgnore;
1015
import com.fasterxml.jackson.annotation.JsonInclude;
1116
import com.fasterxml.jackson.annotation.JsonProperty;
17+
import com.fasterxml.jackson.databind.DeserializationFeature;
18+
import com.fasterxml.jackson.databind.ObjectMapper;
19+
import com.fasterxml.jackson.databind.SerializationFeature;
20+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
21+
import com.github.copilot.CopilotExperimental;
1222

1323
/**
1424
* Defines a tool that can be invoked by the AI assistant.
@@ -163,4 +173,101 @@ public static ToolDefinition createWithDefer(String name, String description, Ma
163173
ToolHandler handler, ToolDefer defer) {
164174
return new ToolDefinition(name, description, schema, handler, null, null, defer);
165175
}
176+
177+
/**
178+
* Discovers tool definitions from an object whose methods are annotated with
179+
* {@code @CopilotTool}. Requires that the {@code CopilotToolProcessor}
180+
* annotation processor ran at compile time (generating the
181+
* {@code $$CopilotToolMeta} companion class).
182+
*
183+
* @param instance
184+
* the object containing {@code @CopilotTool}-annotated methods
185+
* @return list of tool definitions with working invocation handlers
186+
* @throws IllegalStateException
187+
* if the generated {@code $$CopilotToolMeta} class is not found
188+
* (annotation processor did not run)
189+
* @since 1.0.2
190+
*/
191+
@CopilotExperimental
192+
public static List<ToolDefinition> fromObject(Object instance) {
193+
if (instance == null) {
194+
throw new IllegalArgumentException("instance must not be null");
195+
}
196+
Class<?> clazz = instance.getClass();
197+
return loadDefinitions(clazz, instance);
198+
}
199+
200+
/**
201+
* Discovers tool definitions from a class with static
202+
* {@code @CopilotTool}-annotated methods. Requires that the
203+
* {@code CopilotToolProcessor} annotation processor ran at compile time
204+
* (generating the {@code $$CopilotToolMeta} companion class).
205+
*
206+
* @param clazz
207+
* the class containing static {@code @CopilotTool}-annotated methods
208+
* @return list of tool definitions with working invocation handlers
209+
* @throws IllegalStateException
210+
* if the generated {@code $$CopilotToolMeta} class is not found
211+
* (annotation processor did not run)
212+
* @since 1.0.2
213+
*/
214+
@CopilotExperimental
215+
public static List<ToolDefinition> fromClass(Class<?> clazz) {
216+
if (clazz == null) {
217+
throw new IllegalArgumentException("clazz must not be null");
218+
}
219+
List<String> instanceMethods = Arrays.stream(clazz.getDeclaredMethods())
220+
.filter(m -> m.isAnnotationPresent(com.github.copilot.tool.CopilotTool.class))
221+
.filter(m -> !Modifier.isStatic(m.getModifiers())).map(Method::getName).collect(Collectors.toList());
222+
if (!instanceMethods.isEmpty()) {
223+
throw new IllegalArgumentException(
224+
"fromClass() requires all @CopilotTool methods to be static, but found instance methods: "
225+
+ instanceMethods + ". Use fromObject(new " + clazz.getSimpleName() + "()) instead.");
226+
}
227+
return loadDefinitions(clazz, null);
228+
}
229+
230+
@SuppressWarnings("unchecked")
231+
private static List<ToolDefinition> loadDefinitions(Class<?> clazz, Object instance) {
232+
String metaClassName = clazz.getName() + "$$CopilotToolMeta";
233+
try {
234+
Class<?> metaClass = Class.forName(metaClassName, true, clazz.getClassLoader());
235+
var provider = (com.github.copilot.tool.CopilotToolMetadataProvider<Object>) metaClass
236+
.getDeclaredConstructor().newInstance();
237+
return provider.definitions(instance, getConfiguredMapper());
238+
} catch (ClassNotFoundException e) {
239+
throw new IllegalStateException("Generated class " + metaClassName + " not found. "
240+
+ "Ensure the CopilotToolProcessor annotation processor ran during compilation. "
241+
+ "Add the copilot-sdk-java dependency to your annotation processor path.", e);
242+
} catch (ReflectiveOperationException e) {
243+
throw new IllegalStateException("Failed to invoke " + metaClassName + ".definitions()", e);
244+
}
245+
}
246+
247+
/**
248+
* Returns the SDK-configured ObjectMapper for tool argument/result
249+
* serialization. Configuration mirrors
250+
* {@code JsonRpcClient.createObjectMapper()}.
251+
*/
252+
private static ObjectMapper getConfiguredMapper() {
253+
return ConfiguredMapperHolder.INSTANCE;
254+
}
255+
256+
/**
257+
* Lazy holder for the configured ObjectMapper (thread-safe, initialized on
258+
* first access).
259+
*/
260+
private static final class ConfiguredMapperHolder {
261+
static final ObjectMapper INSTANCE = createMapper();
262+
263+
private static ObjectMapper createMapper() {
264+
// Configuration must match JsonRpcClient.createObjectMapper()
265+
var mapper = new ObjectMapper();
266+
mapper.registerModule(new JavaTimeModule());
267+
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
268+
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
269+
mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
270+
return mapper;
271+
}
272+
}
166273
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.tool;
6+
7+
import java.lang.annotation.Documented;
8+
import java.lang.annotation.ElementType;
9+
import java.lang.annotation.Retention;
10+
import java.lang.annotation.RetentionPolicy;
11+
import java.lang.annotation.Target;
12+
13+
import com.github.copilot.CopilotExperimental;
14+
import com.github.copilot.rpc.ToolDefer;
15+
16+
/**
17+
* Marks a method as a Copilot tool. The annotated method will be exposed to the
18+
* model as a callable tool during a session.
19+
*
20+
* <p>
21+
* Example usage:
22+
*
23+
* <pre>
24+
* &#64;CopilotTool("Get weather for a location")
25+
* public CompletableFuture&lt;String&gt; getWeather(&#64;Param(value = "City name", required = true) String location) {
26+
* return CompletableFuture.completedFuture("Sunny in " + location);
27+
* }
28+
* </pre>
29+
*
30+
* @since 1.0.2
31+
*/
32+
@Documented
33+
@Retention(RetentionPolicy.RUNTIME)
34+
@Target(ElementType.METHOD)
35+
@CopilotExperimental
36+
public @interface CopilotTool {
37+
38+
/** Tool description (sent to the model). */
39+
String value();
40+
41+
/** Tool name. Defaults to method name converted to snake_case. */
42+
String name() default "";
43+
44+
/** Whether this tool overrides a built-in tool. */
45+
boolean overridesBuiltInTool() default false;
46+
47+
/** Whether to skip permission checks. */
48+
boolean skipPermission() default false;
49+
50+
/** Defer configuration for this tool. */
51+
ToolDefer defer() default ToolDefer.NONE;
52+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.tool;
6+
7+
import java.util.List;
8+
9+
import com.fasterxml.jackson.databind.ObjectMapper;
10+
import com.github.copilot.CopilotExperimental;
11+
import com.github.copilot.rpc.ToolDefinition;
12+
13+
/**
14+
* Contract for classes that provide {@link ToolDefinition} metadata for
15+
* {@code @CopilotTool}-annotated methods.
16+
*
17+
* <p>
18+
* The {@link CopilotToolProcessor} annotation processor generates an
19+
* implementation of this interface as a {@code $$CopilotToolMeta} companion
20+
* class. Users may also implement this interface directly for full manual
21+
* control over tool registration without using annotation processing.
22+
*
23+
* @param <T>
24+
* the tool class whose methods are described by this provider
25+
* @since 1.0.2
26+
*/
27+
@CopilotExperimental
28+
public interface CopilotToolMetadataProvider<T> {
29+
30+
/**
31+
* Returns tool definitions for the given instance.
32+
*
33+
* @param instance
34+
* the object containing tool methods, or {@code null} for static
35+
* methods
36+
* @param mapper
37+
* the SDK-configured {@link ObjectMapper} for argument
38+
* deserialization
39+
* @return list of tool definitions with working invocation handlers
40+
*/
41+
List<ToolDefinition> definitions(T instance, ObjectMapper mapper);
42+
}

0 commit comments

Comments
 (0)