Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b6e5735
Resume 1682 iterating
edburns Jun 18, 2026
ea213a7
Phase 03 answer questions
edburns Jun 18, 2026
00da2b6
On branch edburns/1682-java-tool-ergonomics
edburns Jun 18, 2026
61e72d2
WIP: Phase 3. Question 3.4
edburns Jun 22, 2026
f7ace06
WIP: Phase 3. Question 3.6
edburns Jun 22, 2026
22958be
WIP: Phase 3. Question 3.6: Answer
edburns Jun 22, 2026
299cb36
Answer 3.7
edburns Jun 22, 2026
aa26d1e
Resolve 3.8
edburns Jun 22, 2026
767a29b
Initial plan
Copilot Jun 22, 2026
0460014
feat(java): create @CopilotTool and @Param annotations with tests
Copilot Jun 22, 2026
fb93b03
spotless
edburns Jun 22, 2026
a5a46ad
fix(java): make ToolDefer.NONE serialize as null to prevent wire leak
edburns Jun 22, 2026
bfa3d0f
WIP Phase 4.1
edburns Jun 22, 2026
66e2277
feat(java): create @CopilotTool and @Param annotations (#1763)
Copilot Jun 23, 2026
106fc3d
Initial plan
Copilot Jun 22, 2026
f9da46f
feat(java): add SchemaGenerator compile-time type-to-JSON-Schema util…
Copilot Jun 23, 2026
a70f31d
WIP 4.3
edburns Jun 23, 2026
d8b0b57
feat(java): Add CopilotToolProcessor annotation processor (task 4.3) …
Copilot Jun 24, 2026
f226734
Give us this day our daily prompts
edburns Jun 24, 2026
fdd4fad
feat(java): Add ToolDefinition.fromObject() and fromClass() registrat…
Copilot Jun 24, 2026
9ea87e4
Give us this day our daily prompts
edburns Jun 24, 2026
c293514
Add E2E integration test for ergonomic @CopilotTool + ToolDefinition.…
Copilot Jun 24, 2026
c47a0fb
Give us this day our daily prompts
edburns Jun 24, 2026
848bea0
Remove before merge
edburns Jun 24, 2026
84d116d
Remove unused parameters flagged by CodeQL
edburns Jun 24, 2026
2a257ed
Update ergonomic_tool_definition snapshot to match searchItems output
edburns Jun 24, 2026
44e52e1
Generate qualified class name for static @CopilotTool method calls
edburns Jun 24, 2026
f193b41
Add JSON Schema format hints for all java.time types
edburns Jun 24, 2026
c1c5b34
Fix Optional parameter extraction in generated tool code
edburns Jun 24, 2026
60eb094
Fix Java tool-processor test generation and stabilize session-id test…
edburns Jun 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file modified java/mvnw
100644 → 100755
Empty file.
31 changes: 29 additions & 2 deletions java/src/main/java/com/github/copilot/rpc/ToolDefer.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,27 @@
*/
public enum ToolDefer {

/**
* No deferral preference set. This is an <b>annotation-only sentinel</b> used
* as the default for {@code @CopilotTool(defer = ToolDefer.NONE)}.
* <p>
* This constant must <b>not</b> be passed to {@link ToolDefinition} factory
* methods. The annotation processor and {@code ToolDefinition.fromObject()}
* must map {@code NONE} to a {@code null} field reference so that
* {@code @JsonInclude(NON_NULL)} on {@link ToolDefinition} omits the
* {@code defer} key from the JSON-RPC wire payload entirely (matching the
* nullable/optional semantics used by all other SDKs).
* <p>
* As a secondary safety net, {@link #getValue()} returns {@code null} for this
* constant. Note that this alone does <b>not</b> cause field omission: if a
* non-null {@code NONE} reference reaches a {@link ToolDefinition} field,
* Jackson's {@code @JsonInclude(NON_NULL)} will still emit the field (as
* {@code "defer": null}) because the field reference itself is not null. The
* primary protection is mapping {@code NONE} to a null field reference before
* constructing the {@link ToolDefinition}.
*/
NONE(""),

/** The tool can be deferred and surfaced through tool search. */
AUTO("auto"),

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

/**
* Returns the JSON value for this deferral mode.
* <p>
* Returns {@code null} for {@link #NONE} to avoid emitting an empty string
* ({@code "defer": ""}) if this sentinel accidentally reaches serialization.
* With {@code null}, the worst-case leak becomes {@code "defer": null} rather
* than an invalid empty string.
*
* @return the string value used in JSON serialization
* @return the string value used in JSON serialization, or {@code null} for
* {@link #NONE}
*/
@JsonValue
public String getValue() {
return value;
return this == NONE ? null : value;
}

/**
Expand Down
107 changes: 107 additions & 0 deletions java/src/main/java/com/github/copilot/rpc/ToolDefinition.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,21 @@

package com.github.copilot.rpc;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.github.copilot.CopilotExperimental;

/**
* Defines a tool that can be invoked by the AI assistant.
Expand Down Expand Up @@ -163,4 +173,101 @@ public static ToolDefinition createWithDefer(String name, String description, Ma
ToolHandler handler, ToolDefer defer) {
return new ToolDefinition(name, description, schema, handler, null, null, defer);
}

/**
* Discovers tool definitions from an object whose methods are annotated with
* {@code @CopilotTool}. Requires that the {@code CopilotToolProcessor}
* annotation processor ran at compile time (generating the
* {@code $$CopilotToolMeta} companion class).
*
* @param instance
* the object containing {@code @CopilotTool}-annotated methods
* @return list of tool definitions with working invocation handlers
* @throws IllegalStateException
* if the generated {@code $$CopilotToolMeta} class is not found
* (annotation processor did not run)
* @since 1.0.2
*/
@CopilotExperimental
public static List<ToolDefinition> fromObject(Object instance) {
if (instance == null) {
throw new IllegalArgumentException("instance must not be null");
}
Class<?> clazz = instance.getClass();
return loadDefinitions(clazz, instance);
}

/**
* Discovers tool definitions from a class with static
* {@code @CopilotTool}-annotated methods. Requires that the
* {@code CopilotToolProcessor} annotation processor ran at compile time
* (generating the {@code $$CopilotToolMeta} companion class).
*
* @param clazz
* the class containing static {@code @CopilotTool}-annotated methods
* @return list of tool definitions with working invocation handlers
* @throws IllegalStateException
* if the generated {@code $$CopilotToolMeta} class is not found
* (annotation processor did not run)
* @since 1.0.2
*/
@CopilotExperimental
public static List<ToolDefinition> fromClass(Class<?> clazz) {
if (clazz == null) {
throw new IllegalArgumentException("clazz must not be null");
}
List<String> instanceMethods = Arrays.stream(clazz.getDeclaredMethods())
.filter(m -> m.isAnnotationPresent(com.github.copilot.tool.CopilotTool.class))
.filter(m -> !Modifier.isStatic(m.getModifiers())).map(Method::getName).collect(Collectors.toList());
if (!instanceMethods.isEmpty()) {
throw new IllegalArgumentException(
"fromClass() requires all @CopilotTool methods to be static, but found instance methods: "
+ instanceMethods + ". Use fromObject(new " + clazz.getSimpleName() + "()) instead.");
}
return loadDefinitions(clazz, null);
}

@SuppressWarnings("unchecked")
private static List<ToolDefinition> loadDefinitions(Class<?> clazz, Object instance) {
String metaClassName = clazz.getName() + "$$CopilotToolMeta";
try {
Class<?> metaClass = Class.forName(metaClassName, true, clazz.getClassLoader());
var provider = (com.github.copilot.tool.CopilotToolMetadataProvider<Object>) metaClass
.getDeclaredConstructor().newInstance();
return provider.definitions(instance, getConfiguredMapper());
} catch (ClassNotFoundException e) {
throw new IllegalStateException("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 (ReflectiveOperationException e) {
throw new IllegalStateException("Failed to invoke " + metaClassName + ".definitions()", e);
}
}

/**
* Returns the SDK-configured ObjectMapper for tool argument/result
* serialization. Configuration mirrors
* {@code JsonRpcClient.createObjectMapper()}.
*/
private static ObjectMapper getConfiguredMapper() {
return ConfiguredMapperHolder.INSTANCE;
}

/**
* Lazy holder for the configured ObjectMapper (thread-safe, initialized on
* first access).
*/
private static final class ConfiguredMapperHolder {
static final ObjectMapper INSTANCE = createMapper();

private static ObjectMapper createMapper() {
// Configuration must match JsonRpcClient.createObjectMapper()
var mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL);
return mapper;
}
}
}
52 changes: 52 additions & 0 deletions java/src/main/java/com/github/copilot/tool/CopilotTool.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

package com.github.copilot.tool;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import com.github.copilot.CopilotExperimental;
import com.github.copilot.rpc.ToolDefer;

/**
* Marks a method as a Copilot tool. The annotated method will be exposed to the
* model as a callable tool during a session.
*
* <p>
* Example usage:
*
* <pre>
* &#64;CopilotTool("Get weather for a location")
* public CompletableFuture&lt;String&gt; getWeather(&#64;Param(value = "City name", required = true) String location) {
* return CompletableFuture.completedFuture("Sunny in " + location);
* }
* </pre>
*
* @since 1.0.2
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@CopilotExperimental
public @interface CopilotTool {

/** Tool description (sent to the model). */
String value();

/** Tool name. Defaults to method name converted to snake_case. */
String name() default "";

/** Whether this tool overrides a built-in tool. */
boolean overridesBuiltInTool() default false;

/** Whether to skip permission checks. */
boolean skipPermission() default false;

/** Defer configuration for this tool. */
ToolDefer defer() default ToolDefer.NONE;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

package com.github.copilot.tool;

import java.util.List;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.copilot.CopilotExperimental;
import com.github.copilot.rpc.ToolDefinition;

/**
* Contract for classes that provide {@link ToolDefinition} metadata for
* {@code @CopilotTool}-annotated methods.
*
* <p>
* The {@link CopilotToolProcessor} annotation processor generates an
* implementation of this interface as a {@code $$CopilotToolMeta} companion
* class. Users may also implement this interface directly for full manual
* control over tool registration without using annotation processing.
*
* @param <T>
* the tool class whose methods are described by this provider
* @since 1.0.2
*/
@CopilotExperimental
public interface CopilotToolMetadataProvider<T> {

/**
* Returns tool definitions for the given instance.
*
* @param instance
* the object containing tool methods, or {@code null} for static
* methods
* @param mapper
* the SDK-configured {@link ObjectMapper} for argument
* deserialization
* @return list of tool definitions with working invocation handlers
*/
List<ToolDefinition> definitions(T instance, ObjectMapper mapper);
}
Loading
Loading