Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,12 @@ private String generateSchemaWithParamMetadata(List<? extends VariableElement> p
// Cast to Map<String, Object> via raw type for consistent Map.ofEntries typing
propertyEntries.add("Map.entry(\"" + paramName + "\", (Map<String, Object>)(Map) " + propertySchema + ")");

// Determine if required
if (paramAnnotation == null || paramAnnotation.required()) {
// Determine if required (Optional* types are never required)
boolean isOptionalType = paramType.getKind() == TypeKind.DECLARED && Set
.of("java.util.Optional", "java.util.OptionalInt", "java.util.OptionalLong",
"java.util.OptionalDouble")
.contains(((TypeElement) ((DeclaredType) paramType).asElement()).getQualifiedName().toString());
if (!isOptionalType && (paramAnnotation == null || paramAnnotation.required())) {
requiredNames.add("\"" + paramName + "\"");
}
}
Expand Down Expand Up @@ -284,7 +288,7 @@ private String generateLambdaBody(ExecutableElement method) {
String typeName = getTypeString(params.get(0).asType());
String paramName = params.get(0).getSimpleName().toString();
sb.append(" ").append(typeName).append(" ").append(paramName)
.append(" = invocation.getArgumentsAs(").append(typeName).append(".class);\n");
.append(" = mapper.convertValue(args, ").append(typeName).append(".class);\n");
} else {
for (VariableElement param : params) {
String paramName = getParamName(param);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Hand-written test fixture mimicking CopilotToolProcessor output.
package com.github.copilot.e2e;

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

import java.util.*;
import java.util.concurrent.CompletableFuture;

public final class ErgonomicTestTools$$CopilotToolMeta implements CopilotToolMetadataProvider<ErgonomicTestTools> {

private static Map<String, Object> withMeta(Map<String, Object> base, String description, Object defaultValue) {
var result = new LinkedHashMap<String, Object>(base);
if (description != null)
result.put("description", description);
if (defaultValue != null)
result.put("default", defaultValue);
return Collections.unmodifiableMap(result);
}

@Override
@SuppressWarnings({"unchecked", "rawtypes"})
public List<ToolDefinition> definitions(ErgonomicTestTools instance, ObjectMapper mapper) {
return List.of(new ToolDefinition("set_current_phase", "Sets the current phase of the agent",
Map.of("type", "object", "properties",
Map.ofEntries(Map.entry("phase",
(Map<String, Object>) (Map) withMeta(Map.of("type", "string"),
"The phase to transition to", null))),
"required", List.of("phase")),
invocation -> {
Map<String, Object> args = invocation.getArguments();
String phase = (String) args.get("phase");
return CompletableFuture.completedFuture(instance.setCurrentPhase(phase));
}, null, null, null),
new ToolDefinition(
"search_items", "Search for items by keyword", Map
.of("type", "object", "properties",
Map.ofEntries(Map.entry("keyword",
(Map<String, Object>) (Map) withMeta(Map.of("type", "string"),
"Search keyword", null))),
"required", List.of("keyword")),
invocation -> {
Map<String, Object> args = invocation.getArguments();
String keyword = (String) args.get("keyword");
return CompletableFuture.completedFuture(instance.searchItems(keyword));
}, null, null, null));
}
}
32 changes: 32 additions & 0 deletions java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

package com.github.copilot.e2e;

import com.github.copilot.tool.CopilotTool;
import com.github.copilot.tool.Param;

/**
* Tool fixture for the ergonomic {@code @CopilotTool} E2E integration test.
*
* <p>
* This class exercises the annotation-based tool definition API, producing
* identical wire-level tool schemas to the low-level
* {@code ToolDefinition.create()} API.
*/
class ErgonomicTestTools {

String currentPhase;

@CopilotTool("Sets the current phase of the agent")
public String setCurrentPhase(@Param("The phase to transition to") String phase) {
currentPhase = phase;
return "Phase set to " + phase;
}

@CopilotTool("Search for items by keyword")
public String searchItems(@Param("Search keyword") String keyword) {
return "Found: item_alpha, item_beta";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

package com.github.copilot.e2e;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.List;
import java.util.concurrent.TimeUnit;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import com.github.copilot.CopilotClient;
import com.github.copilot.CopilotSession;
import com.github.copilot.E2ETestContext;
import com.github.copilot.generated.AssistantMessageEvent;
import com.github.copilot.rpc.MessageOptions;
import com.github.copilot.rpc.PermissionHandler;
import com.github.copilot.rpc.SessionConfig;
import com.github.copilot.rpc.ToolDefinition;
import com.github.copilot.rpc.ToolSet;

/**
* Failsafe integration test for the ergonomic {@code @CopilotTool} +
* {@code ToolDefinition.fromObject()} API.
*
* <p>
* This test proves that the ergonomic annotation-based API produces identical
* wire behavior to the low-level {@code ToolDefinition.create()} API tested in
* {@code LowLevelToolDefinitionIT}.
*
* @see Snapshot: tools/ergonomic_tool_definition
*/
class ErgonomicToolDefinitionIT {

private static E2ETestContext ctx;

@BeforeAll
static void setup() throws Exception {
ctx = E2ETestContext.create();
}

@AfterAll
static void teardown() throws Exception {
if (ctx != null) {
ctx.close();
}
}

@Test
void ergonomicToolDefinition() throws Exception {
ctx.configureForTest("tools", "ergonomic_tool_definition");

ErgonomicTestTools tools = new ErgonomicTestTools();
List<ToolDefinition> toolDefs = ToolDefinition.fromObject(tools);

try (CopilotClient client = ctx.createClient()) {
CopilotSession session = client
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setAvailableTools(new ToolSet().addCustom("*").addBuiltIn("web_fetch")).setTools(toolDefs))
.get(30, TimeUnit.SECONDS);

try {
AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt(
"First, set the current phase to 'analyzing'. Then search for items with keyword 'copilot'. Report the phase and search results."),
60_000).get(90, TimeUnit.SECONDS);
Comment thread
edburns marked this conversation as resolved.

assertNotNull(response, "Expected a response from the assistant");
String content = response.getData().content().toLowerCase();
assertTrue(content.contains("analyzing"),
"Response should contain the updated phase: " + response.getData().content());
assertTrue(content.contains("item_alpha") || content.contains("item_beta"),
"Response should contain search results: " + response.getData().content());
assertTrue("analyzing".equals(tools.currentPhase),
"Expected currentPhase to be 'analyzing' but was: " + tools.currentPhase);
} finally {
session.close();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@
/**
* End-to-end tests for {@link ToolDefinition#fromObject(Object)}.
* <p>
* The annotation processor generates {@code $$CopilotToolMeta} companion
* classes for the fixture classes during test compilation.
* These tests use hand-written {@code $$CopilotToolMeta} companion classes
* under {@code com.github.copilot.rpc.fixtures} that mimic
* {@link com.github.copilot.tool.CopilotToolProcessor} output.
*/
@AllowCopilotExperimental
class ToolDefinitionFromObjectTest {
Expand Down
32 changes: 32 additions & 0 deletions test/snapshots/tools/ergonomic_tool_definition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
models:
- claude-sonnet-4.5
conversations:
- messages:
- role: system
content: ${system}
- role: user
content: First, set the current phase to 'analyzing'. Then search for items with keyword 'copilot'. Report the phase and
search results.
- role: assistant
content: I'll set the phase and run the search now.
tool_calls:
- id: toolcall_0
type: function
function:
name: set_current_phase
arguments: '{"phase":"analyzing"}'
- id: toolcall_1
type: function
function:
name: search_items
arguments: '{"keyword":"copilot"}'
- role: tool
tool_call_id: toolcall_0
content: Phase set to analyzing
- role: tool
tool_call_id: toolcall_1
content: "Found: item_alpha, item_beta"
- role: assistant
content: |-
Current phase: analyzing
Search results: item_alpha, item_beta
Loading