Skip to content

Commit fdd4fad

Browse files
Copilotedburns
andcommitted
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>
1 parent f226734 commit fdd4fad

16 files changed

Lines changed: 774 additions & 2 deletions

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: 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+
}

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,14 @@ private void writeMetaClass(PrintWriter out, String packageName, String simpleCl
123123

124124
out.println("import com.github.copilot.rpc.ToolDefinition;");
125125
out.println("import com.github.copilot.rpc.ToolDefer;");
126+
out.println("import com.github.copilot.tool.CopilotToolMetadataProvider;");
126127
out.println("import com.fasterxml.jackson.databind.ObjectMapper;");
127128
out.println("import java.util.*;");
128129
out.println("import java.util.concurrent.CompletableFuture;");
129130
out.println();
130131

131-
out.println("final class " + metaClassName + " {");
132+
out.println("public final class " + metaClassName + " implements CopilotToolMetadataProvider<" + simpleClassName
133+
+ "> {");
132134
out.println();
133135

134136
// Helper method for adding description/default to schema maps
@@ -144,9 +146,10 @@ private void writeMetaClass(PrintWriter out, String packageName, String simpleCl
144146
}
145147

146148
// definitions method
149+
out.println(" @Override");
147150
out.println(" @SuppressWarnings({\"unchecked\", \"rawtypes\"})");
148151
out.println(
149-
" static List<ToolDefinition> definitions(" + simpleClassName + " instance, ObjectMapper mapper) {");
152+
" public List<ToolDefinition> definitions(" + simpleClassName + " instance, ObjectMapper mapper) {");
150153
out.println(" return List.of(");
151154

152155
for (int i = 0; i < methods.size(); i++) {

0 commit comments

Comments
 (0)