You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add static methods ToolDefinition.fromObject(Object) and ToolDefinition.fromClass(Class<?>) that load processor-generated $$CopilotToolMeta classes and return List<ToolDefinition> with fully working tool definitions (schema + invocation handlers).
Branch:edburns/1682-java-tool-ergonomics on upstream (⚠️ NOT main — PRs must target this branch)
Prerequisites
Tasks 4.1 (annotations), 4.2 (SchemaGenerator), and 4.3 (CopilotToolProcessor) 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.6 — ToolDefinition.fromObject(Object) registration API (Resolution: processor-only approach, no reflection fallback)
Section 3.7 — module-info.java impact (Resolution: Class.forName works within same module/classloader)
Section 4.4 — ToolDefinition.fromObject(Object) (the primary task description)
Deliverables
Files to modify
java/src/main/java/com/github/copilot/rpc/ToolDefinition.java — add fromObject(Object) and fromClass(Class<?>) static methods.
⚠️ Critical: ObjectMapper contract with generated code
The generated $$CopilotToolMeta.definitions() method accepts an ObjectMapper as its second parameter. This is an internal contract established in PR #1777 (task 4.3, CopilotToolProcessor).
Why: The generated code uses the ObjectMapper for:
Argument coercion — converting complex types (enums, records, POJOs, java.time.*) from the args Map
Result serialization — converting non-String return values to JSON
The mapper must be configured identically to JsonRpcClient.createObjectMapper():
How to obtain the configured mapper (without exposing it as public API):fromObject() is in com.github.copilot.rpc.ToolDefinition. The canonical mapper config lives in com.github.copilot.JsonRpcClient.createObjectMapper() (package-private class). Options for bridging:
Add a package-private static ObjectMapper field on ToolDefinition that is initialized by RpcHandlerDispatcher (in com.github.copilot) at startup via a package-private setter or static initializer block.
Or replicate the 4-line configuration inline in ToolDefinition with a comment linking to JsonRpcClient.createObjectMapper() as the canonical source.
The choice is left to the implementer, but the mapper must not be exposed as a public API return value.
Implementation specification
/** * Discovers tool definitions from an object whose methods are annotated with @CopilotTool. * Requires that the CopilotToolProcessor annotation processor ran at compile time * (generating the $$CopilotToolMeta companion class). * * @param instance the object containing @CopilotTool-annotated methods * @return list of tool definitions with working invocation handlers * @throws IllegalStateException if the generated $$CopilotToolMeta class is not found * (annotation processor did not run) */@CopilotExperimentalpublicstaticList<ToolDefinition> fromObject(Objectinstance) {
Class<?> clazz = instance.getClass();
StringmetaClassName = clazz.getName() + "$$CopilotToolMeta";
try {
Class<?> metaClass = Class.forName(metaClassName, true, clazz.getClassLoader());
Methoddefs = metaClass.getDeclaredMethod("definitions", clazz, ObjectMapper.class);
defs.setAccessible(true);
@SuppressWarnings("unchecked")
List<ToolDefinition> result = (List<ToolDefinition>) defs.invoke(null, instance, getConfiguredMapper());
returnresult;
} catch (ClassNotFoundExceptione) {
thrownewIllegalStateException(
"Generated class " + metaClassName + " not found. "
+ "Ensure the CopilotToolProcessor annotation processor ran during compilation. "
+ "Add the copilot-sdk-java dependency to your annotation processor path.",
e);
} catch (ReflectiveOperationExceptione) {
thrownewIllegalStateException("Failed to invoke " + metaClassName + ".definitions()", e);
}
}
/** * Returns the SDK-configured ObjectMapper for tool argument/result serialization. * Package-private — not exposed as public API. */privatestaticObjectMappergetConfiguredMapper() {
// Implementation options (choose one):// Option A: Static field initialized by RpcHandlerDispatcher at startup// Option B: Inline configuration matching JsonRpcClient.createObjectMapper()// The mapper MUST have JavaTimeModule, NON_NULL, lenient unknown-properties.
}
/** * Discovers tool definitions from a class with static @CopilotTool methods. * Requires that the CopilotToolProcessor annotation processor ran at compile time. */@CopilotExperimentalpublicstaticList<ToolDefinition> fromClass(Class<?> clazz) {
// For static tool methods — generates with null instance// Implementation detail: the generated $$CopilotToolMeta has a definitions(null) overload// or the method handles null instance for static methods.
...
}
Key design decisions (from Resolution 3.6)
No reflection fallback. If $$CopilotToolMeta is not found, throw IllegalStateException with a helpful message. Do NOT fall back to runtime reflection scanning.
Use clazz.getClassLoader() in Class.forName() to handle named JPMS modules correctly.
The generated definitions() method is package-private, so use setAccessible(true) since ToolDefinition is in a different package (com.github.copilot.rpc) than the generated class.
All of the following must pass before this task is considered complete:
End-to-end unit test: Create java/src/test/java/com/github/copilot/rpc/ToolDefinitionFromObjectTest.java that:
Defines a test class with @CopilotTool methods (the annotation processor will generate $$CopilotToolMeta during test compilation).
Calls ToolDefinition.fromObject(instance) and verifies the returned list has the correct number of tools.
Verifies each tool's name, description, and parameter schema.
Invokes each tool's handler with test arguments and verifies the correct method was called with correct arguments and the return value is correct.
Handler invocation tests: For each return type pattern (String, void, CompletableFuture), verify the handler produces the expected result:
String method → handler returns the string value.
void method → handler returns "Success".
CompletableFuture<String> method → handler returns the async result.
Argument coercion end-to-end test: Define a tool method with parameters of type String, int, boolean, and an enum. Call fromObject(), then invoke the handler with a Map<String, Object> containing appropriate values. Verify the method receives correctly typed arguments.
Default value test: Define a tool method with @Param(defaultValue="42") int count. Invoke the handler with arguments that OMIT the count key. Verify the method receives 42.
Error case test: Verify that calling fromObject() on an object whose class was NOT processed (no $$CopilotToolMeta exists) throws IllegalStateException with a message mentioning the annotation processor.
java.time argument test: Define a tool method with a java.time.LocalDateTime parameter. Call fromObject(), invoke the handler with an ISO-8601 string argument. Verify the method receives a correctly parsed LocalDateTime. This validates that the ObjectMapper contract (with JavaTimeModule) is working end-to-end.
Override tool test: Define a method with @CopilotTool(value="...", name="grep", overridesBuiltInTool=true). Verify fromObject() returns a ToolDefinition that is properly configured as an override.
Spotless format check:mvn spotless:check passes.
Full test suite:mvn clean verify passes (existing tests and tasks 4.1–4.3 tests not broken).
⚠️ Critical: ToolDefer.NONE → null mapping
When the @CopilotTool annotation has defer = ToolDefer.NONE (the default), fromObject() must pass null for the defer parameter to ToolDefinition.create() — notToolDefer.NONE itself. A non-null NONE reference would leak "defer": null onto the JSON-RPC wire payload because Jackson's @JsonInclude(NON_NULL) checks the field reference, not the @JsonValue return. See the Javadoc on ToolDefer.NONE for the full explanation.
Add a gating test: call fromObject() on a class with @CopilotTool that does NOT set defer (uses the default NONE), serialize the resulting ToolDefinition to JSON, and assert the "defer" key is absent from the output.
Constraints
✅✅ YOU MUST run mvn spotless:apply before every commit.
Do NOT implement a reflection fallback. Throw on missing generated class.
Do NOT modify any files outside the java/ directory.
The fromObject() method must be annotated with @CopilotExperimental.
Follow existing code style (4-space indent, Javadoc on public APIs).
Do NOT expose ObjectMapper as a public API return type. The mapper is an internal implementation detail passed through the definitions() contract.
Overview
Add static methods
ToolDefinition.fromObject(Object)andToolDefinition.fromClass(Class<?>)that load processor-generated$$CopilotToolMetaclasses and returnList<ToolDefinition>with fully working tool definitions (schema + invocation handlers).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
ToolDefinition.fromObject(Object)registration API (Resolution: processor-only approach, no reflection fallback)module-info.javaimpact (Resolution: Class.forName works within same module/classloader)ToolDefinition.fromObject(Object)(the primary task description)Deliverables
Files to modify
java/src/main/java/com/github/copilot/rpc/ToolDefinition.java— addfromObject(Object)andfromClass(Class<?>)static methods.The generated
$$CopilotToolMeta.definitions()method accepts anObjectMapperas its second parameter. This is an internal contract established in PR #1777 (task 4.3,CopilotToolProcessor).Why: The generated code uses the ObjectMapper for:
java.time.*) from the args MapThe mapper must be configured identically to
JsonRpcClient.createObjectMapper():JavaTimeModuleregisteredDeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES = falseSerializationFeature.WRITE_DATES_AS_TIMESTAMPS = falseJsonInclude.Include.NON_NULLGenerated method signature (internal contract):
fromObject()must pass a properly configured mapper when reflectively invokingdefinitions():How to obtain the configured mapper (without exposing it as public API):
fromObject()is incom.github.copilot.rpc.ToolDefinition. The canonical mapper config lives incom.github.copilot.JsonRpcClient.createObjectMapper()(package-private class). Options for bridging:ObjectMapperfield onToolDefinitionthat is initialized byRpcHandlerDispatcher(incom.github.copilot) at startup via a package-private setter or static initializer block.ToolDefinitionwith a comment linking toJsonRpcClient.createObjectMapper()as the canonical source.Implementation specification
Key design decisions (from Resolution 3.6)
$$CopilotToolMetais not found, throwIllegalStateExceptionwith a helpful message. Do NOT fall back to runtime reflection scanning.clazz.getClassLoader()inClass.forName()to handle named JPMS modules correctly.definitions()method is package-private, so usesetAccessible(true)sinceToolDefinitionis in a different package (com.github.copilot.rpc) than the generated class.definitions()method takes TWO parameters: the instance AND anObjectMapper. This is the internal contract from PR feat(java): Add CopilotToolProcessor annotation processor (task 4.3) #1777.Gating tests and criteria
All of the following must pass before this task is considered complete:
End-to-end unit test: Create
java/src/test/java/com/github/copilot/rpc/ToolDefinitionFromObjectTest.javathat:@CopilotToolmethods (the annotation processor will generate$$CopilotToolMetaduring test compilation).ToolDefinition.fromObject(instance)and verifies the returned list has the correct number of tools.Handler invocation tests: For each return type pattern (String, void, CompletableFuture), verify the handler produces the expected result:
Stringmethod → handler returns the string value.voidmethod → handler returns "Success".CompletableFuture<String>method → handler returns the async result.Argument coercion end-to-end test: Define a tool method with parameters of type
String,int,boolean, and an enum. CallfromObject(), then invoke the handler with aMap<String, Object>containing appropriate values. Verify the method receives correctly typed arguments.Default value test: Define a tool method with
@Param(defaultValue="42") int count. Invoke the handler with arguments that OMIT thecountkey. Verify the method receives42.Error case test: Verify that calling
fromObject()on an object whose class was NOT processed (no$$CopilotToolMetaexists) throwsIllegalStateExceptionwith a message mentioning the annotation processor.java.time argument test: Define a tool method with a
java.time.LocalDateTimeparameter. CallfromObject(), invoke the handler with an ISO-8601 string argument. Verify the method receives a correctly parsedLocalDateTime. This validates that the ObjectMapper contract (withJavaTimeModule) is working end-to-end.Override tool test: Define a method with
@CopilotTool(value="...", name="grep", overridesBuiltInTool=true). VerifyfromObject()returns aToolDefinitionthat is properly configured as an override.Spotless format check:
mvn spotless:checkpasses.Full test suite:
mvn clean verifypasses (existing tests and tasks 4.1–4.3 tests not broken).ToolDefer.NONE→nullmappingWhen the
@CopilotToolannotation hasdefer = ToolDefer.NONE(the default),fromObject()must passnullfor the defer parameter toToolDefinition.create()— notToolDefer.NONEitself. A non-nullNONEreference would leak"defer": nullonto the JSON-RPC wire payload because Jackson's@JsonInclude(NON_NULL)checks the field reference, not the@JsonValuereturn. See the Javadoc onToolDefer.NONEfor the full explanation.Add a gating test: call
fromObject()on a class with@CopilotToolthat does NOT setdefer(uses the defaultNONE), serialize the resultingToolDefinitionto JSON, and assert the"defer"key is absent from the output.Constraints
✅✅ YOU MUST run
mvn spotless:applybefore every commit.Do NOT implement a reflection fallback. Throw on missing generated class.
Do NOT modify any files outside the
java/directory.The
fromObject()method must be annotated with@CopilotExperimental.Follow existing code style (4-space indent, Javadoc on public APIs).
Do NOT expose
ObjectMapperas a public API return type. The mapper is an internal implementation detail passed through thedefinitions()contract.