From f1c08972a85934e8c941f99bba3e8122a672f6a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:18:28 +0000 Subject: [PATCH 1/9] Initial plan From aeb9f24e99f312e68351ed0ea309ae181e0ce5a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:18:28 +0000 Subject: [PATCH 2/9] Initial plan From ccb2a338c3dc93728cac6b0fe7ae728f9601e3dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 18:06:38 +0000 Subject: [PATCH 3/9] Initial plan From 99069864a69654d79e524df9645a04c788d781a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 24 Jun 2026 18:14:42 +0000 Subject: [PATCH 4/9] 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 github/copilot-sdk#1762 --- .../ErgonomicTestTools$$CopilotToolMeta.java | 60 +++++++++++++ .../copilot/e2e/ErgonomicTestTools.java | 37 ++++++++ .../e2e/ErgonomicToolDefinitionIT.java | 86 +++++++++++++++++++ .../tools/ergonomic_tool_definition.yaml | 32 +++++++ 4 files changed, 215 insertions(+) create mode 100644 java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools$$CopilotToolMeta.java create mode 100644 java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools.java create mode 100644 java/src/test/java/com/github/copilot/e2e/ErgonomicToolDefinitionIT.java create mode 100644 test/snapshots/tools/ergonomic_tool_definition.yaml diff --git a/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools$$CopilotToolMeta.java b/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools$$CopilotToolMeta.java new file mode 100644 index 0000000000..4fc9609fb4 --- /dev/null +++ b/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools$$CopilotToolMeta.java @@ -0,0 +1,60 @@ +// 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 { + + private static Map withMeta(Map base, String description, Object defaultValue) { + var result = new LinkedHashMap(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 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) (Map) withMeta(Map.of("type", "string"), + "The phase to transition to", null))), + "required", List.of("phase")), + invocation -> { + Map 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) (Map) withMeta(Map.of("type", "string"), + "Search keyword", null))), + "required", List.of("keyword")), + invocation -> { + Map args = invocation.getArguments(); + String keyword = (String) args.get("keyword"); + return CompletableFuture.completedFuture(instance.searchItems(keyword)); + }, null, null, null), + new ToolDefinition("grep", "Custom grep override", + Map.of("type", "object", "properties", + Map.ofEntries(Map.entry("query", + (Map) (Map) withMeta(Map.of("type", "string"), + "Search query", null))), + "required", List.of("query")), + invocation -> { + Map args = invocation.getArguments(); + String query = (String) args.get("query"); + return CompletableFuture.completedFuture(instance.grepOverride(query)); + }, Boolean.TRUE, null, null)); + } +} diff --git a/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools.java b/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools.java new file mode 100644 index 0000000000..5e52633ef0 --- /dev/null +++ b/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools.java @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * 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. + * + *

+ * 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"; + } + + @CopilotTool(value = "Custom grep override", name = "grep", overridesBuiltInTool = true) + public String grepOverride(@Param("Search query") String query) { + return "CUSTOM_GREP: " + query; + } +} diff --git a/java/src/test/java/com/github/copilot/e2e/ErgonomicToolDefinitionIT.java b/java/src/test/java/com/github/copilot/e2e/ErgonomicToolDefinitionIT.java new file mode 100644 index 0000000000..705005193a --- /dev/null +++ b/java/src/test/java/com/github/copilot/e2e/ErgonomicToolDefinitionIT.java @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * 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. + * + *

+ * 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 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); + + 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(); + } + } + } +} diff --git a/test/snapshots/tools/ergonomic_tool_definition.yaml b/test/snapshots/tools/ergonomic_tool_definition.yaml new file mode 100644 index 0000000000..03cb0748a2 --- /dev/null +++ b/test/snapshots/tools/ergonomic_tool_definition.yaml @@ -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 From 141b5acd296ec97c76f5112d0683ca392e0e8034 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Wed, 24 Jun 2026 14:35:06 -0400 Subject: [PATCH 5/9] spotless --- .../ErgonomicTestTools$$CopilotToolMeta.java | 42 +++++++++---------- .../e2e/ErgonomicToolDefinitionIT.java | 3 +- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools$$CopilotToolMeta.java b/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools$$CopilotToolMeta.java index 4fc9609fb4..45abcd7311 100644 --- a/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools$$CopilotToolMeta.java +++ b/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools$$CopilotToolMeta.java @@ -22,34 +22,32 @@ private static Map withMeta(Map base, String des @Override @SuppressWarnings({"unchecked", "rawtypes"}) public List 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) (Map) withMeta(Map.of("type", "string"), - "The phase to transition to", null))), - "required", List.of("phase")), - invocation -> { - Map 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) (Map) withMeta(Map.of("type", "string"), - "Search keyword", null))), - "required", List.of("keyword")), + 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) (Map) withMeta(Map.of("type", "string"), + "The phase to transition to", null))), + "required", List.of("phase")), + invocation -> { + Map 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) (Map) withMeta(Map.of("type", "string"), + "Search keyword", null))), + "required", List.of("keyword")), invocation -> { Map args = invocation.getArguments(); String keyword = (String) args.get("keyword"); return CompletableFuture.completedFuture(instance.searchItems(keyword)); }, null, null, null), new ToolDefinition("grep", "Custom grep override", - Map.of("type", "object", "properties", - Map.ofEntries(Map.entry("query", - (Map) (Map) withMeta(Map.of("type", "string"), - "Search query", null))), + Map.of("type", "object", "properties", Map.ofEntries(Map.entry("query", + (Map) (Map) withMeta(Map.of("type", "string"), "Search query", null))), "required", List.of("query")), invocation -> { Map args = invocation.getArguments(); diff --git a/java/src/test/java/com/github/copilot/e2e/ErgonomicToolDefinitionIT.java b/java/src/test/java/com/github/copilot/e2e/ErgonomicToolDefinitionIT.java index 705005193a..c74e945444 100644 --- a/java/src/test/java/com/github/copilot/e2e/ErgonomicToolDefinitionIT.java +++ b/java/src/test/java/com/github/copilot/e2e/ErgonomicToolDefinitionIT.java @@ -61,8 +61,7 @@ void ergonomicToolDefinition() throws Exception { try (CopilotClient client = ctx.createClient()) { CopilotSession session = client .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) - .setAvailableTools(new ToolSet().addCustom("*").addBuiltIn("web_fetch")) - .setTools(toolDefs)) + .setAvailableTools(new ToolSet().addCustom("*").addBuiltIn("web_fetch")).setTools(toolDefs)) .get(30, TimeUnit.SECONDS); try { From 1a919f2acc84ce46643339df0a0df1fa39deef6e Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Wed, 24 Jun 2026 15:01:50 -0400 Subject: [PATCH 6/9] 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. --- .../main/java/com/github/copilot/tool/CopilotToolProcessor.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java b/java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java index 098c9faa91..48fca6284c 100644 --- a/java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java +++ b/java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java @@ -284,7 +284,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); From bdc3c698fecc7a7ce7759596e940d103b517ea0d Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Wed, 24 Jun 2026 15:05:32 -0400 Subject: [PATCH 7/9] 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. --- .../com/github/copilot/tool/CopilotToolProcessor.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java b/java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java index 48fca6284c..4821c97869 100644 --- a/java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java +++ b/java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java @@ -237,8 +237,12 @@ private String generateSchemaWithParamMetadata(List p // Cast to Map via raw type for consistent Map.ofEntries typing propertyEntries.add("Map.entry(\"" + paramName + "\", (Map)(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 + "\""); } } From a277021bd6ed011170cc4cd4ebd12b6ef5756e91 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Wed, 24 Jun 2026 16:06:05 -0400 Subject: [PATCH 8/9] 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 none and these fixtures are hand-written classes under com.github.copilot.rpc.fixtures. Addresses review comment r3469523833. --- .../com/github/copilot/rpc/ToolDefinitionFromObjectTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/java/src/test/java/com/github/copilot/rpc/ToolDefinitionFromObjectTest.java b/java/src/test/java/com/github/copilot/rpc/ToolDefinitionFromObjectTest.java index bea1aad194..74d223e632 100644 --- a/java/src/test/java/com/github/copilot/rpc/ToolDefinitionFromObjectTest.java +++ b/java/src/test/java/com/github/copilot/rpc/ToolDefinitionFromObjectTest.java @@ -34,8 +34,9 @@ /** * End-to-end tests for {@link ToolDefinition#fromObject(Object)}. *

- * 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 { From a423f7cf523e5d55f5edbddb11adb85065786b11 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Wed, 24 Jun 2026 16:15:41 -0400 Subject: [PATCH 9/9] 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. --- .../e2e/ErgonomicTestTools$$CopilotToolMeta.java | 11 +---------- .../com/github/copilot/e2e/ErgonomicTestTools.java | 5 ----- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools$$CopilotToolMeta.java b/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools$$CopilotToolMeta.java index 45abcd7311..703a6b0102 100644 --- a/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools$$CopilotToolMeta.java +++ b/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools$$CopilotToolMeta.java @@ -44,15 +44,6 @@ public List definitions(ErgonomicTestTools instance, ObjectMappe Map args = invocation.getArguments(); String keyword = (String) args.get("keyword"); return CompletableFuture.completedFuture(instance.searchItems(keyword)); - }, null, null, null), - new ToolDefinition("grep", "Custom grep override", - Map.of("type", "object", "properties", Map.ofEntries(Map.entry("query", - (Map) (Map) withMeta(Map.of("type", "string"), "Search query", null))), - "required", List.of("query")), - invocation -> { - Map args = invocation.getArguments(); - String query = (String) args.get("query"); - return CompletableFuture.completedFuture(instance.grepOverride(query)); - }, Boolean.TRUE, null, null)); + }, null, null, null)); } } diff --git a/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools.java b/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools.java index 5e52633ef0..1b5abba9fa 100644 --- a/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools.java +++ b/java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools.java @@ -29,9 +29,4 @@ public String setCurrentPhase(@Param("The phase to transition to") String phase) public String searchItems(@Param("Search keyword") String keyword) { return "Found: item_alpha, item_beta"; } - - @CopilotTool(value = "Custom grep override", name = "grep", overridesBuiltInTool = true) - public String grepOverride(@Param("Search query") String query) { - return "CUSTOM_GREP: " + query; - } }