From 85a7134f81e4b5757c71c06d369392a937acf821 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Mon, 22 Jun 2026 19:34:07 -0400 Subject: [PATCH 1/5] WIP Phase 4.1 --- .../20260622-prompts.md | 271 ++++++++++++++++++ 1 file changed, 271 insertions(+) diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/20260622-prompts.md b/1682-java-tool-ergonomics-prompts-remove-before-merge/20260622-prompts.md index 39109bee2..5f453e841 100644 --- a/1682-java-tool-ergonomics-prompts-remove-before-merge/20260622-prompts.md +++ b/1682-java-tool-ergonomics-prompts-remove-before-merge/20260622-prompts.md @@ -737,3 +737,274 @@ For Java's `NONE("")`: if Jackson serializes it, it would emit `"defer": ""` — For all the created issues please find the right place to add the following requirement: ✅✅ YOU MUST run `mvn spotless:apply` before every commit. + +and add it. + +### 19:13 Response + + +Updated todo list + +Ran terminal command: gh issue view 1758 --repo github/copilot-sdk --json body -q .body | tail -20 + +I'll add the requirement to the Constraints section of each issue. The best placement is at the top of Constraints since it's a workflow mandate: + +Ran terminal command: cd /Users/edburns/workareas/copilot-sdk && for ISSUE in 1758 1759 1760 1761 1762; do + BODY=$(gh issue view "$ISSUE" --repo github/copilot-sdk --json body -q .body) + UPDATED=$(echo "$BODY" | sed 's/## Constraints/## Constraints\n\n- ✅✅ YOU MUST run `mvn spotless:apply` before every commit./') + echo "$UPDATED" | gh issue edit "$ISSUE" --repo github/copilot-sdk --body-file - + echo "Updated issue #$ISSUE" +done + +Done. All 5 issues (#1758–#1762) now have `✅✅ YOU MUST run mvn spotless:apply before every commit.` as the first bullet in their Constraints section. + +Created 1 todo + +### 19:13 Prompt + + +Ugh, I did a git problem. + +``` +edburns@Edwards-MacBook-Pro copilot-sdk % git push upstream +git push upstream +To github.com:github/copilot-sdk.git + ! [rejected] edburns/1682-java-tool-ergonomics -> edburns/1682-java-tool-ergonomics (non-fast-forward) +error: failed to push some refs to 'github.com:github/copilot-sdk.git' +hint: Updates were rejected because the tip of your current branch is behind +hint: its remote counterpart. If you want to integrate the remote changes, +hint: use 'git pull' before pushing again. +``` + +Please fix it and push. Make sure not to overwrite anything. + +### 19:16 Response + +Updated todo list + +Ran terminal command: git log --oneline -5 + +Ran terminal command: git log --oneline -5 upstream/edburns/1682-java-tool-ergonomics + +Ran terminal command: git fetch upstream edburns/1682-java-tool-ergonomics && git log --oneline upstream/edburns/1682-java-tool-ergonomics..HEAD + +Ran terminal command: git log --oneline HEAD..upstream/edburns/1682-java-tool-ergonomics + +Ran terminal command: git merge-base HEAD upstream/edburns/1682-java-tool-ergonomics + +Ran terminal command: git log --oneline d15cfcbe -1 + +Ran terminal command: echo "Local tree (Resolve 3.8):" && git cat-file -p e9c978bb | grep tree && echo "Remote tree (Resolve 3.8):" && git cat-file -p 622e3ae0 | grep tree + +Ran terminal command: git diff upstream/edburns/1682-java-tool-ergonomics..HEAD --stat + +Ran terminal command: git diff HEAD..upstream/edburns/1682-java-tool-ergonomics --stat + +The remote is strictly a subset of local — it just has deletions (all the same files local added). The local branch has everything the remote has plus more. A safe rebase will work: + +Ran terminal command: git rebase upstream/edburns/1682-java-tool-ergonomics + +Ran terminal command: git stash && git rebase upstream/edburns/1682-java-tool-ergonomics + +Ran terminal command: git stash pop && git log --oneline -5 + +Rebase succeeded — local is now 4 commits ahead of remote with no divergence. Now pushing: + +Ran terminal command: git push upstream edburns/1682-java-tool-ergonomics + +Fixed. The rebase detected that your local commits (with different hashes but same content) were duplicates of what was already on the remote, skipped them, and replayed only the unique commits on top. Push succeeded as a fast-forward. + +Created 3 todos + +### 19:16 Prompt + +Did you push it to upstream? + +### 19:17 Response + + +Updated todo list + + +Yes. The output confirms it pushed to `upstream`: + +``` +To github.com:github/copilot-sdk.git + 622e3ae0..170d5aaa edburns/1682-java-tool-ergonomics -> edburns/1682-java-tool-ergonomics +``` + +### 19:17 Prompt + + + +I don't see the commits at https://github.com/github/copilot-sdk/pull/1763/commits . + +Why? + +### 19:18 Response + +Updated todo list + +Ran terminal command: gh pr view 1763 --repo github/copilot-sdk --json headRefName,baseRefName,headRepository + +PR #1763 is tracking branch `copilot/edburns1682-java-tool-ergonomics`, but you pushed to `edburns/1682-java-tool-ergonomics`. They're different branches. + +You either need to: +1. Push to the PR's branch: `git push upstream edburns/1682-java-tool-ergonomics:copilot/edburns1682-java-tool-ergonomics` +2. Or update the PR to point to your branch instead. + +Which would you prefer? + +### 19:18 Prompt + +We should always and only be pushing to the PR's branch. + +Updated todo list + +Ran terminal command: git push upstream edburns/1682-java-tool-ergonomics:copilot/edburns1682-java-tool-ergonomics + +Ran terminal command: git fetch upstream copilot/edburns1682-java-tool-ergonomics && git log --oneline upstream/copilot/edburns1682-java-tool-ergonomics -5 + +Ran terminal command: git log --oneline HEAD -5 + +Ran terminal command: git diff upstream/copilot/edburns1682-java-tool-ergonomics..HEAD --stat | tail -5 + +Ran terminal command: git diff HEAD..upstream/copilot/edburns1682-java-tool-ergonomics --stat + +Local is a strict superset — the reverse diff only shows deletions (content local has that the PR branch doesn't). Nothing would be lost. This needs a force push since the histories diverged. + +Analyzing your answers... + +Ran terminal command: git push upstream edburns/1682-java-tool-ergonomics:copilot/edburns1682-java-tool-ergonomics --force-with-lease + +Done. PR #1763 now shows the commits at `https://github.com/github/copilot-sdk/pull/1763/commits`. + +Created 2 todos + +### 19:24 Prompt + +Now there are ton of files in the **Files changed** tab that are not at all related to this PR: + +https://github.com/github/copilot-sdk/pull/1763 + +Please fix that so only the files relevant to our work are in there. + +Updated todo list + +Ran terminal command: git log --oneline upstream/main..HEAD + +Ran terminal command: git diff upstream/main..HEAD --stat | tail -40 + +Ran terminal command: git show 5c360d56 --stat + +Ran terminal command: git fetch upstream main && git log --oneline upstream/main -3 + +The branch has a duplicate of commit `a3dbdd19` (already on main) with a different hash (`5c360d56`). Rebasing onto current `upstream/main` will drop it: + +Ran terminal command: git stash && git rebase upstream/main + +Git skipped the duplicate. Let me verify and push: + +Ran terminal command: git diff upstream/main..HEAD --stat && echo "---" && git log --oneline upstream/main..HEAD + +Only Java and prompt files now — no dotnet/go/nodejs/python/rust files. Pushing: + +Ran terminal command: git push upstream edburns/1682-java-tool-ergonomics:copilot/edburns1682-java-tool-ergonomics --force-with-lease && git stash pop + +Done. The PR's "Files changed" tab should now show only the 19 relevant files — all Java source, prompts, and plan files. The unrelated dotnet/go/nodejs/python/rust changes from the duplicate commit are gone. + +Created 2 todos + +### 19:29 Prompt + +Now we come back to the most important thing to fix, that `NONE` field in `java/src/main/java/com/github/copilot/rpc/ToolDefer.java`. + +Read this comment from a reviewer: https://github.com/github/copilot-sdk/pull/1763#discussion_r3455952418 + +And recall what we elicited earlier: + +> Let's take .NET. What is the behavior of the tool in the "absence" case? And what does that serialize as? + +> There's the answer: + +> **In .NET, when `Defer` is `null`, the key is simply omitted from the JSON.** +> +> The serializer uses `DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull`, so any `null`-valued property (like `Defer`, `OverridesBuiltInTool`, `SkipPermission`) is not written to the wire at all. The CLI receives a `ToolDefinition` JSON object with no `defer` key — meaning "no preference, use server default." +> +> So the wire behavior for "absence" is: **the `defer` field does not appear in the JSON-RPC message**. +> +> --- +> +> For Java's `NONE("")`: if Jackson serializes it, it would emit `"defer": ""` — which is **not the same** as omitting the key. The Java code must treat `NONE` as "do not serialize this field" (i.e., pass `null` to `ToolDefinition.create()` for the defer parameter). The `@JsonValue` of `""` is a landmine if it ever leaks to the wire. + +It seems to me at this point the best thing is to update the Javadoc so that the future work to implement the annotation processor doesn't cause the `defer` to leak onto the wire. + +Is there any other better way at this point? + +### 19:31 Response + +Updated todo list + +Read [](file:///Users/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefer.java#1-1), lines 1 to 70 + +Read [](file:///Users/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefer.java#70-70), lines 70 to 76 + + + +Fetched https://github.com/github/copilot-sdk/pull/1763#discussion_r3455952418 + +There's a better option than Javadoc alone: make `NONE` serialize as `null` instead of `""`. This provides a safety net at the Jackson level: + +```java +@JsonValue +public String getValue() { + return this == NONE ? null : value; +} +``` + +With this change, even if `ToolDefer.NONE` accidentally reaches a `ToolDefinition`, Jackson serializes it as `null`, and `@JsonInclude(NON_NULL)` (already on `ToolDefinition`) omits it from the wire. The bug becomes impossible regardless of what the processor does. + +The internal `""` string is still needed because Java annotations require compile-time constants (`null` can't be used as an annotation element default), but the `@JsonValue` doesn't have to expose it. + +**Tradeoff:** + +| Approach | Prevents wire leak? | Relies on downstream code? | +|----------|---------------------|---------------------------| +| Javadoc only | No — still serializes as `""` if leaked | Yes — processor must map to `null` | +| `@JsonValue` returns `null` for NONE | Yes — impossible to emit `""` | No — defense in depth | + +I'd recommend doing **both**: the `@JsonValue` fix for safety, plus a Javadoc update explaining `NONE` is an annotation-only sentinel. Want me to make those changes? + +Created 1 todo + +### 19:31 Prompt + +Yes. Please do that and commit it. But do not push it. + +Updated todo list + +Read [](file:///Users/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefer.java#22-22), lines 22 to 55 + +Replacing 14 lines with 28 lines in [](file:///Users/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefer.java) + + +``` + +``` + + +Ran terminal command: cd /Users/edburns/workareas/copilot-sdk/java && export JAVA_HOME="/Library/Java/JavaVirtualMachines/microsoft-25.jdk/Contents/Home" && export M2_HOME=${HOME}/Downloads/apache-maven-3.9.8 && export PATH=${M2_HOME}/bin:${JAVA_HOME}/bin:${PATH} && mvn spotless:apply -q + +Ran terminal command: cd /Users/edburns/workareas/copilot-sdk && git add java/src/main/java/com/github/copilot/rpc/ToolDefer.java && git commit -m "fix(java): make ToolDefer.NONE serialize as null to prevent wire leak + +NONE is an annotation-only sentinel for @CopilotTool(defer=...) defaults. +Its @JsonValue now returns null so @JsonInclude(NON_NULL) omits it from +the JSON-RPC payload, matching the nullable/optional semantics used by +all other SDKs (.NET CopilotToolDefer?, Node defer?, Go omitempty, +Python | None, Rust Option)." + +Committed as `b8ad8264`. Spotless check passed. Not pushed. + +Created 1 todo + +Made changes. From a4fe9b270e0a796a2510d37ded05581d13c8c746 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Mon, 22 Jun 2026 19:49:45 -0400 Subject: [PATCH 2/5] Remove prompts, pre-merge --- .../1682-low-level-tool-definition.md | 94 -- .../20260615-prompts.md | 613 ---------- .../20260616-prompts.md | 218 ---- .../20260617-prompts.md | 374 ------ .../20260618-prompts.md | 135 --- .../20260622-prompts.md | 1010 ----------------- ...that-use-low_level_tool_definition.yaml.md | 151 --- ...rance-reduction-for-implementation-plan.md | 504 -------- .../dd3021192/dependency-reduced-pom.xml | 39 - .../dd3021192/pom.xml | 55 - .../main/java/com/github/dd3021192/Main.java | 58 - .../dd3021192/MyTools$$CopilotToolMeta.java | 40 - .../java/com/github/dd3021192/MyTools.java | 20 - .../dd3021192/src/main/java/module-info.java | 9 - 14 files changed, 3320 deletions(-) delete mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/1682-low-level-tool-definition.md delete mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/20260615-prompts.md delete mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/20260616-prompts.md delete mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/20260617-prompts.md delete mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/20260618-prompts.md delete mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/20260622-prompts.md delete mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/add-tests-that-use-low_level_tool_definition.yaml.md delete mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md delete mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/dependency-reduced-pom.xml delete mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/pom.xml delete mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/Main.java delete mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/MyTools$$CopilotToolMeta.java delete mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/MyTools.java delete mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/module-info.java diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/1682-low-level-tool-definition.md b/1682-java-tool-ergonomics-prompts-remove-before-merge/1682-low-level-tool-definition.md deleted file mode 100644 index 34fb2ee91..000000000 --- a/1682-java-tool-ergonomics-prompts-remove-before-merge/1682-low-level-tool-definition.md +++ /dev/null @@ -1,94 +0,0 @@ -# Plan: Add E2E test for non-ergonomic (low-level) tool definition - -## Goal - -Add a failsafe IT test that exercises the **current explicit** `ToolDefinition.create()` / `ToolDefinition.createOverride()` API — the "non-ergonomic" approach — with multiple tools, `ToolSet` with `addCustom`/`addBuiltIn`, `getArgumentsAs()` deserialization into a record, and a tool handler that mutates application state. This establishes baseline test coverage before issue #1682 adds the annotation-driven ergonomic API. - -## Instructions - -Read `java.instructions.md` in my User level Copilot instructions. This session is about Java. - -Use the `new-java-e2e-test-yaml-and-test` skill to create a new failsafe IT test that exercises the non-ergonomic-tool-definition approach to tool definition. - -### What the test must exercise - -The test class should be `LowLevelToolDefinitionIT.java` in `java/src/test/java/com/github/copilot/`. It must demonstrate **all** of the following in a single session: - -1. **`ToolDefinition.create(name, description, schema, handler)`** — define at least two custom tools explicitly with `Map` schemas. -2. **`ToolDefinition.createOverride(name, description, schema, handler)`** — define one tool that overrides a built-in tool. -3. **`invocation.getArgumentsAs(SomeRecord.class)`** — at least one handler must deserialize arguments into a Java record (not `getArguments()` returning raw Map). -4. **`invocation.getArguments()`** — at least one handler must use the raw `Map` accessor. -5. **`ToolSet` with `addCustom("*").addBuiltIn("web_fetch")`** — pass `setAvailableTools(...)` on the `SessionConfig`. -6. **Handler mutates state** — one tool handler should mutate a field on the test class and the test should assert that the field was updated after the response. -7. **Handler returns `CompletableFuture.completedFuture(...)`** — all handlers return completed futures (as is the current pattern). - -### Concrete test design - -#### Snapshot category - -`tools` (reuse the existing category under `test/snapshots/tools/`). - -#### Snapshot file - -`test/snapshots/tools/low_level_tool_definition.yaml` - -#### Java test method name - -`lowLevelToolDefinition` (converts to `low_level_tool_definition` for snapshot lookup). - -#### Tool definitions for the test - -| Tool | Factory | Name | Description | Schema | Handler behavior | -|------|---------|------|-------------|--------|-----------------| -| Set Phase | `create` | `set_current_phase` | "Sets the current phase of the agent" | `{ type: object, properties: { phase: { type: string, enum: [searching, analyzing, done] } }, required: [phase] }` | Deserializes via `getArgumentsAs(PhaseArgs.class)` where `record PhaseArgs(String phase) {}`. Mutates a `currentPhase` field on the test. Returns `"Phase set to " + phase`. | -| Search | `create` | `search_items` | "Search for items by keyword" | `{ type: object, properties: { keyword: { type: string } }, required: [keyword] }` | Uses `getArguments()` raw Map. Returns a fixed string like `"Found: item_alpha, item_beta"`. | -| Override grep | `createOverride` | `grep` | "Custom grep override" | `{ type: object, properties: { query: { type: string } }, required: [query] }` | Uses `getArguments()`. Returns `"CUSTOM_GREP: " + query`. | - -#### Prompt - -``` -First, set the current phase to 'analyzing'. Then search for items with keyword 'copilot'. Report the phase and search results. -``` - -#### YAML snapshot structure - -Two conversations (one for the tool-call turn, one for the final response turn after tool results are provided): - -- **Conversation 1** (tool call turn): system `${system}` + user prompt → assistant with `tool_calls` for `set_current_phase` and `search_items`. -- **Conversation 2** (final response turn): full history including tool results → assistant final content mentioning "analyzing", "item_alpha", "item_beta". - -Study the existing snapshot files in `test/snapshots/tools/` carefully. In particular, study the snapshot file for the `testInvokesCustomTool` test in `ToolsTest.java` (`test/snapshots/tools/invokes_custom_tool.yaml`). It shows how tool call and tool result conversations are structured. Additionally, study `test/snapshots/tools/should_execute_multiple_custom_tools_in_parallel_single_turn.yaml` which shows multiple parallel tool calls in a single turn. - -#### Assertions - -1. `response` is not null. -2. Response content contains `"analyzing"` (confirming the phase tool was called). -3. Response content contains `"item_alpha"` or `"item_beta"` (confirming search tool was called). -4. The `currentPhase` field on the test class equals `"analyzing"` (confirming handler mutated state). - -#### Session config - -```java -new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) - .setAvailableTools(new ToolSet().addCustom("*").addBuiltIn("web_fetch")) - .setTools(List.of(setPhaseTool, searchTool, grepOverrideTool)) -``` - -### Step-by-step execution - -1. Create the YAML snapshot file at `test/snapshots/tools/low_level_tool_definition.yaml`. -2. Create the Java IT file at `java/src/test/java/com/github/copilot/LowLevelToolDefinitionIT.java`. -3. Run `mvn spotless:apply` from the `java/` directory (using the background + log pattern from `java.instructions.md`). -4. Run the test in isolation: - ```sh - cd java - LOG="$(date +%Y%m%d-%H%M)-job-logs.txt" && mvn failsafe:integration-test -Dit.test="LowLevelToolDefinitionIT#lowLevelToolDefinition" -Denforcer.skip=true > "$LOG" 2>&1 & tail -f "$LOG" - ``` -5. Fix any failures. Iterate until the isolated test passes cleanly. -6. Run the full build: - ```sh - cd java - LOG="$(date +%Y%m%d-%H%M)-job-logs.txt" && mvn clean verify > "$LOG" 2>&1 & tail -f "$LOG" - ``` -7. Fix any failures from the full build. Iterate until `mvn clean verify` passes cleanly. diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/20260615-prompts.md b/1682-java-tool-ergonomics-prompts-remove-before-merge/20260615-prompts.md deleted file mode 100644 index 6d2f3ec1a..000000000 --- a/1682-java-tool-ergonomics-prompts-remove-before-merge/20260615-prompts.md +++ /dev/null @@ -1,613 +0,0 @@ -### 18:28 Prompt - -Consider this C# usage of our API: - -```C# - [DisplayName("set_current_phase")] - [Description("Sets the current phase of the agent. Use this to report progress.")] - private void SetCurrentPhase(Phase phase) - { - Phase = phase; - updateUi(); - } - -... - - Session = await client.CreateSessionAsync(new() - { - OnPermissionRequest = PermissionHandler.ApproveAll, - SystemMessage = systemMessageConfig, - AvailableTools = new ToolSet().AddCustom("*").AddBuiltIn("web_fetch"), - Tools = [ - CopilotTool.DefineTool(SetCurrentPhase), - CopilotTool.DefineTool(ReportIntent, new() { OverridesBuiltInTool = true }), - CopilotTool.DefineTool(database.SearchProperties), - ], - }); -``` - -How would one do this with the Java API? - -### 18:30 Response - -Searched for regex `public static ToolDefinition create` (`**/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java`), 4 results - -Searched for regex `class ToolSet|addCustom|addBuiltIn|AddCustom|AddBuiltIn` (`**/java/src/main/java/com/github/copilot/rpc/ToolSet.java`), 8 results - -Searched for regex `setAvailableTools|setTools|List` (`**/java/src/main/java/com/github/copilot/rpc/SessionConfig.java`), 5 results - -Here's the Java equivalent: - -```java -var session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) - .setSystemMessage(systemMessageConfig) - .setAvailableTools(new ToolSet().addCustom("*").addBuiltIn("web_fetch")) - .setTools(List.of( - ToolDefinition.create("set_current_phase", "Sets the current phase of the agent. Use this to report progress.", - Map.of("type", "object", - "properties", Map.of("phase", Map.of("type", "string", "enum", List.of("searching", "analyzing", "done"))), - "required", List.of("phase")), - invocation -> { - Phase phase = invocation.getArgumentsAs(PhaseArgs.class).phase(); - this.phase = phase; - updateUi(); - return CompletableFuture.completedFuture("Phase set to " + phase); - }), - ToolDefinition.createOverride("report_intent", "Reports the agent's intent", - Map.of("type", "object", "properties", Map.of(/* ... */)), - invocation -> { /* ... */ }), - ToolDefinition.create("search_properties", "Search property listings", - Map.of("type", "object", "properties", Map.of(/* ... */)), - invocation -> database.searchProperties(invocation.getArguments())) - ))).get(30, TimeUnit.SECONDS); -``` - -The key differences from the C# version: - -| C# | Java | -|---|---| -| `CopilotTool.DefineTool(SetCurrentPhase)` — infers name, description, and schema from the method's attributes via reflection | `ToolDefinition.create(name, description, schema, handler)` — all explicit | -| `new() { OverridesBuiltInTool = true }` (options object) | `ToolDefinition.createOverride(...)` (dedicated factory) | -| `[DisplayName]` / `[Description]` attributes on the method | No annotation-based discovery; you provide the strings directly | -| Implicit JSON Schema generation from parameter types | You supply the `Map` schema manually | - -The Java SDK does not currently have a reflection/annotation-based `DefineTool` that can inspect a method and auto-generate the name, description, and parameter schema. Everything is explicit. - -### 18:31 Prompt - -Oof. That is very ugly. So you're saying there is no `DefineTool` analog in Java? - -### 18:34 Response - -Searched for regex `defineTool|DefineTool|@Tool|@CopilotTool|annotation.*tool|reflect.*tool|schema.*generat` (`**/java/src/main/java/**`), no results - -Correct — there is no `DefineTool` analog in the Java SDK today. No reflection-based discovery, no annotations, no automatic JSON Schema generation from parameter types. The only API is `ToolDefinition.create(name, description, schema, handler)` where you supply everything manually. - -The C# version gets its ergonomics from `CopilotTool.DefineTool(MethodGroup)` which uses .NET reflection to pull `[DisplayName]`, `[Description]`, and parameter types to auto-generate the schema. The Go SDK has `DefineTool[T, U](name, desc, handler)` which uses generics to derive the schema from the input type `T`. Python has `@define_tool` which introspects the decorated function's signature and docstring. - -Java *could* support something similar — e.g. a `DefineTool.from(instance, "methodName")` that reflects on the method's parameters and annotations, or a generic approach using a record type for args (similar to Go). But that doesn't exist yet. - -### 18:34 Prompt - -Given that `ToolDefinition` is already a record, run with that fact and sketch out what a `DefineTool` analog might look like. - -### 18:38 Response - -Here's a sketch leveraging records as the args type (similar to Go's `DefineTool[T, U]`): - -```java -// ─── User defines args as a record ─────────────────────────────────────────── - -record WeatherArgs( - @Param("City name") String location, - @Param(value = "Temperature unit", required = false) String unit -) {} - -// ─── Usage: one-liner tool definition ──────────────────────────────────────── - -var tool = ToolDefinition.define("get_weather", "Get current weather for a location", - WeatherArgs.class, - (args, invocation) -> { - String weather = fetchWeather(args.location(), args.unit()); - return CompletableFuture.completedFuture(weather); - }); -``` - -The handler receives the **deserialized record** directly — no more `invocation.getArgumentsAs(...)` boilerplate. - ---- - -### Implementation sketch - -```java -// ─── New annotation ────────────────────────────────────────────────────────── - -package com.github.copilot.rpc; - -import java.lang.annotation.*; - -@Target(ElementType.RECORD_COMPONENT) -@Retention(RetentionPolicy.RUNTIME) -public @interface Param { - /** Description of this parameter. */ - String value(); - /** Whether this parameter is required. Default true. */ - boolean required() default true; -} - -// ─── New typed handler functional interface ────────────────────────────────── - -@FunctionalInterface -public interface TypedToolHandler { - CompletableFuture handle(T args, ToolInvocation invocation); -} - -// ─── New factory method on ToolDefinition ──────────────────────────────────── - -public record ToolDefinition(...) { - - // Existing factories... - - /** - * Defines a tool with automatic JSON Schema generation from a record type. - * - *

Record components become the schema's {@code properties}. Use - * {@link Param} to annotate descriptions and optional flags. - * - * @param the args record type - * @param name tool name - * @param description tool description - * @param argsType the record class (e.g. {@code WeatherArgs.class}) - * @param handler receives the deserialized record + invocation context - * @return a fully constructed ToolDefinition with auto-generated schema - */ - public static ToolDefinition define( - String name, - String description, - Class argsType, - TypedToolHandler handler) { - - Map schema = SchemaGenerator.fromRecord(argsType); - - ToolHandler rawHandler = invocation -> { - T args = invocation.getArgumentsAs(argsType); - return handler.handle(args, invocation); - }; - - return new ToolDefinition(name, description, schema, rawHandler, null, null, null); - } -} -``` - ---- - -### Schema generation from record components - -```java -package com.github.copilot.rpc; - -import java.lang.reflect.RecordComponent; -import java.util.*; - -final class SchemaGenerator { - - static Map fromRecord(Class type) { - RecordComponent[] components = type.getRecordComponents(); - Map properties = new LinkedHashMap<>(); - List required = new ArrayList<>(); - - for (RecordComponent comp : components) { - Map prop = new LinkedHashMap<>(); - prop.put("type", jsonType(comp.getType())); - - Param param = comp.getAnnotation(Param.class); - if (param != null) { - prop.put("description", param.value()); - if (param.required()) { - required.add(comp.getName()); - } - } else { - // No annotation → required by default - required.add(comp.getName()); - } - - properties.put(comp.getName(), prop); - } - - Map schema = new LinkedHashMap<>(); - schema.put("type", "object"); - schema.put("properties", properties); - if (!required.isEmpty()) { - schema.put("required", required); - } - return schema; - } - - private static String jsonType(Class type) { - if (type == String.class) return "string"; - if (type == int.class || type == Integer.class - || type == long.class || type == Long.class) return "integer"; - if (type == double.class || type == Double.class - || type == float.class || type == Float.class) return "number"; - if (type == boolean.class || type == Boolean.class) return "boolean"; - if (type.isArray() || List.class.isAssignableFrom(type)) return "array"; - return "object"; - } -} -``` - ---- - -### The BRK206 example rewritten - -```java -record PhaseArgs(@Param("The phase to transition to") Phase phase) {} - -record SearchArgs(@Param("Search query") String query, - @Param(value = "Max results", required = false) Integer limit) {} - -var session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) - .setSystemMessage(systemMessageConfig) - .setAvailableTools(new ToolSet().addCustom("*").addBuiltIn("web_fetch")) - .setTools(List.of( - ToolDefinition.define("set_current_phase", - "Sets the current phase of the agent. Use this to report progress.", - PhaseArgs.class, - (args, inv) -> { - this.phase = args.phase(); - updateUi(); - return CompletableFuture.completedFuture("Phase set to " + args.phase()); - }), - ToolDefinition.define("report_intent", - "Reports the agent's intent", - ReportIntentArgs.class, - (args, inv) -> { /* ... */ }) - .withOverridesBuiltInTool(true), // would need a wither - ToolDefinition.define("search_properties", - "Search property listings", - SearchArgs.class, - (args, inv) -> database.searchProperties(args.query(), args.limit())) - ))).get(30, TimeUnit.SECONDS); -``` - ---- - -### Trade-offs - -| Pro | Con | -|-----|-----| -| Schema auto-generated from record components | Requires runtime reflection (one-time, at definition) | -| Type-safe handler — no manual `getArgumentsAs()` | Nested/complex schemas (arrays of objects) need more work | -| `@Param` keeps description co-located with the field | New annotation + new interface = new public API surface | -| Records are already idiomatic for value objects in Java 17+ | Enums in records (like `Phase`) need custom mapping logic | -| Very close to Go's `DefineTool[T, U]` ergonomics | Can't infer name/description from the method itself (unlike C#) | - -The main limitation vs C# is that Java doesn't have method-group-to-delegate conversion, so you can't just pass `this::setCurrentPhase` and have it figure out the name and schema. You still provide the name and description strings — but the schema becomes free. - -### 18:41 Prompt - -I have added the folder `langchain4j-00-main` to this VS Code workspace. - -Look at the design of `./langchain4j-core/src/main/java/dev/langchain4j/agent/tool/Tool.java`. Explain how that design differs from the record based one you proposed. - -### 18:46 Response - -Searched for files matching `**/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/Tool.java`, 1 match - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/Tool.java#1-1), lines 1 to 50 - -Searched for text `@interface P` (`**/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/P.java`), 1 result - -Searched for regex `class ToolSpecification|record ToolSpecification|String name|String description|JsonObjectSchema` (`**/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/ToolSpecification.java`), 14 results - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/Tool.java#50-50), lines 50 to 60 - -Searched for regex `toolSpecificationFrom|toolSpecificationsFrom|class ToolSpecifications` (`**/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/ToolSpecifications.java`), 6 results - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/P.java#1-1), lines 1 to 60 - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/ToolSpecifications.java#100-100), lines 100 to 160 - -Here's how the two designs differ: - ---- - -### LangChain4j: **Annotation-on-method** approach - -```java -@Tool("Get current weather for a location") -String getWeather(@P("City name") String location, - @P(value = "Unit", required = false) String unit) { - return fetchWeather(location, unit); -} -``` - -The framework discovers tools by scanning an **object's methods** for `@Tool`. It then: -1. Reflects on the `Method` to get the name (from `@Tool(name=...)` or the method name itself) -2. Reflects on each `Parameter` to build the JSON Schema (using `@P` for description/required) -3. Generates a `ToolSpecification` (name + description + schema) automatically -4. At invocation time, dispatches to the annotated method with deserialized args passed as method parameters - -**The user never writes a schema or a handler lambda** — the method IS the handler. - ---- - -### My record-based proposal: **Record-as-schema + explicit lambda** approach - -```java -record WeatherArgs(@Param("City name") String location, - @Param(value = "Unit", required = false) String unit) {} - -var tool = ToolDefinition.define("get_weather", "Get current weather", - WeatherArgs.class, - (args, invocation) -> CompletableFuture.completedFuture(fetchWeather(args.location(), args.unit()))); -``` - -The record defines the schema shape; the lambda is the handler. - ---- - -### Key differences - -| Aspect | LangChain4j (`@Tool` on method) | Record-based proposal | -|--------|--------------------------------|----------------------| -| **Discovery** | Automatic — scan object for `@Tool` methods | Manual — you explicitly call `define(...)` | -| **Tool name** | Derived from method name or `@Tool(name=...)` | Provided as a string argument | -| **Description** | `@Tool("description")` on the method | Provided as a string argument | -| **Parameter schema** | Generated from method parameter types + `@P` annotations | Generated from record components + `@Param` annotations | -| **Handler** | The annotated method itself — no lambda | Explicit `TypedToolHandler` lambda | -| **Arg delivery** | Each parameter is a separate method argument | Single deserialized record object | -| **Return type** | Any (String sent as-is, others serialized to JSON) | Always `CompletableFuture` | -| **Registration** | Implicit (pass object to framework, it finds all `@Tool` methods) | Explicit (build a `List`) | -| **Async** | Synchronous method (framework handles threading) | Explicitly async via `CompletableFuture` | - ---- - - - -### The fundamental philosophical difference - -LangChain4j treats **the method as the tool**. You annotate existing methods and the framework does everything else. This is closest to C#'s `CopilotTool.DefineTool(SetCurrentPhase)`. - -My record-based proposal treats **the record as the schema** but still requires explicit wiring. It's closer to Go's `DefineTool[T, U]` — you get schema generation for free but still wire things manually. - -If you wanted the LangChain4j-style ergonomics in the Copilot Java SDK, it would look something like: - -```java -class MyTools { - @CopilotTool("Sets the current phase of the agent") - CompletableFuture setCurrentPhase(@Param("The phase") Phase phase) { - this.phase = phase; - updateUi(); - return CompletableFuture.completedFuture("Done"); - } -} - -// Registration: scan an object for @CopilotTool methods -var tools = ToolDefinition.fromObject(myToolsInstance); -``` - -That's the most ergonomic option but requires the most framework machinery (method scanning, reflective invocation, argument marshaling per-parameter rather than per-record). - -### 18:46 Prompt - -Yes, I like the langchain4j approach much better. Let's start by filing an issue in `copilot-sdk`. - -Title - -Java: Ergonomics: Defining tools - -Description - -The current way of defining tools in Java is quite awful. - - -```java -var session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) - .setSystemMessage(systemMessageConfig) - .setAvailableTools(new ToolSet().addCustom("*").addBuiltIn("web_fetch")) - .setTools(List.of( - ToolDefinition.create("set_current_phase", "Sets the current phase of the agent. Use this to report progress.", - Map.of("type", "object", - "properties", Map.of("phase", Map.of("type", "string", "enum", List.of("searching", "analyzing", "done"))), - "required", List.of("phase")), - invocation -> { - Phase phase = invocation.getArgumentsAs(PhaseArgs.class).phase(); - this.phase = phase; - updateUi(); - return CompletableFuture.completedFuture("Phase set to " + phase); - }), - ToolDefinition.createOverride("report_intent", "Reports the agent's intent", - Map.of("type", "object", "properties", Map.of(/* ... */)), - invocation -> { /* ... */ }), - ToolDefinition.create("search_properties", "Search property listings", - Map.of("type", "object", "properties", Map.of(/* ... */)), - invocation -> database.searchProperties(invocation.getArguments())) - ))).get(30, TimeUnit.SECONDS); -``` - -Compare that with what you get in langchain4j: - -```java -@Tool("Get current weather for a location") -String getWeather(@P("City name") String location, - @P(value = "Unit", required = false) String unit) { - return fetchWeather(location, unit); -} -``` - -We simply must do better. - - - -### 18:51 Response - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && gh issue create \ - --repo github/copilot-sdk \ - --title "Java: Ergonomics: Defining tools" \ - --body 'The current way of defining tools in Java is quite awful. - -```java -var session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) - .setSystemMessage(systemMessageConfig) - .setAvailableTools(new ToolSet().addCustom("*").addBuiltIn("web_fetch")) - .setTools(List.of( - ToolDefinition.create("set_current_phase", "Sets the current phase of the agent. Use this to report progress.", - Map.of("type", "object", - "properties", Map.of("phase", Map.of("type", "string", "enum", List.of("searching", "analyzing", "done"))), - "required", List.of("phase")), - invocation -> { - Phase phase = invocation.getArgumentsAs(PhaseArgs.class).phase(); - this.phase = phase; - updateUi(); - return CompletableFuture.completedFuture("Phase set to " + phase); - }), - ToolDefinition.createOverride("report_intent", "Reports the agent'''s intent", - Map.of("type", "object", "properties", Map.of(/* ... */)), - invocation -> { /* ... */ }), - ToolDefinition.create("search_properties", "Search property listings", - Map.of("type", "object", "properties", Map.of(/* ... */)), - invocation -> database.searchProperties(invocation.getArguments())) - ))).get(30, TimeUnit.SECONDS); -``` - -Compare that with what you get in [langchain4j](https://github.com/langchain4j/langchain4j): - -```java -@Tool("Get current weather for a location") -String getWeather(@P("City name") String location, - @P(value = "Unit", required = false) String unit) { - return fetchWeather(location, unit); -} -``` - -We simply must do better.' - -Filed: [#1682](https://github.com/github/copilot-sdk/issues/1682) — Java: Ergonomics: Defining tools. - -### 18:52 Prompt - -Define a new ADR in `java/docs/adr/adr-005-tool-definition.md`. Look at the existing ADRs in there, and follow ADR best practice. Create the new ADR describing the approaches we evaluated and how we ended up choosing the langchain4j approach as shown in https://github.com/langchain4j/langchain4j/blob/main/docs/docs/tutorials/tools.md#high-level-tool-api . - -Be explicit about how we looked at the record approach, the current approach, and chose the langchain4j approach for its Java idiomatic qualities. - -### 19:02 Response - -Searched for files matching `**/java/docs/adr/*.md`, 4 matches - -Read [](file:///home/edburns/workareas/copilot-sdk/java/docs/adr/adr-004-copilotexperimental.md#1-1), lines 1 to 80 - -Now let me fetch the langchain4j tools tutorial for reference: - - - -Fetched https://github.com/langchain4j/langchain4j/blob/main/docs/docs/tutorials/tools.md - -Now I have all the context. Let me create the ADR: - -Created [](file:///home/edburns/workareas/copilot-sdk/java/docs/adr/adr-005-tool-definition.md) - -Created adr-005-tool-definition.md. It documents all three options evaluated (status quo, record-as-schema, annotation-on-method), the rationale for choosing the langchain4j-style approach, and links to the issue and reference implementations. - -Made changes. - - -### 19:02 Prompt - - - -Is there a way around this? - ------ -- Build requirement: recommend `-parameters` javac flag in documentation. Without it, developers must use `@Param(name="...")` explicitly (same trade-off as langchain4j, Spring, etc.). ------ - -We did an automatic annotation processor for `@CopilotExperimental`. Can't we do something similar here? - -### 19:06 Response - -Yes — this is completely solvable with an annotation processor. - -The key insight: **`javax.lang.model.element.VariableElement.getSimpleName()` always returns the real parameter name at compile time**, regardless of whether `-parameters` is passed to `javac`. The `-parameters` flag only controls whether those names survive into the `.class` bytecode for runtime reflection. An annotation processor sees the source-level names unconditionally. - -### How it would work - -The processor runs at compile time, finds all `@CopilotTool`-annotated methods, and generates a metadata class (or resource) that bakes in the parameter names, types, and descriptions: - -```java -// GENERATED — do not edit -final class MyTools$$CopilotToolMeta { - static List definitions(MyTools instance) { - return List.of( - new ToolDefinition("set_current_phase", - "Sets the current phase of the agent.", - Map.of("type", "object", - "properties", Map.of("phase", Map.of("type", "string", "description", "The phase to transition to")), - "required", List.of("phase")), - invocation -> { - Phase phase = invocation.getArgumentsAs(Phase.class); - return CompletableFuture.completedFuture( - instance.setCurrentPhase(phase)); - }, null, null, null) - ); - } -} -``` - -At runtime, `ToolDefinition.fromObject(myTools)` just loads the generated `$$CopilotToolMeta` class — zero reflection, zero dependency on `-parameters`. - -### Precedent in the ecosystem - -| Framework | Approach | -|-----------|----------| -| **Micronaut** | Annotation processor generates all DI metadata at compile time — no runtime reflection, no `-parameters` needed | -| **Dagger 2** | Processor generates `_Factory` / `_MembersInjector` classes | -| **MapStruct** | Processor generates mapper implementations from interface method signatures | -| **Our own `@CopilotExperimental`** | Processor walks declared elements via JSR 269 | - -### Advantages over the `-parameters` fallback - -1. **No build configuration requirement** — works out of the box with any `javac` invocation. -2. **No runtime reflection** — the generated code calls the method directly. Faster, GraalVM-native-friendly, no `setAccessible` issues. -3. **Compile-time validation** — the processor can emit errors if `@Param` is missing on a parameter, if a type isn't serializable, if two tools have the same name, etc. (same as we did for `@CopilotExperimental`). -4. **Schema correctness guaranteed** — the schema is computed once from the source AST, not guessed at runtime from erased generics. - -### Trade-off vs. pure runtime reflection (langchain4j default) - -| | Annotation processor | Runtime reflection | -|---|---|---| -| Requires `-parameters`? | No | Yes (or `@P(name=...)`) | -| GraalVM native-image friendly? | Yes | Needs reflection config | -| Compile-time error checking? | Yes | Fails at runtime | -| Extra generated source files? | Yes | None | -| Works without running the processor? | No — but fails loudly at compile time | Yes (degraded) | - -So yes: we can follow the same JSR 269 pattern as `@CopilotExperimental` to eliminate the `-parameters` requirement entirely. The processor sees the real names from source, bakes them into generated code, and the runtime never needs to call `Parameter.getName()`. - - - -Want me to update the ADR to reflect this? - -### 19:07 Prompt - -Yes please. - -### 19:14 Response - -Read [](file:///home/edburns/workareas/copilot-sdk/java/docs/adr/adr-005-tool-definition.md#195-195), lines 195 to 220 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/docs/adr/adr-005-tool-definition.md#218-218), lines 218 to 240 - -Replacing 15 lines with 10 lines in [](file:///home/edburns/workareas/copilot-sdk/java/docs/adr/adr-005-tool-definition.md) - - -``` - -``` -Done. Added the `@CopilotExperimental` decision as a consequence bullet — the new annotations and utility methods will be gated behind experimental opt-in until the API stabilizes. - -Made changes. diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/20260616-prompts.md b/1682-java-tool-ergonomics-prompts-remove-before-merge/20260616-prompts.md deleted file mode 100644 index 931f02d08..000000000 --- a/1682-java-tool-ergonomics-prompts-remove-before-merge/20260616-prompts.md +++ /dev/null @@ -1,218 +0,0 @@ -# DAY: 2026-06-16 - -### 19:31 Prompt - -Consider this C# usage of our API: - -```C# - [DisplayName("set_current_phase")] - [Description("Sets the current phase of the agent. Use this to report progress.")] - private void SetCurrentPhase(Phase phase) - { - Phase = phase; - updateUi(); - } - -... - - Session = await client.CreateSessionAsync(new() - { - OnPermissionRequest = PermissionHandler.ApproveAll, - SystemMessage = systemMessageConfig, - AvailableTools = new ToolSet().AddCustom("*").AddBuiltIn("web_fetch"), - Tools = [ - CopilotTool.DefineTool(SetCurrentPhase), - CopilotTool.DefineTool(ReportIntent, new() { OverridesBuiltInTool = true }), - CopilotTool.DefineTool(database.SearchProperties), - ], - }); -``` - -According to our chat yesterday, here's how you'd do it with our Java API. For discussion call this the non-ergonomic-tool-definition. - -```java -var session = client.createSession(new SessionConfig() - .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) - .setSystemMessage(systemMessageConfig) - .setAvailableTools(new ToolSet().addCustom("*").addBuiltIn("web_fetch")) - .setTools(List.of( - ToolDefinition.create("set_current_phase", "Sets the current phase of the agent. Use this to report progress.", - Map.of("type", "object", - "properties", Map.of("phase", Map.of("type", "string", "enum", List.of("searching", "analyzing", "done"))), - "required", List.of("phase")), - invocation -> { - Phase phase = invocation.getArgumentsAs(PhaseArgs.class).phase(); - this.phase = phase; - updateUi(); - return CompletableFuture.completedFuture("Phase set to " + phase); - }), - ToolDefinition.createOverride("report_intent", "Reports the agent's intent", - Map.of("type", "object", "properties", Map.of(/* ... */)), - invocation -> { /* ... */ }), - ToolDefinition.create("search_properties", "Search property listings", - Map.of("type", "object", "properties", Map.of(/* ... */)), - invocation -> database.searchProperties(invocation.getArguments())) - ))).get(30, TimeUnit.SECONDS); -``` - -Also according to our chat yesterday: - ------ - -The key differences from the C# version: - -| C# | Java | -|---|---| -| `CopilotTool.DefineTool(SetCurrentPhase)` — infers name, description, and schema from the method's attributes via reflection | `ToolDefinition.create(name, description, schema, handler)` — all explicit | -| `new() { OverridesBuiltInTool = true }` (options object) | `ToolDefinition.createOverride(...)` (dedicated factory) | -| `[DisplayName]` / `[Description]` attributes on the method | No annotation-based discovery; you provide the strings directly | -| Implicit JSON Schema generation from parameter types | You supply the `Map` schema manually | - -The Java SDK does not currently have a reflection/annotation-based `DefineTool` that can inspect a method and auto-generate the name, description, and parameter schema. Everything is explicit. - ------- - -We are working on issue https://github.com/github/copilot-sdk/issues/1682 that addresses this gap. - -But first, I want to add test coverage for the way to do it that apparently currently does work. - -Write a plan to `1682-java-tool-ergonomics-prompts-remove-before-merge/1682-low-level-tool-definition.md` to instruct a `copilot --yolo` session to: - -- Read `java.instructions.md` in my User level Copilot instructions. This session is about Java. -- Use the `new-java-e2e-test-yaml-and-test` skill to create a new failsafe IT test that exercises the non-ergonomic-tool-definition approach to tool definition. -- Run the test in isolation until you get a clean run of just that test. -- Run all the automated tests according to the `java.instructions.md` and `mvn clean verify`. - - - -### 19:40 Prompt - -Right now, the most important thing we did in this session was learn how to create a net new snapshot YAML, `test/snapshots/system_message_sections/should_use_replaced_identity_section_in_response.yaml` and use it in a net-new failsafe IT test. - -This is an important skill. - -I've never used skills before. - -Help me package up this knowledge as a reusable skill. - -I guess skills have names, right? Well, let's call this `new-java-e2e-test-yaml-and-test`, using whatever naming convention one uses for skills. - -Explain what you're donig step by step. I have a need for this skill right now. - -### 19:53 Prompt - -▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ -◐ The user wants to create a reusable skill for creating new E2E test YAML snapshots and corresponding failsafe IT tests. Let me first understand how skills work in this repo - I can see from the environment that there's already a `java-coding-skill` defined. Let me look at how skills are structured. - -● Let me look at how skills are structured in this repo so I can explain and create one for you. - -● Find existing skills (shell) - │ find /home/edburns/workareas/copilot-sdk/.github/skills -type f | sort - └ 4 lines... - -● List directory .github/skills/java-coding-skill - └ 1 file found - -● List directory .github/skills/rust-coding-skill - └ 2 files found - -● Read SKILL.md - │ .github/skills/rust-coding-skill/SKILL.md - └ L1:30 (30 lines read) - -● Read SKILL.md - │ .github/skills/java-coding-skill/SKILL.md - └ L1:10 (10 lines read) - -● Good. Here's what I'm seeing and what I'll do: - - How Skills Work (Step by Step) - - 1. Skills live in .github/skills// — each skill is a directory containing at minimum a SKILL.md file - 2. SKILL.md has YAML frontmatter with name and description — the description tells Copilot when to invoke the skill - 3. The body of SKILL.md is the instruction content — the "knowledge" that gets loaded when the skill is invoked - 4. Optional extra files (like examples.md) can be included for reference material - - What I'll Create - - For your new-e2e-test-yaml-and-test skill, I'll create: - - - .github/skills/new-e2e-test-yaml-and-test/SKILL.md — the main skill with step-by-step instructions on how to create a new YAML snapshot and corresponding failsafe IT test - - .github/skills/new-e2e-test-yaml-and-test/examples.md — the actual example files we created this session as reference - - Let me create it now: - -✗ Create skill directory (shell) - │ mkdir -p /home/edburns/workareas/copilot-sdk/.github/skills/new-e2e-test-yaml-and-test - └ The user rejected this tool call. User feedback: I named it wrong. The skill should be called - `new-java-e2e-test-yaml-and-test` - -● Create skill directory (shell) - │ mkdir -p /home/edburns/workareas/copilot-sdk/.github/skills/new-java-e2e-test-yaml-and-test - └ 1 line... - -● IDE connection lost: Visual Studio Code - Insiders closed - -● Now let me create the main SKILL.md: - -● Create SKILL.md +222 - └ .github/skills/new-java-e2e-test-yaml-and-test/SKILL.md - -● Now let me add the examples file with the actual artifacts from this session: - -● Get example YAML (shell) - │ cat /home/edburns/workareas/copilot-sdk/test/snapshots/system_message_sections/should_use_replaced_identity_section_in - │ _response.yaml - └ 16 lines... - -● All permissions are now enabled. Tool, path, and URL requests will be automatically approved. - -● Get example test (shell) - │ cat /home/edburns/workareas/copilot-sdk/java/src/test/java/com/github/copilot/SystemMessageSectionsIT.java - └ 193 lines... - -● Create examples.md +178 - └ .github/skills/new-java-e2e-test-yaml-and-test/examples.md - -● Done. Here's what I created: - - .github/skills/new-java-e2e-test-yaml-and-test/ - ├── SKILL.md ← Main instructions (when/how to create new E2E tests) - └── examples.md ← Real examples from this session (simple + tool-call) - - How it works: - - - The description in the frontmatter tells Copilot to invoke this skill whenever you're creating a new Java E2E integration test with a YAML snapshot - - SKILL.md covers: YAML format, proxy matching logic, the Java test template, common pitfalls, and the key constraint that you must handcraft YAMLs (can't record via Java) - - examples.md has two working examples — the simple Botanica test and the multi-turn tool-call test - - The skill is ready to use. Next time you (or Copilot) need to add a new E2E test, the skill will be available as new-java-e2e-test-yaml-and-test. - -### 19:54 Prompt - -Ok, now, in a subsequent session, I will want to use the new skill `new-java-e2e-test-yaml-and-test`. - -How do I instruct the agent to use this skill to do its job? - -Is it something like - -`Use the skill new-java-e2e-test-yaml-and-test to create a new test and yaml that exercises the feature we just created.` - -Or something else? - -### 19:56 Response - - Based on the documentation, here's how skills work: - - You don't need to explicitly invoke skills. Skills are automatically loaded based on their description field in the frontmatter. The agent sees the skill listed in and invokes it when the task matches the description. - - However, you can be explicit if you want. Any of these would work: - - 1. Implicit (recommended): Just describe what you want — the skill's description matches "creating a new Java E2E integration test that requires a new replay proxy YAML snapshot": - Create a new failsafe IT test with a YAML snapshot that exercises the new feature. - 2. Explicit mention: Reference the skill by name to make sure it's invoked: - Use the new-java-e2e-test-yaml-and-test skill to create a test and YAML snapshot for the new feature. - 3. Check available skills: Use /skills in the CLI to see what's loaded, or /env to see the full environment including skills. - - Both approaches work. The agent sees the skill in its available tools and will call it before generating code. The explicit mention is a good safety net if you want to be sure. - diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/20260617-prompts.md b/1682-java-tool-ergonomics-prompts-remove-before-merge/20260617-prompts.md deleted file mode 100644 index 8926ad586..000000000 --- a/1682-java-tool-ergonomics-prompts-remove-before-merge/20260617-prompts.md +++ /dev/null @@ -1,374 +0,0 @@ -## dd-3018003-implement-tools - -Review the chat history in `copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/20260615-prompts.md`. - -Most important is the content within the XML element `` - -Also important is the content within the XML element `` - -Use `gh` to open and read and understand any issue or PR links to https://github.com/github/copilot-sdk. - -Read and understand the ADR in `java/docs/adr/adr-005-tool-definition.md`. - -We're not yet ready to ask you to write a `copilot --yolo` ready plan to do the work. We need to break it down better. - -Write a markdown file to `1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md` that is a human-targeted plan for how to proceed with the implementation of the `@CopilotTool` ergonomics improvement. - -- At a high level, I know we'll need to define the - - - `@CopilotTool` annotation, with `@CopilotExperimental`. - - - The annotation processor that makes the `@CopilotTool` annotation work. - - - Some way to "register" a tool. A Java idiomatic way of this in C# - - ```C# - CopilotTool.DefineTool(SetCurrentPhase), - CopilotTool.DefineTool(ReportIntent, new() { OverridesBuiltInTool = true }), - CopilotTool.DefineTool(database.SearchProperties), - ``` - - - **MAYBE** something like this: - - ``` - // Registration: scan an object for @CopilotTool methods - var tools = ToolDefinition.fromObject(myToolsInstance); - ``` - - - I say maybe, because I'm not exactly sure how LangChain4J does it. Also, I've been told that micronaut also has a similar thing. I have added the folders `langchain4j-00-main` and `micronaut-core-00` to this VS Code project for reference. - -Break the work down into phases. Note, we've already done these first two phases. - -1. ✅ Define the problem and make an architectural decision, recorded in an ADR. - -2. ✅ Verify the existing way works in Java. (This work is proceeding but as the human DRI, I am confident it will complete successfully.) - -### 13:41 Response - -Updated todo list - -Let me gather all the necessary context first. - -Read [](file:///home/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/20260615-prompts.md#1-1), lines 1 to 100 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/docs/adr/adr-005-tool-definition.md#1-1), lines 1 to 200 - -Read [](file:///home/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/20260615-prompts.md#100-100), lines 100 to 400 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/docs/adr/adr-005-tool-definition.md#200-200), lines 200 to 236 - -Read [](file:///home/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/20260615-prompts.md#400-400), lines 400 to 600 - -Read [](file:///home/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/20260616-prompts.md#1-1), lines 1 to 100 - -Read [](file:///home/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/20260616-prompts.md#100-100), lines 100 to 200 - -Now let me look at how LangChain4J and Micronaut handle tool registration to give good guidance in the plan. - -Research LangChain4J and Micronaut tool registration - -Read [](file:///home/edburns/workareas/copilot-sdk/test/snapshots/tools/low_level_tool_definition.yaml) - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/test/java/com/github/copilot/LowLevelToolDefinitionIT.java#1-1), lines 1 to 100 - -Read [](file:///home/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/add-tests-that-use-low_level_tool_definition.yaml.md) - -Searched for files matching `langchain4j-00-main` · `langchain4j-core/src/main/java/dev/langchain4j/agent/tool/*.java`, 9 matches - -Searched for files matching `micronaut-core-00` · `core-processor/src/main/java/**/*Processor.java`, 1 match - -No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. -If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). -Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. - -Searched for files matching `micronaut-core-00` · `core-processor/src/**/*.java`, 30 matches - -No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. -If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). -Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/Tool.java) - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/ToolSpecifications.java#1-1), lines 1 to 150 - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/P.java#1-1), lines 1 to 100 - -No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. -If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). -Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. - -Searched for text `ToolExecutor` (`langchain4j-00-main`), 39 results - -Searched for files matching `micronaut-core-00` · `core-processor/src/main/java/io/micronaut/inject/writer/**`, 20 matches - -Searched for files matching `langchain4j-00-main` · `langchain4j/src/main/java/dev/langchain4j/service/tool/*.java`, 18 matches - -Searched for text `BeanDefinitionWriter` (`micronaut-core-00` · `core-processor/src/main/java/io/micronaut/inject/**`), 49 results - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j/src/main/java/dev/langchain4j/service/tool/ToolExecutor.java) - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j/src/main/java/dev/langchain4j/service/tool/DefaultToolExecutor.java#1-1), lines 1 to 120 - -No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. -If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). -Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. - -Read [](file:///home/edburns/workareas/micronaut-core-00/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java#1-1), lines 1 to 150 - -Searched for files matching `micronaut-core-00` · `core-processor/src/main/java/io/micronaut/inject/processing/**/*.java`, 17 matches - -Read [](file:///home/edburns/workareas/micronaut-core-00/core-processor/src/main/java/io/micronaut/inject/visitor/TypeElementVisitor.java#1-1), lines 1 to 80 - -No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. -If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). -Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. - -Read [](file:///home/edburns/workareas/micronaut-core-00/core-processor/src/main/java/io/micronaut/inject/visitor/BeanElementVisitor.java) - -Searched for files matching `micronaut-core-00` · `core-processor/src/main/java/io/micronaut/inject/*.java`, no matches - -Read [](file:///home/edburns/workareas/micronaut-core-00/core-processor/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java) - -No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. -If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). -Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. - -Read [](file:///home/edburns/workareas/micronaut-core-00/core-processor/src/main/java/io/micronaut/inject/processing/definition/DefaultElementBeanDefinitionBuilderFactory.java#1-1), lines 1 to 80 - -No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. -If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). -Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. - -No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. -If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). -Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. - -Read [](file:///home/edburns/workareas/micronaut-core-00/core-processor/src/main/java/io/micronaut) - -Searched for files matching `micronaut-core-00` · `core-processor/src/main/java/io/micronaut/inject/*.java`, no matches - -Read [](file:///home/edburns/workareas/micronaut-core-00/core-processor/src/main/java/io/micronaut/inject) - -No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. -If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). -Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j/src/main/java/dev/langchain4j/service/tool/DefaultToolExecutor.java#120-120), lines 120 to 250 - -Searched for text `Processor extends AbstractProcessor` (`micronaut-core-00`), 2 results - -Searched for files matching `micronaut-core-00` · `**/*Processor.java`, 5 matches - -Read [](file:///home/edburns/workareas/micronaut-core-00/inject-java/src/main/java/io/micronaut/annotation/processing/AbstractInjectAnnotationProcessor.java#1-1), lines 1 to 150 - -Read [](file:///home/edburns/workareas/micronaut-core-00/inject-java/src/main/java/io/micronaut/annotation/processing/AbstractInjectAnnotationProcessor.java#150-150), lines 150 to 250 - -Created memory file [](file:///home/edburns/.vscode-server-insiders/data/User/workspaceStorage/3f1fe06cc8e5cdbce2aa19a5a3bd183d/GitHub.copilot-chat/memory-tool/memories/ZGI5Y2E3ZTMtNDc2My00MTdkLWFjMDEtMDlmY2ViM2M0NTcy/tool-registration-research.md) - -Now let me read the reference prompt file and the existing LowLevelToolDefinitionIT test fully: - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/test/java/com/github/copilot/LowLevelToolDefinitionIT.java#100-100), lines 100 to 112 - -Read [](file:///home/edburns/workareas/copilot-sdk/temporary-prompts/add-tests-that-use-should_use_replaced_identity_section_in_response.md) - -Let me look at the existing `@CopilotExperimental` annotation processor for the pattern we'll follow: - -Explore CopilotExperimental processor pattern - -Searched for text `@CopilotExperimental` (`copilot-sdk` · `java/**`), 101 results - -Searched for files matching `copilot-sdk` · `java/**/CopilotExperimental.java`, 1 match - -Searched for files matching `copilot-sdk` · `java/**/AbstractProcessor.java`, no matches - -Searched for files matching `copilot-sdk` · `java/**/Processor.java`, no matches - -Searched for files matching `copilot-sdk` · `java/**/ToolDefinition.java`, 1 match - -Searched for files matching `copilot-sdk` · `java/**/ToolHandler.java`, 1 match - -Searched for files matching `copilot-sdk` · `java/**/ToolInvocation.java`, 1 match - -Searched for files matching `copilot-sdk` · `java/**/ToolSet.java`, 1 match - -Searched for text `AbstractProcessor` (`copilot-sdk` · `java/**`), 4 results - -Searched for text `process(Set annotations` (`copilot-sdk` · `java/**`), 1 result - -Searched for text `Processor` (`copilot-sdk` · `java/**/META-INF/services/**`), 2 results - -Read [](file:///home/edburns/workareas/copilot-sdk/java/pom.xml#1-1), lines 1 to 100 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/CopilotExperimental.java) - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/CopilotExperimentalProcessor.java#1-1), lines 1 to 150 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java#1-1), lines 1 to 100 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolHandler.java) - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolInvocation.java#1-1), lines 1 to 80 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolSet.java#1-1), lines 1 to 100 - -Searched for files matching `copilot-sdk` · `java/**/AllowCopilotExperimental.java`, 1 match - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/CopilotExperimentalProcessor.java#140-140), lines 140 to 165 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/pom.xml#100-100), lines 100 to 200 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/docs/adr/adr-004-copilotexperimental.md) - -Read [](file:///home/edburns/workareas/copilot-sdk/java) - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot) - -Read [](file:///home/edburns/workareas/copilot-sdk/java/pom.xml#1-1), lines 1 to 600 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/AllowCopilotExperimental.java) - -Searched for files matching `copilot-sdk` · `java/src/test/java/**/CopilotExperimentalProcessorTest.java`, 1 match - -Searched for text `CopilotExperimental` (`copilot-sdk` · `java/src/test/**`), 13 results - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolInvocation.java#80-80), lines 80 to 172 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolSet.java#100-100), lines 100 to 123 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/resources/META-INF/services) - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java#80-80), lines 80 to 150 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/test/java/com/github/copilot/CopilotExperimentalProcessorTest.java) - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/resources/META-INF/services/javax.annotation.processing.Processor) - -Searched for files matching `copilot-sdk` · `java/**/module-info.java`, 1 match - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc) - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/module-info.java) - -Read memory - -Created memory file [](file:///home/edburns/.vscode-server-insiders/data/User/workspaceStorage/3f1fe06cc8e5cdbce2aa19a5a3bd183d/GitHub.copilot-chat/memory-tool/memories/ZGI5Y2E3ZTMtNDc2My00MTdkLWFjMDEtMDlmY2ViM2M0NTcy/copilot-experimental-annotation-exploration.md) - -Read [](file:///home/edburns/.vscode-server-insiders/data/User/workspaceStorage/3f1fe06cc8e5cdbce2aa19a5a3bd183d/GitHub.copilot-chat/chat-session-resources/db9ca7e3-4763-417d-ac01-09fceb3c4572/toolu_vrtx_01XThiJD8h5cYY6QPnjSbxVy__vscode-1781713597255/content.txt#1-1), lines 1 to 200 - -Now let me look at the specific LangChain4J implementation for reference on schema generation and tool execution: - -LangChain4J tool schema generation details - -Searched for files matching `langchain4j-00-main` · `langchain4j-core/src/main/java/dev/langchain4j/agent/tool/ToolSpecifications.java`, 1 match - -Searched for files matching `langchain4j-00-main` · `langchain4j-core/src/main/java/dev/langchain4j/agent/tool/Tool.java`, 1 match - -Searched for files matching `langchain4j-00-main` · `langchain4j-core/src/main/java/dev/langchain4j/agent/tool/P.java`, 1 match - -Searched for files matching `langchain4j-00-main` · `langchain4j-core/src/main/java/dev/langchain4j/agent/tool/*Executor*.java`, no matches - -Searched for files matching `langchain4j-00-main` · `langchain4j-core/src/main/java/dev/langchain4j/model/chat/request/json/**`, 13 matches - -No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. -If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). -Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/ToolSpecifications.java) - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/Tool.java) - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/P.java) - -Searched for files matching `langchain4j-00-main` · `langchain4j-core/src/main/java/dev/langchain4j/agent/tool/*.java`, 9 matches - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/ToolSpecifications.java#224-224), lines 224 to 224 - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/model/chat/request/json/JsonSchema.java) - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/model/chat/request/json/JsonSchemaElement.java) - -Searched for regex `class.*JsonSchemaElementUtils|jsonSchemaElementFrom` (`langchain4j-00-main` · `langchain4j-core/src/main/java/dev/langchain4j/**`), 10 results - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/ToolExecutionRequest.java) - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/ToolSpecification.java#1-1), lines 1 to 200 - -No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. -If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). -Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/internal/JsonSchemaElementUtils.java#1-1), lines 1 to 500 - -Searched for regex `Method\.invoke|coerceArguments|parseArguments|jsonToObjects` (`langchain4j-00-main`), 23 results - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/internal/JsonSchemaElementUtils.java#100-100), lines 100 to 500 - -Searched for regex `execute.*Tool|invoke.*request|class.*Handler.*Tool` (`langchain4j-00-main` · `langchain4j-core/**`), 22 results - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-skills/src/main/java/dev/langchain4j/skills/AbstractSkillToolExecutor.java) - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/model/chat/request/json/JsonObjectSchema.java#1-1), lines 1 to 200 - -Searched for regex `class.*AiService|AiServiceImpl` (`langchain4j-00-main` · `langchain4j-core/src/main/java/dev/langchain4j/**`), 28 results - -No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. -If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). -Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/service/tool) - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/service) - -Searched for files matching `langchain4j-00-main` · `langchain4j-core/src/main/java/dev/langchain4j/**/ToolExecutor.java`, no matches - -No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. -If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). -Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. - -Searched for regex `interface ToolExecutor` (`langchain4j-00-main`), 1 result - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-skills/src/main/java/dev/langchain4j/skills/ActivateSkillToolExecutor.java) - -Searched for regex `.*` (`langchain4j-00-main` · `langchain4j/src/main/java/dev/langchain4j/service/tool/**`), 101 results - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j/src/main/java/dev/langchain4j/service/tool/ToolExecutor.java) - -Searched for regex `class.*ToolInvoker|class.*DefaultToolExecutor|executeToolMethod|invokeToolMethod` (`langchain4j-00-main`), 2 results - -Searched for files matching `langchain4j-00-main` · `langchain4j/src/main/java/dev/langchain4j/service/tool/ToolExecutionResult.java`, 1 match - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j/src/main/java/dev/langchain4j/service/tool/DefaultToolExecutor.java#1-1), lines 1 to 500 - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j/src/main/java/dev/langchain4j/service/tool/ToolExecutionResult.java#1-1), lines 1 to 100 - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j/src/main/java/dev/langchain4j/service/tool/ToolExecutionRequestUtil.java) - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/model/chat/request/json/JsonArraySchema.java) - -Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/model/chat/request/json/JsonEnumSchema.java) - -Created memory file [](file:///home/edburns/.vscode-server-insiders/data/User/workspaceStorage/3f1fe06cc8e5cdbce2aa19a5a3bd183d/GitHub.copilot-chat/memory-tool/memories/ZGI5Y2E3ZTMtNDc2My00MTdkLWFjMDEtMDlmY2ViM2M0NTcy/langchain4j-tool-implementation-analysis.md) - -Created 6 todos - -Now I have comprehensive context from all sources. Let me create the plan: - -Created [](file:///home/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md) - -Created dd-3018003-ignorance-reduction-for-implementation-plan.md. - -The plan has 6 phases: - -1. **✅ ADR** — Done (ADR-005). -2. **✅ Low-level path verified** — `LowLevelToolDefinitionIT` passes. -3. **Ignorance reduction** — 8 open questions to resolve before coding (package placement, annotation design, type mapping, generated code shape, `fromObject()` API, module-info, processor registration, access level requirements). -4. **Implementation** — 6 ordered steps: annotations → schema generator → processor → `fromObject()` → E2E test → reflection fallback (optional). -5. **Documentation** — README updates. -6. **Cross-language port** — Evaluate whether the ergonomic API snapshot needs porting (may not — other SDKs have their own ergonomic paths). - -Key architectural decision captured: **langchain4j's API** (annotations on methods) + **Micronaut's implementation strategy** (JSR 269 compile-time code generation, no `-parameters` flag needed). - -Completed: *Write implementation plan for @CopilotTool ergonomics* (1/6) - -Made changes. - diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/20260618-prompts.md b/1682-java-tool-ergonomics-prompts-remove-before-merge/20260618-prompts.md deleted file mode 100644 index 03a1b5af8..000000000 --- a/1682-java-tool-ergonomics-prompts-remove-before-merge/20260618-prompts.md +++ /dev/null @@ -1,135 +0,0 @@ -### 18:56 Prompt - -Consider `copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md`. - -I am going through the open questions and answering them. - -I am at this one: - -**Open questions:** - -> 1. Should `@CopilotTool` have `@Retention(SOURCE)` (processor-only, like Dagger) or `RUNTIME` (fallback reflection path, like langchain4j)? ADR-005 says "compile-time preferred, runtime fallback" — if we want a fallback path, we need `RUNTIME`. If we commit to processor-only, `SOURCE` is sufficient. -> -> 2. Is `ToolDefer` (the defer config from `ToolDefinition`) needed on the annotation, or is that too niche for v1? -> -> **Recommendation:** Start with `RUNTIME` retention so the reflection fallback works. Defer `ToolDefer` support to a follow-up. - -I need to know what `ToolDefer` is. Make me smart. - -### 19:00 Response - - I’ll trace  ToolDefer  in the SDK and summarize what it does, where it’s used, and why it matters for your annotation design decision. - - ● Search "\bToolDefer\b" 21 lines found - ● Search "defer" in **/*.java (java) 28 lines found - - JV Read ToolDefer.java 69 lines read - JV Read ToolDefinition.java 166 lines read - JV Read ToolDefinitionTest.java 62 lines read - GO Read types.go L1120:1175 (55 lines read) - MD Read README.md L1:260 (155 lines read) - MD Read dd-3018003-ignorance-reduction-for-implementation-plan.md L1:220 (220 lines read) - - ● Search ""defer"|ToolDefer|deferred tools|tool search" (docs, java/docs, java/README.md) No matches found - - MD Read adr-005-tool-definition.md L1:260 (235 lines read) - - ●  ToolDefer  is the per-tool loading policy in  ToolDefinition —it tells Copilot whether to preload a tool definition or allow lazy loading via tool search. - - In this repo it has exactly two values: - - 1.  AUTO  ( "auto" ): tool may be deferred and surfaced through tool search. - 2.  NEVER  ( "never" ): tool is always pre-loaded. - - If unset ( null  / omitted JSON), runtime decides. In Java, this is  com.github.copilot.rpc.ToolDefer  and is wired via  ToolDefinition.createWithDefer(...) ; same concept exists in Go ( Tool.Defer ). - - For your open question: this is an advanced optimization knob (prompt/tool catalog size vs immediate availability), not core ergonomics. So “defer  ToolDefer  support on  @CopilotTool  for v1” is a reasonable cut, as long as low-level  ToolDefinition.createWithDefer(...)  remains available for power users. - - -### 18:59 Prompt - -Now to this one - -> ### 3.3 — `@Param` annotation design - -> **Question:** What attributes does `@Param` need? -> -> ```java -> @Documented -> @Retention(RetentionPolicy.RUNTIME) -> @Target(ElementType.PARAMETER) -> public @interface Param { -> /** Parameter description (sent to the model). */ -> String value() default ""; -> -> /** Parameter name override. Defaults to the actual parameter name. */ -> String name() default ""; -> -> /** Whether this parameter is required. Default true. */ -> boolean required() default true; -> } -> ``` -> -> **Open question:** LangChain4J's `@P` also has `defaultValue()`. Do we need that? Probably not for v1 — the model doesn't use default values; it's for documentation. - -Does the Zod schema that dictates tool definition have a concept of a default value? We can't add one in the Java SDK if it doesn't exist in the lower level Copilot CLI. - -### 19:28 Response - - ● Short answer: you’re not blocked by the lower-level schema. - - The wire-level tool shape treats  parameters  as an open JSON object, not a closed schema with fixed keys: - - • Node RPC type:  Tool.parameters?: { [k: string]: unknown }  ( nodejs/src/generated/rpc.ts ) - • Java generated RPC type:  @JsonProperty("parameters") Map  ( java/src/generated/java/com/github/copilot/generated/rpc/Tool.java ) - • Go type:  Parameters map[string]any  ( go/types.go ) - - So JSON Schema keywords like  default  can flow through. There is just no dedicated first-class  defaultValue  field in  ToolDefinition ; it would have to be emitted into the JSON schema for that parameter ( properties..default ). - - Practically: adding  @Param(defaultValue=...)  is compatible at the transport layer, but likely documentation/hint only unless you also implement SDK/runtime behavior to apply defaults when args are missing. - -### 19:29 Prompt - -Well, of course, if the user specifies a default, then we need to make it so that default does flow through to the tool invocation. Is that implementable? I don't see why not. - -### 19:31 Response - - ● Yes—it’s implementable, and you should do it in the Java SDK layer. - - The CLI/tool wire contract already passes raw argument JSON through; Java can apply defaults before invoking the method. A solid design is: - - 1. Emit  default  into the generated JSON Schema ( properties..default ) for model guidance. - 2. In generated/reflection invocation code, merge defaults into the incoming args map only when a key is absent (not when present with explicit  null , unless you define that behavior). - 3. Then run normal coercion/casting and method invocation. - - Key policy choices to lock down: - - •  required  +  defaultValue : either forbid this combo, or allow it and treat default as satisfying missing input. - • Type parsing:  defaultValue  is annotation text, so processor/reflection path must parse/validate it against param type (fail fast on mismatch). - • Path parity: compile-time generated path and runtime reflection fallback must apply identical defaulting semantics. - - So your instinct is right: no lower-level CLI blocker; it’s a Java API/runtime behavior decision. - -### 19:32 Prompt - -Consider your response. - -> The CLI/tool wire contract already passes raw argument JSON through; Java can apply defaults before invoking the method. A solid design is: -> -> 1. Emit  default  into the generated JSON Schema ( properties..default ) for model guidance. -> 2. In generated/reflection invocation code, merge defaults into the incoming args map only when a key is absent (not when present with explicit  null , unless you define that behavior). -> 3. Then run normal coercion/casting and method invocation. - -> •  required  +  defaultValue : either forbid this combo, or allow it and treat default as satisfying missing input. - -We must forbid this. - -> • Type parsing:  defaultValue  is annotation text, so processor/reflection path must parse/validate it against param type (fail fast on mismatch). - -Yes, we must have this. - -> • Path parity: compile-time generated path and runtime reflection fallback must apply identical defaulting semantics. - -Yes, we must have this. - -Yes, I want to support `defaultValue()` like LangChain4J's `@P`. I am annotating `copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md` as I go. Capture this decision, and the very useful aspects of your previous response and edit `dd-3018003-ignorance-reduction-for-implementation-plan.md` to state this as the Resolution to question 3.3. diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/20260622-prompts.md b/1682-java-tool-ergonomics-prompts-remove-before-merge/20260622-prompts.md deleted file mode 100644 index 5f453e841..000000000 --- a/1682-java-tool-ergonomics-prompts-remove-before-merge/20260622-prompts.md +++ /dev/null @@ -1,1010 +0,0 @@ -### 15:51 Prompt - -#### Context - -Read `copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md` to get the context. - -In this session, we are working on "Phase 3 — Ignorance reduction: questions to answer before writing code". - -We have completed and answered up to, but not including, "3.4 — Type-to-JSON-Schema mapping". - -I will proceed after this prompt. - -### 15:57 Response - -Updated todo list - -Read [](file:///home/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md#1-1), lines 1 to 200 - -Read [](file:///home/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md#200-200), lines 200 to 400 - -I've read the full implementation plan. We're at **Phase 3, section 3.4 — Type-to-JSON-Schema mapping**. The remaining Phase 3 items to resolve are: - -- **3.4** — Type-to-JSON-Schema mapping (compile-time `TypeMirror` → JSON Schema) -- **3.5** — Generated code shape (access levels, return types, argument deserialization) -- **3.6** — `ToolDefinition.fromObject(Object)` registration API -- **3.7** — `module-info.java` impact -- **3.8** — Processor registration - -Ready when you are. - -### 15:59 Prompt - -Consider: "3.4 — Type-to-JSON-Schema mapping" - -> **Question:** What Java types do we need to map to JSON Schema, and how? - -> Minimum viable set (from langchain4j's `JsonSchemaElementUtils`): -> -> | Java type | JSON Schema | -> |-----------|-------------| -> | `String` | `{"type": "string"}` | -> | `int`, `Integer`, `long`, `Long` | `{"type": "integer"}` | -> | `double`, `Double`, `float`, `Float` | `{"type": "number"}` | -> | `boolean`, `Boolean` | `{"type": "boolean"}` | -> | `enum` types | `{"type": "string", "enum": ["V1", "V2", ...]}` | -> | `List`, `Collection` | `{"type": "array", "items": }` | -> | `Map` | `{"type": "object"}` (opaque — no inner schema) | -> | Records / POJOs | `{"type": "object", "properties": {...}, "required": [...]}` | -> - -The entire GitHub Copilot SDK for Java is really a JSON RPC wrapper to communicate with the GitHub Copilot CLI. As such, it already has established user expectations for type mappings. The reasonable answer to question 3.4 is: all of the ones mapped elsewhere in the API. - -You suggested a minimum viable set. Looking at the code in packages `com.github.copilot.rpc` and `com.github.copilot.generated.rpc`, produce the maximum viable set in a similar table format. - -### 16:09 Response - -Updated todo list - -Catalog Java types in rpc packages - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc) - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/generated/java/com/github/copilot/generated/rpc) - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java#1-1), lines 1 to 200 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java) - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolHandler.java#1-1), lines 1 to 50 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolInvocation.java#1-1), lines 1 to 100 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/SessionConfig.java#1-1), lines 1 to 200 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java#200-200), lines 200 to 400 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/SessionConfig.java#200-200), lines 200 to 700 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolInvocation.java#100-100), lines 100 to 172 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/SessionConfig.java#700-700), lines 700 to 1200 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java#400-400), lines 400 to 600 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/SessionConfig.java#1200-1200), lines 1200 to 1700 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java#600-600), lines 600 to 700 - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && find java/src/main/java/com/github/copilot/rpc/ -name "*.java" -exec wc -l {} + | sort -n | tail -5 - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rn "private\|public\|protected" java/src/main/java/com/github/copilot/rpc/*.java | grep -E "(private|public|protected)\s+(static\s+)?(final\s+)?\S+\s+\w+" | grep -v "return\|this\|void\|class\|interface\|enum\|record\|package\|import\|@\|//\|/\*\|\*" | grep -oP '(private|public|protected)\s+(static\s+)?(final\s+)?(\S+(<[^>]+>)?)\s+\w+' | grep -oP '(private|public|protected)\s+(static\s+)?(final\s+)?\K\S+(<[^>]+>)?' | sort -u - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rn "CompletableFuture" java/src/main/java/com/github/copilot/rpc/*.java | grep -oP 'CompletableFuture<[^>]+>' | sort -u - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rn "Map<" java/src/main/java/com/github/copilot/rpc/*.java | grep -oP 'Map<[^>]+>' | sort -u - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rl "^public enum\|^public record\|^public class\|^public interface\|^public final class\|^public sealed\|^public abstract" java/src/main/java/com/github/copilot/rpc/*.java | xargs grep -l "^public enum" 2>/dev/null - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rl "^public record" java/src/main/java/com/github/copilot/rpc/*.java - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rl "^public interface\|^public @FunctionalInterface\|@FunctionalInterface" java/src/main/java/com/github/copilot/rpc/*.java - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && for f in ProviderConfig NamedProviderConfig ProviderModelConfig ModelInfo ModelCapabilitiesOverride ModelCapabilities ModelSupports ModelLimits ModelVisionLimits ModelBilling ModelPolicy McpServerConfig McpStdioServerConfig McpHttpServerConfig CustomAgentConfig DefaultAgentConfig InfiniteSessionConfig SystemMessageConfig MemoryConfiguration LargeToolOutputConfig TelemetryConfig; do echo "=== $f ==="; grep -n "private\|public record\|public class\|public enum\|public sealed" java/src/main/java/com/github/copilot/rpc/${f}.java 2>/dev/null | head -30; done - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && for f in SessionHooks CommandDefinition CommandContext CommandWireDefinition ElicitationSchema ElicitationContext ElicitationParams ElicitationResult SectionOverride PermissionInvocation PermissionRequest PermissionRequestResult PermissionRequestResultKind UserInputRequest UserInputResponse UserInputInvocation AutoModeSwitchInvocation AutoModeSwitchRequest ExitPlanModeInvocation ExitPlanModeRequest ExitPlanModeResult HookInvocation PreToolUseHookInput PostToolUseHookInput PostToolUseFailureHookInput PreMcpToolCallHookInput SessionStartHookInput SessionEndHookInput Attachment BlobAttachment MessageAttachment InputOptions; do echo "=== $f ==="; grep -n "private\|public record\|public class\|public enum\|public sealed" java/src/main/java/com/github/copilot/rpc/${f}.java 2>/dev/null | head -20; done - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && for f in AzureOptions ModelBillingTokenPrices ToolBinaryResult ToolResultObject ToolSet BuiltInTools SessionCapabilities SessionContext SendMessageRequest SendMessageResponse MessageOptions CreateSessionRequest CreateSessionResponse ResumeSessionRequest ResumeSessionConfig ResumeSessionResponse AgentInfo SessionLifecycleEvent SessionLifecycleEventTypes SessionLifecycleEventMetadata SessionListFilter SessionMetadata GetAuthStatusResponse GetStatusResponse GetModelsResponse GetMessagesResponse PingResponse CloudSessionOptions CloudSessionRepository SessionUiApi SessionUiCapabilities; do echo "=== $f ==="; grep -n "private\|public record\|public class\|public enum\|public sealed\|public interface" java/src/main/java/com/github/copilot/rpc/${f}.java 2>/dev/null | head -30; done - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && for f in PermissionRequestResultKind PostToolUseFailureHookOutput PreMcpToolCallHookOutput PostToolUseHookOutput PreToolUseHookOutput SessionStartHookOutput SessionEndHookOutput UserPromptSubmittedHookInput UserPromptSubmittedHookOutput JsonRpcError JsonRpcRequest JsonRpcResponse ListSessionsResponse DeleteSessionResponse GetForegroundSessionResponse GetLastSessionIdResponse GetSessionMetadataResponse SetForegroundSessionRequest SetForegroundSessionResponse; do echo "=== $f ==="; head -50 java/src/main/java/com/github/copilot/rpc/${f}.java 2>/dev/null | grep -E "private |public record|public class|JsonProperty"; done - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/SendMessageResponse.java) - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/SessionUiApi.java) - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/CreateSessionResponse.java) - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ModelBilling.java) - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && find java/src/generated/java/com/github/copilot/generated/rpc/ -name "*.java" | wc -l - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rn "private\|public record\|public enum\|public class\|public interface\|public sealed" java/src/generated/java/com/github/copilot/generated/rpc/*.java | grep -E "^\S+:\d+:\s+(private|public)\s+" | grep -oP '(private|public)\s+(static\s+)?(final\s+)?(\S+(<[^>]+>)?)\s+\w+' | grep -oP '(private|public)\s+(static\s+)?(final\s+)?\K\S+(<[^>]+>)?' | sort -u - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "private " java/src/generated/java/com/github/copilot/generated/rpc/*.java | sed 's/^[[:space:]]*//' | grep "^private " | sed 's/private //' | sed 's/ [a-zA-Z_][a-zA-Z0-9_]*;.*//' | sed 's/ [a-zA-Z_][a-zA-Z0-9_]* =.*//' | sort -u - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rPoh '(?<=private\s)\S+(?:\s*<[^>]+>)?' java/src/generated/java/com/github/copilot/generated/rpc/*.java | sort -u - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/generated/java/com/github/copilot/generated/rpc/SessionSendParams.java) - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java#1-1), lines 1 to 80 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/generated/java/com/github/copilot/generated/rpc/Model.java) - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java#1-1), lines 1 to 80 - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "^public enum" java/src/generated/java/com/github/copilot/generated/rpc/*.java | sed 's/public enum //' | sed 's/ {.*//' | sort -u - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "^public record\|^public class\|^public interface\|^public sealed\|^public final class" java/src/generated/java/com/github/copilot/generated/rpc/*.java | sed 's/public record //' | sed 's/public class //' | sed 's/public interface //' | sed 's/public sealed interface //' | sed 's/public final class //' | sed 's/(.*//' | sed 's/ {.*//' | sed 's/ extends.*//' | sed 's/ implements.*//' | sort -u - -Read [](file:///home/edburns/.vscode-server-insiders/data/User/workspaceStorage/0a47833b2fec2df1980b294571e4f51d/GitHub.copilot-chat/chat-session-resources/e01ff3e8-22ab-4df6-878d-6311894eaf0a/toolu_vrtx_011124MhaCgPR2gvRHwJ4Ndy__vscode-1782158068142/content.txt#1-1), lines 1 to 300 - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "^public record" java/src/generated/java/com/github/copilot/generated/rpc/*.java | grep -oP '\b\w+(?:<[^>]+>)?' | grep -v "^public\|^record\|^@\|^String\|^void\|^JsonProperty\|^JsonInclude\|^JsonIgnoreProperties\|^Generated\|^CopilotExperimental" | sort -u | head -80 - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rPoh '(?<=@JsonProperty\("[^"]*"\)\s)\S+(?:<[^>]+>)?' java/src/generated/java/com/github/copilot/generated/rpc/*.java | sort -u - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "@JsonProperty" java/src/generated/java/com/github/copilot/generated/rpc/*.java | grep -oP '@JsonProperty\("[^"]*"\)\s+\K\S+' | sort -u - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "Map<" java/src/generated/java/com/github/copilot/generated/rpc/*.java | grep -oP 'Map<[^>]+>' | sort -u - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "CompletableFuture<" java/src/generated/java/com/github/copilot/generated/rpc/*.java | grep -oP 'CompletableFuture<[^>]+>' | sort -u - -Read [](file:///home/edburns/.vscode-server-insiders/data/User/workspaceStorage/0a47833b2fec2df1980b294571e4f51d/GitHub.copilot-chat/chat-session-resources/e01ff3e8-22ab-4df6-878d-6311894eaf0a/toolu_vrtx_011q19D6rVhGooBNSoryk2X8__vscode-1782158068148/content.txt) - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "import java\.\|import com.fasterxml\|import com.github" java/src/generated/java/com/github/copilot/generated/rpc/*.java | grep -v "^import com.github.copilot.generated.rpc\.\|^import com.github.copilot.CopilotExperimental" | sort -u - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "import java\.\|import com.fasterxml\|import com.github" java/src/main/java/com/github/copilot/rpc/*.java | grep -v "^import com.github.copilot.rpc\.\|^import com.github.copilot.CopilotExperimental\|^import com.github.copilot.SystemMessageMode\|^import com.github.copilot.generated.SessionEvent" | sort -u - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "sealed\|@JsonSubTypes\|@JsonTypeInfo" java/src/generated/java/com/github/copilot/generated/rpc/*.java | head -30 - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rl "sealed" java/src/generated/java/com/github/copilot/generated/rpc/*.java - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rn "sealed" java/src/main/java/com/github/copilot/rpc/*.java - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -n "private\|public class\|public record" java/src/main/java/com/github/copilot/rpc/SystemMessageSections.java java/src/main/java/com/github/copilot/rpc/SystemPromptSections.java java/src/main/java/com/github/copilot/rpc/SystemMessageConfig.java | head -30 - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rn "int\|long\|double\|boolean\|float\|byte\|short\|char" java/src/generated/java/com/github/copilot/generated/rpc/*.java | grep "@JsonProperty" | grep -oP '@JsonProperty\("[^"]*"\)\s+\K(int|long|double|boolean|float|byte|short|char)\b' | sort -u - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rn "int\b\|long\b\|double\b\|boolean\b" java/src/main/java/com/github/copilot/rpc/*.java | grep "private " | grep -oP 'private\s+(int|long|double|boolean)\b' | sort | uniq -c | sort -rn - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rn "Consumer<\|Function<\|Supplier<\|Executor\b" java/src/main/java/com/github/copilot/rpc/*.java | grep "private\|public" | grep -oP '(Consumer<[^>]+>|Function<[^>]+>|Supplier<[^>]+>|Executor)\b' | sort -u - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "import com.github.copilot" java/src/main/java/com/github/copilot/rpc/*.java | grep -v "^import com.github.copilot.rpc\.\|^import com.github.copilot.generated.rpc\." | sort -u - -Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rn "SessionMetadataContextInfoResultContextInfo\|SessionMetadataSnapshotResultWorkspace\|SessionWorkspacesGetWorkspaceResultWorkspace\|SessionWorkspacesSaveLargePasteResultSaved\|SessionToolsUpdateSubagentSettingsParamsSubagents" java/src/generated/java/com/github/copilot/generated/rpc/*.java | head -10 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/McpServerConfig.java#1-1), lines 1 to 60 - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/Attachment.java) - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolResultObject.java#1-1), lines 1 to 60 - -Read [](file:///home/edburns/.vscode-server-insiders/data/User/workspaceStorage/0a47833b2fec2df1980b294571e4f51d/GitHub.copilot-chat/chat-session-resources/e01ff3e8-22ab-4df6-878d-6311894eaf0a/toolu_vrtx_01UxE8XUJogukPbL4Wd7rWHr__vscode-1782158068103/content.txt) - -Here's the **maximum viable set** derived from every type actually used across `com.github.copilot.rpc` and `com.github.copilot.generated.rpc`: - -| Java type | JSON Schema | SDK usage examples | -|-----------|-------------|-------------------| -| `String` | `{"type": "string"}` | Pervasive (IDs, names, paths, messages) | -| `int`, `Integer` | `{"type": "integer"}` | `ProviderConfig`, `ModelLimits`, `McpServerConfig` | -| `long`, `Long` | `{"type": "integer"}` | Timestamps in hook inputs, JSON-RPC IDs | -| `double`, `Double` | `{"type": "number"}` | `ModelBilling.multiplier`, `InfiniteSessionConfig` thresholds | -| `float`, `Float` | `{"type": "number"}` | Not used in SDK today, but natural complement | -| `boolean`, `Boolean` | `{"type": "boolean"}` | Extensively in `SessionConfig`, hook outputs, options | -| `String[]` | `{"type": "array", "items": {"type": "string"}}` | `CopilotClientOptions.cliArgs` | -| `enum` types | `{"type": "string", "enum": ["V1", ...]}` | 92 enums (`ToolDefer`, `AgentMode`, 85 generated) | -| `UUID` | `{"type": "string", "format": "uuid"}` | Generated record ID fields | -| `OffsetDateTime` | `{"type": "string", "format": "date-time"}` | Generated record timestamp fields | -| `JsonNode` | `{}` (any) | `ToolInvocation.argumentsNode`, hook args/results | -| `Object` | `{}` (any) | `ToolDefinition.parameters`, `JsonRpcRequest.params` | -| `List`, `Collection` | `{"type": "array", "items": }` | 50+ distinct `T` across the API | -| `Map` | `{"type": "object", "additionalProperties": {"type": "string"}}` | `ProviderConfig.headers`, env maps | -| `Map` | `{"type": "object", "additionalProperties": {"type": "boolean"}}` | Generated records | -| `Map` | `{"type": "object", "additionalProperties": {"type": "integer"}}` | Generated records | -| `Map` | `{"type": "object"}` (opaque) | `ToolInvocation.getArguments()`, telemetry, extensionData | -| `Map` (typed `T`) | `{"type": "object", "additionalProperties": }` | `Map`, `Map`, etc. | -| `Map>` | `{"type": "object", "additionalProperties": {"type": "array", "items": {"type": "string"}}}` | Generated records | -| Records / POJOs | `{"type": "object", "properties": {...}, "required": [...]}` | ~690 types (26 handwritten records, ~65 POJOs, ~600 generated) | -| Sealed / `@JsonSubTypes` | `{"oneOf": [...]}` with discriminator | `McpServerConfig`, `MessageAttachment`, `SlashCommandInvocationResult` | -| `Optional` | Schema of `T`, not in `required` array | `Optional` in `SessionConfig` | -| `OptionalInt` | `{"type": "integer"}`, not in `required` | `CopilotClientOptions.getSessionIdleTimeoutSeconds()` | -| `OptionalDouble` | `{"type": "number"}`, not in `required` | `ModelBilling.getMultiplierOpt()` | - -**Types intentionally excluded** (not sensible as tool parameter types): - -| Java type | Reason | -|-----------|--------| -| `CompletableFuture` | Return type only, not a parameter type | -| `Consumer`, `Supplier`, `Function`, `Executor` | Callback/functional types, not serializable | -| `ObjectMapper`, `TypeReference` | Infrastructure, not data | - -The minimum viable set from the plan covers 8 rows. This maximum viable set covers **23 rows** — adding `String[]`, `UUID`, `OffsetDateTime`, `JsonNode`/`Object` (any), typed `Map` variants, sealed/polymorphic types, and `Optional` variants. These all have established Jackson serialization semantics in the existing SDK. - -Created 2 todos - -### 16:11 Prompt - -Now we come to "3.5 — Generated code shape". - -> **Question:** What exactly does the processor generate? - -This is why, before we started even creating the phase plan, we created a test of record that shows the current, non-ergonomic, way works and is functional. - -In the context of question 3.5, look at known-passing test `java/src/test/java/com/github/copilot/LowLevelToolDefinitionIT.java`. Note that it defines these tools - -- `setPhaseTool` -- `searchTool` -- `grepOverrideTool` - -Compare that known-passing code with what you grabbed from ADR-005: - -> ```java -> // GENERATED — do not edit -> final class MyTools$$CopilotToolMeta { -> static List definitions(MyTools instance) { -> return List.of( -> new ToolDefinition("set_current_phase", "Sets the current phase", -> Map.of("type", "object", -> "properties", Map.of("phase", Map.of("type", "string", -> "description", "The phase to transition to")), -> "required", List.of("phase")), -> invocation -> { -> String phase = (String) invocation.getArguments().get("phase"); -> return CompletableFuture.completedFuture( -> instance.setCurrentPhase(phase)); -> }, null, null, null) -> ); -> } -> } -> ``` - -And then ask yourself the questions from the plan - -> 1. **Method invocation in generated code:** The generated lambda calls `instance.setCurrentPhase(phase)` directly — no reflection at runtime. But this requires the method to be accessible (not private). What access levels do we support? langchain4j uses `method.setAccessible(true)` at runtime. We'd need to either: -> - Require `public` or package-private methods, OR -> - Generate a helper that uses `MethodHandles.Lookup` to access private methods (complex), OR -> - Just require non-private. - -> 2. **Return type handling:** What does the generated code do with the method's return value? - -> 3. **Argument deserialization in generated code:** How does the generated lambda extract and coerce arguments? - -Does the known-passing test code even ask these questions? Does it answer them? How? - -### 16:18 Response - -Updated todo list - -Read [](file:///home/edburns/workareas/copilot-sdk/java/src/test/java/com/github/copilot/LowLevelToolDefinitionIT.java) - -The known-passing test **doesn't ask** these questions — it **bypasses** them entirely. Here's why: - ---- - -**Q1 (Access levels):** Not applicable. The test uses inline lambdas, not method references: - -```java -invocation -> { - currentPhase = args.phase(); - return CompletableFuture.completedFuture("Phase set to " + currentPhase); -} -``` - -There's no method to invoke — the tool logic IS the lambda. The access-level question only arises when a processor needs to generate code that calls `instance.someMethod()`. - ---- - -**Q2 (Return type handling):** Already resolved by fiat. Every handler explicitly returns `CompletableFuture`: - -```java -return CompletableFuture.completedFuture("Phase set to " + currentPhase); -return CompletableFuture.completedFuture("Found: item_alpha, item_beta"); -return CompletableFuture.completedFuture("CUSTOM_GREP: " + query); -``` - -The low-level API's handler type signature forces the user to produce `CompletableFuture` (or `Object`). No wrapping, no serialization — that's the user's problem. - ---- - -**Q3 (Argument deserialization):** Answered — it shows **both** patterns that the generated code would need: - -| Pattern | Test usage | -|---------|-----------| -| Jackson deserialization to a record | `invocation.getArgumentsAs(PhaseArgs.class)` for `setPhaseTool` | -| Direct map access + cast | `(String) invocation.getArguments().get("keyword")` for `searchTool` and `grepOverrideTool` | - ---- - -**Summary:** The low-level test is the **specification** of what the generated code must emit — it shows the exact lambdas the processor must synthesize. The test proves the wire format works. The three questions from 3.5 are about automating what the test author does manually: - -1. The processor must generate `instance.method()` calls → needs at least package-private access -2. The processor must wrap arbitrary return types into `CompletableFuture` → needs the return-type table -3. The processor must generate either `getArgumentsAs(Record.class)` or `getArguments().get("name")` + cast → depending on parameter complexity - -The test doesn't need to answer these questions because the human already made all three decisions by hand-writing the lambdas. - -### 16:19 Prompt - -I have been going through the plan `copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md` and filling in **Resolution** for each question. Absorb what we learned and output what I should record as the **Resolution** for question 3.5. - -### 16:21 Response - -Updated todo list - -**Resolution:** - -The generated `$$CopilotToolMeta` class produces lambdas identical to what `LowLevelToolDefinitionIT` writes by hand. Decisions: - -1. **Access levels:** Require at least package-private. The generated `$$CopilotToolMeta` lives in the same package as the user's class, so package-private and above work without reflection. Emit a compile error for `private` methods annotated with `@CopilotTool`. - -2. **Return type handling:** - - | Return type | Generated code | - |-------------|----------------| - | `String` | `CompletableFuture.completedFuture(instance.method(...))` | - | `CompletableFuture` | `instance.method(...)` (use as-is) | - | `void` | `instance.method(...); return CompletableFuture.completedFuture("Success")` | - | `CompletableFuture` (non-String) | `instance.method(...).thenApply(objectMapper::writeValueAsString)` | - | Other `T` | `CompletableFuture.completedFuture(objectMapper.writeValueAsString(instance.method(...)))` | - -3. **Argument deserialization:** Follow the two patterns proven in `LowLevelToolDefinitionIT`: - - **Simple types** (`String`, primitives, boxed): generate direct cast from `invocation.getArguments().get("name")` (e.g., `(String) args.get("city")`, `((Number) args.get("count")).intValue()`). - - **Complex types** (enums, records, POJOs): generate `objectMapper.convertValue(invocation.getArguments().get("name"), TargetType.class)`. - - **Single-record-parameter shortcut:** When a method has exactly one parameter that is a record/POJO (matching the full argument set), generate `invocation.getArgumentsAs(RecordType.class)` — the same pattern `LowLevelToolDefinitionIT.setPhaseTool` uses. - - The `ObjectMapper` instance: use a `private static final` field in the generated `$$CopilotToolMeta` class, initialized from `com.github.copilot.rpc.RpcMapper.INSTANCE` (already exists in the SDK). - -### 16:37 Prompt - -Read `copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md` to get the context. - -In this session, we are working on "Phase 3 — Ignorance reduction: questions to answer before writing code". - -We have completed and answered up to, but not including, "3.7 — `module-info.java` impact". - -I will proceed after this prompt. - -### 16:40 Response - -Updated todo list - -Read [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md#1-1), lines 1 to 300 - -Read [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md#300-300), lines 300 to 491 - -I've read the full document. Here's my understanding of where we stand: - -**Phase 3 — completed items (3.1–3.6):** -- **3.1** — Option B: new `com.github.copilot.tool` package for annotations + processor + schema generation; `ToolDefinition` stays in `rpc`. -- **3.2** — `@CopilotTool` with `RUNTIME` retention; include `ToolDefer` support. -- **3.3** — `@Param` with `defaultValue()` that is behaviorally effective; `required=true` + non-empty `defaultValue` is a compile-time error. -- **3.4** — Maximum viable type set (23 rows) including `Optional`, sealed/`@JsonSubTypes`, typed `Map` variants, `UUID`, `OffsetDateTime`, etc. -- **3.5** — Generated `$$CopilotToolMeta` class; package-private minimum access; direct casts for simple types, `ObjectMapper.convertValue()` for complex types; `RpcMapper.INSTANCE` for the `ObjectMapper`. -- **3.6** — Processor-only approach (no reflection fallback). - -**Remaining items:** -- **3.7** — `module-info.java` impact (answer is drafted: no special exports needed since generated class is in user's package, but needs verification) -- **3.8** — Processor registration (add to `META-INF/services` + `module-info.java` `provides` clause) - -Ready for your next instruction. - -## dd-3021192-module-info.java-impact - -### 16:40 Prompt - -Create a self-contained Maven module rooted at `1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/pom.xml`. The `artifactId` is `dd3021192` and the `groupId` is `com.github`. It depends on - -```xml - com.github - copilot-sdk-java - 1.0.2-SNAPSHOT -``` - -It creates a JAR that can be run with java -jar . This is the "simple named-module test." mentioned in "3.7 — `module-info.java` impact". - -When you run the jar, it demonstrates that this works. - -### 17:02 Response - -Updated todo list - -Searched for text `module com.github.copilot` (`**/java/src/main/java/module-info.java`), 1 result - -Read [](file:///Users/edburns/workareas/copilot-sdk/java/src/main/java/module-info.java) - -Now I have the module name. Let me create the test module. The key idea: simulate what the processor will generate (`$$CopilotToolMeta`) and prove that `Class.forName()` can find it from within a named JPMS module. - -Created [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/pom.xml) - -Created [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/module-info.java) - -Created [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/MyTools.java) - -Created 5 todos - -Created [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/MyTools%24%24CopilotToolMeta.java) - -Created [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/Main.java) - -Now let me build and run it. First, I need to ensure `copilot-sdk-java` is installed in the local Maven repo, then build the test module. - -Ran terminal command: export JAVA_HOME="/Library/Java/JavaVirtualMachines/microsoft-25.jdk/Contents/Home" && export M2_HOME=${HOME}/Downloads/apache-maven-3.9.8 && export PATH=${M2_HOME}/bin:${JAVA_HOME}/bin:${PATH} && cd /Users/edburns/workareas/copilot-sdk/java && LOG="$(date +%Y%m%d-%H%M)-job-logs.txt" && mvn install -DskipTests -pl . > "$LOG" 2>&1 & tail -f "$LOG" - -Made changes. - -### 17:46 Prompt - -Read `copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md` (the plan) to get the context. - -✅✅Pay very close attention to each **Resolution:** to the questions in `Phase 3 — Ignorance reduction: questions to answer before writing code`. - -In this session, we are creating issues of type **Task** as children of Epic https://github.com/github/copilot-sdk/issues/1682 . These issues correspond to the sub-sections in `## Phase 4 — Implementation (the build order)`. - -I want you to use `gh` to create issues as sub-issues of 1682. - -- I will dispatch these issues by assigning them to an agent. Leave them unassigned for now. - -- The description you put in the issues you create is the specification for the work the agent assigned to the issue must do. Therefore, you need to be very specific and targeted when writing the description. - -- Set the type to **Task**. - -- Each issue will be worked on and resolved in serial. You can assume: - - - All issues will operate in this topic branch of `upstream`: `edburns/1682-java-tool-ergonomics`. - - - The assignee agent has access to the topic branch and, most importantly, to the prompts in the directory `1682-java-tool-ergonomics-prompts-remove-before-merge`, which includes the plan. Therefore, you can instruct the agent to access the plan by reference, you don't need to reference it by value directly in the issue. - -- The sub-sections of `## Phase 4 — Implementation (the build order)` have essential details for each corresponding **Task**. - -- Instruct the agent to read the entire plan before working. - -- Instruct the agent to carefully re-read the relevant sections of the of the plan. ❌❌You can't just say, "read the relevant sections of the plan". ✅✅In the description of the issue, list exactly what sections of the plan apply to this particular issue. - -- Though the sub-sections of `## Phase 4 — Implementation (the build order)` have essential details for each corresponding **Task**, I want you to carefully consider if there additional gating tests you can add so that as the agent proceeds from one task to the next, the likelihood of rework is reduced. - -### 18:16 Response - -Updated todo list - -Read [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md#1-1), lines 1 to 100 - -Read [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md#101-101), lines 101 to 200 - -Read [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md#201-201), lines 201 to 300 - -Read [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md#301-301), lines 301 to 400 - -Read [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md#401-401), lines 401 to 500 - -Read [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md#501-501), lines 501 to 505 - -Now I have the full plan. Let me create the 5 issues (4.1–4.5, skipping 4.6 which is deferred) as sub-issues of #1682. I'll write detailed descriptions with gating tests. - -Ran terminal command: gh issue create --help 2>&1 | head -40 - -`gh issue create` supports `--parent` and `--type` directly. I'll create each issue with a body file. - -Created [](file:///tmp/issue-4.1-body.md) - -Created [](file:///tmp/issue-4.2-body.md) - -Created [](file:///tmp/issue-4.3-body.md) - -Created 5 todos - -Created [](file:///tmp/issue-4.4-body.md) - -Created [](file:///tmp/issue-4.5-body.md) - -Now I'll create the issues using `gh`: - -Ran terminal command: cd /Users/edburns/workareas/copilot-sdk && gh issue create \ - --repo github/copilot-sdk \ - --title "[Java] @CopilotTool ergonomics 4.1: Create @CopilotTool and @Param annotations" \ - --body-file /tmp/issue-4.1-body.md \ - --type "Task" \ - --parent 1682 - -Ran terminal command: cd /Users/edburns/workareas/copilot-sdk && gh issue create \ - --repo github/copilot-sdk \ - --title "[Java] @CopilotTool ergonomics 4.2: Schema generation utility (TypeMirror to JSON Schema)" \ - --body-file /tmp/issue-4.2-body.md \ - --type "Task" \ - --parent 1682 - -Ran terminal command: cd /Users/edburns/workareas/copilot-sdk && gh issue create \ - --repo github/copilot-sdk \ - --title "[Java] @CopilotTool ergonomics 4.3: Annotation processor (CopilotToolProcessor)" \ - --body-file /tmp/issue-4.3-body.md \ - --type "Task" \ - --parent 1682 - -Ran terminal command: cd /Users/edburns/workareas/copilot-sdk && gh issue create \ - --repo github/copilot-sdk \ - --title "[Java] @CopilotTool ergonomics 4.4: ToolDefinition.fromObject(Object) registration API" \ - --body-file /tmp/issue-4.4-body.md \ - --type "Task" \ - --parent 1682 - -Ran terminal command: cd /Users/edburns/workareas/copilot-sdk && gh issue create \ - --repo github/copilot-sdk \ - --title "[Java] @CopilotTool ergonomics 4.5: E2E integration test with replay proxy" \ - --body-file /tmp/issue-4.5-body.md \ - --type "Task" \ - --parent 1682 - -All 5 issues created as sub-issues of #1682: - -| Phase | Issue | Title | -|-------|-------|-------| -| 4.1 | [#1758](https://github.com/github/copilot-sdk/issues/1758) | Create `@CopilotTool` and `@Param` annotations | -| 4.2 | [#1759](https://github.com/github/copilot-sdk/issues/1759) | Schema generation utility (TypeMirror → JSON Schema) | -| 4.3 | [#1760](https://github.com/github/copilot-sdk/issues/1760) | Annotation processor (`CopilotToolProcessor`) | -| 4.4 | [#1761](https://github.com/github/copilot-sdk/issues/1761) | `ToolDefinition.fromObject(Object)` registration API | -| 4.5 | [#1762](https://github.com/github/copilot-sdk/issues/1762) | E2E integration test with replay proxy | - -Each issue includes: -- Explicit list of plan sections to re-read (by section number and title) -- Instruction to read the full plan before starting -- Detailed gating tests beyond what the plan specifies (e.g., reflection-based annotation attribute verification in 4.1, handler invocation tests in 4.4) -- Clear prerequisites (serial dependency chain) -- Branch and constraint information - -Updated todo list - -Made changes. - -### 18:37 Prompt - -Consider this text from the plan - --------- - -**Question:** What attributes does `@CopilotTool` need? - -Based on ADR-005 and the C#/langchain4j comparisons: - -```java -@Documented -@Retention(RetentionPolicy.SOURCE) // only needed at compile time for processor -@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; -} -``` - - ------- - -I'm concerned about relying on snake_case here. That's a Python thing. Java uses camelCase. - -What would be the changes to the overall design if we said camelCase instead of snake_case? - -### 18:40 Prompt - -Updated todo list - -Let me check what naming convention the existing SDK and wire protocol actually use for tool names. - -Searched for regex `tool.*name` (`**/test/snapshots/tools/*.yaml`), no results - -Searched for regex `snake_case|snakeCase|toSnakeCase|to_snake` (`**/java/**`), 9 results - -Searched for regex `DefineTool|define_tool|defineTool` (`**/nodejs/src/**`), 4 results - -Read [](file:///Users/edburns/workareas/copilot-sdk/test/snapshots/tools/low_level_tool_definition.yaml) - -Searched for regex `DefineTool|tool.*name` (`**/go/*.go`), 10 results - -Searched for regex `set_current_phase|search_items|tool.*name` (`**/java/src/test/**/*LowLevel*"`), no results - -Searched for regex `set_current_phase|search_items` (`**/java/src/test/**"`), no results - -Searched for regex `tool.*name.*snake|tool.*name.*camel` (`**/nodejs/test/**`), no results - -Searched for regex `set_current_phase|search_items` (`**/java/src/test/**`), 2 results - -Searched for regex `defineTool.*name` (`**/nodejs/samples/**`), no results - -Searched for regex `Name:` (`**/go/samples/**`), 1 result - -Searched for regex `defineTool` (`**/nodejs/examples/**`), 2 results - -Searched for regex `DefineTool|tool.*name` (`**/dotnet/samples/**`), 2 results - -Here's the analysis: - -## The wire protocol uses `snake_case` — it's not just a Python thing - -Looking at the evidence: - -1. **Replay snapshot** (low_level_tool_definition.yaml): `set_current_phase`, `search_items` -2. **Node.js examples**: `defineTool("lookup_fact", ...)` -3. **Go samples**: `Name: "manual_resume_status"` -4. **.NET samples**: `"manual_resume_status"` -5. **Java existing test**: `ToolDefinition.create("set_current_phase", ...)` - -Every SDK across all languages uses `snake_case` for tool names on the wire. This is the **Copilot CLI / model-facing convention** — it's how the LLM identifies and calls tools in function-calling. It's not language-internal naming; it's a wire protocol identifier. - -## Impact of switching to `camelCase` - -If you changed the default conversion to camelCase (or just identity — since Java methods are already camelCase): - -| Aspect | Impact | -|--------|--------| -| **Wire compatibility** | Tool names like `setCurrentPhase` instead of `set_current_phase`. The model still works (it's just a string), but diverges from every other SDK's convention. | -| **Cross-SDK consistency** | A tool that's `set_current_phase` in Node/Go/.NET/Python/Rust would be `setCurrentPhase` in Java — confusing for multi-SDK teams. | -| **Snapshot reuse** | Can't share the same test snapshots across SDKs (model expects specific tool names in responses). | -| **Code change** | Actually simplifies the processor — no case conversion logic needed (identity transform). | -| **Override tools** | Built-in Copilot tools use `snake_case` (like `grep`). Overriding them already requires explicit `name="grep"`, so no impact there. | - -## Recommendation - -Keep `snake_case` as the **default** wire name derivation. The rationale is protocol-level consistency, not Java style: - -- The Java **method** stays camelCase (`setCurrentPhase`) — idiomatic Java. -- The **tool name** sent to the model is `set_current_phase` — idiomatic function-calling protocol. -- Users can always override with `@CopilotTool(name="whateverTheyWant")`. - -This is the same pattern Jackson uses: Java fields are camelCase, but `@JsonProperty` / `PropertyNamingStrategies` controls the wire format. The wire convention is independent of the source language. - -If you still prefer dropping the conversion, the only code changes would be: -1. Remove the snake_case converter from the processor (4.3). -2. Update the test in 4.3 that verifies `setCurrentPhase` → `set_current_phase`. -3. Update the E2E snapshot/test (4.5) tool names. - -### 18:41 Prompt - -Look at what the agent changed in commit 8b899e15254b2b539ae585c85d1e7c39ef864194 to `java/src/main/java/com/github/copilot/rpc/ToolDefer.java`. It added NONE. - -Is there a precedent for this NONE thing in the other languages? - -### 18:43 Response - -Updated todo list - -Ran terminal command: git show 8b899e15254b2b539ae585c85d1e7c39ef864194 -- java/src/main/java/com/github/copilot/rpc/ToolDefer.java - -Searched for regex `ToolDefer|toolDefer|tool_defer|defer` (`**/nodejs/src/**`), 20 results - -Searched for regex `ToolDefer|Defer` (`**/go/**/*.go`), 20 results - -Searched for regex `ToolDefer|Defer` (`**/dotnet/src/**`), 20 results - -Searched for regex `CopilotToolDefer` (`**/dotnet/src/**`), 8 results - -Searched for regex `defer` (`**/nodejs/src/types.ts`), 4 results - -Read [](file:///Users/edburns/workareas/copilot-sdk/dotnet/src/CopilotTool.cs#155-155), lines 155 to 170 - -Read [](file:///Users/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefer.java#1-1), lines 1 to 10 - -Read [](file:///Users/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefer.java#20-20), lines 20 to 50 - -**No, there is no precedent for `NONE` in the other languages.** - -- **.NET**: `CopilotToolDefer` has only `Auto` and `Never`. The field is `CopilotToolDefer?` (nullable) — absence is expressed as `null`. -- **Node.js**: `defer?: "auto" | "never"` — absence is expressed as `undefined` (the field is optional). -- **Go**: Would use a pointer or zero value. - -Every other SDK uses **nullability/optionality** to represent "no defer preference." The agent invented `NONE("")` to solve a Java annotation limitation: annotation attributes can't be `null`, so a sentinel value is needed for the default. - -This is a reasonable Java-specific workaround (annotations require compile-time constants), but it introduces a concern: `NONE("")` will serialize as `""` via `@JsonValue` if it's ever accidentally included in a JSON-RPC message. The processor/`fromObject()` code must treat `NONE` as equivalent to `null` (i.e., don't pass it to `ToolDefinition.create()`). You'll want to verify that gate exists in task 4.3/4.4. - -### 18:45 Prompt - -Let's take .NET. What is the behavior of the tool in the "absence" case? And what does that serialize as? - -### 18:47 Response - - -Updated todo list - -Read [](file:///Users/edburns/workareas/copilot-sdk/dotnet/src/CopilotTool.cs#140-140), lines 140 to 160 - -Read [](file:///Users/edburns/workareas/copilot-sdk/dotnet/src/Client.cs#2415-2415), lines 2415 to 2450 - -Now let me see how `ToolDefinition` serializes when `Defer` is null: - -Searched for regex `ToolDefinition.*serial|WriteJson|JsonSerializ|toolDefinition.*json|RegisterTools|registerTools` (`**/dotnet/src/Client.cs`), 20 results - -Read [](file:///Users/edburns/workareas/copilot-sdk/dotnet/src/Client.cs#2117-2117), lines 2117 to 2130 - -There's the answer: - -**In .NET, when `Defer` is `null`, the key is simply omitted from the JSON.** - -The serializer uses `DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull`, so any `null`-valued property (like `Defer`, `OverridesBuiltInTool`, `SkipPermission`) is not written to the wire at all. The CLI receives a `ToolDefinition` JSON object with no `defer` key — meaning "no preference, use server default." - -So the wire behavior for "absence" is: **the `defer` field does not appear in the JSON-RPC message**. - ---- - -For Java's `NONE("")`: if Jackson serializes it, it would emit `"defer": ""` — which is **not the same** as omitting the key. The Java code must treat `NONE` as "do not serialize this field" (i.e., pass `null` to `ToolDefinition.create()` for the defer parameter). The `@JsonValue` of `""` is a landmine if it ever leaks to the wire. - -### 19:11 Prompt - -For all the created issues please find the right place to add the following requirement: - -✅✅ YOU MUST run `mvn spotless:apply` before every commit. - -and add it. - -### 19:13 Response - - -Updated todo list - -Ran terminal command: gh issue view 1758 --repo github/copilot-sdk --json body -q .body | tail -20 - -I'll add the requirement to the Constraints section of each issue. The best placement is at the top of Constraints since it's a workflow mandate: - -Ran terminal command: cd /Users/edburns/workareas/copilot-sdk && for ISSUE in 1758 1759 1760 1761 1762; do - BODY=$(gh issue view "$ISSUE" --repo github/copilot-sdk --json body -q .body) - UPDATED=$(echo "$BODY" | sed 's/## Constraints/## Constraints\n\n- ✅✅ YOU MUST run `mvn spotless:apply` before every commit./') - echo "$UPDATED" | gh issue edit "$ISSUE" --repo github/copilot-sdk --body-file - - echo "Updated issue #$ISSUE" -done - -Done. All 5 issues (#1758–#1762) now have `✅✅ YOU MUST run mvn spotless:apply before every commit.` as the first bullet in their Constraints section. - -Created 1 todo - -### 19:13 Prompt - - -Ugh, I did a git problem. - -``` -edburns@Edwards-MacBook-Pro copilot-sdk % git push upstream -git push upstream -To github.com:github/copilot-sdk.git - ! [rejected] edburns/1682-java-tool-ergonomics -> edburns/1682-java-tool-ergonomics (non-fast-forward) -error: failed to push some refs to 'github.com:github/copilot-sdk.git' -hint: Updates were rejected because the tip of your current branch is behind -hint: its remote counterpart. If you want to integrate the remote changes, -hint: use 'git pull' before pushing again. -``` - -Please fix it and push. Make sure not to overwrite anything. - -### 19:16 Response - -Updated todo list - -Ran terminal command: git log --oneline -5 - -Ran terminal command: git log --oneline -5 upstream/edburns/1682-java-tool-ergonomics - -Ran terminal command: git fetch upstream edburns/1682-java-tool-ergonomics && git log --oneline upstream/edburns/1682-java-tool-ergonomics..HEAD - -Ran terminal command: git log --oneline HEAD..upstream/edburns/1682-java-tool-ergonomics - -Ran terminal command: git merge-base HEAD upstream/edburns/1682-java-tool-ergonomics - -Ran terminal command: git log --oneline d15cfcbe -1 - -Ran terminal command: echo "Local tree (Resolve 3.8):" && git cat-file -p e9c978bb | grep tree && echo "Remote tree (Resolve 3.8):" && git cat-file -p 622e3ae0 | grep tree - -Ran terminal command: git diff upstream/edburns/1682-java-tool-ergonomics..HEAD --stat - -Ran terminal command: git diff HEAD..upstream/edburns/1682-java-tool-ergonomics --stat - -The remote is strictly a subset of local — it just has deletions (all the same files local added). The local branch has everything the remote has plus more. A safe rebase will work: - -Ran terminal command: git rebase upstream/edburns/1682-java-tool-ergonomics - -Ran terminal command: git stash && git rebase upstream/edburns/1682-java-tool-ergonomics - -Ran terminal command: git stash pop && git log --oneline -5 - -Rebase succeeded — local is now 4 commits ahead of remote with no divergence. Now pushing: - -Ran terminal command: git push upstream edburns/1682-java-tool-ergonomics - -Fixed. The rebase detected that your local commits (with different hashes but same content) were duplicates of what was already on the remote, skipped them, and replayed only the unique commits on top. Push succeeded as a fast-forward. - -Created 3 todos - -### 19:16 Prompt - -Did you push it to upstream? - -### 19:17 Response - - -Updated todo list - - -Yes. The output confirms it pushed to `upstream`: - -``` -To github.com:github/copilot-sdk.git - 622e3ae0..170d5aaa edburns/1682-java-tool-ergonomics -> edburns/1682-java-tool-ergonomics -``` - -### 19:17 Prompt - - - -I don't see the commits at https://github.com/github/copilot-sdk/pull/1763/commits . - -Why? - -### 19:18 Response - -Updated todo list - -Ran terminal command: gh pr view 1763 --repo github/copilot-sdk --json headRefName,baseRefName,headRepository - -PR #1763 is tracking branch `copilot/edburns1682-java-tool-ergonomics`, but you pushed to `edburns/1682-java-tool-ergonomics`. They're different branches. - -You either need to: -1. Push to the PR's branch: `git push upstream edburns/1682-java-tool-ergonomics:copilot/edburns1682-java-tool-ergonomics` -2. Or update the PR to point to your branch instead. - -Which would you prefer? - -### 19:18 Prompt - -We should always and only be pushing to the PR's branch. - -Updated todo list - -Ran terminal command: git push upstream edburns/1682-java-tool-ergonomics:copilot/edburns1682-java-tool-ergonomics - -Ran terminal command: git fetch upstream copilot/edburns1682-java-tool-ergonomics && git log --oneline upstream/copilot/edburns1682-java-tool-ergonomics -5 - -Ran terminal command: git log --oneline HEAD -5 - -Ran terminal command: git diff upstream/copilot/edburns1682-java-tool-ergonomics..HEAD --stat | tail -5 - -Ran terminal command: git diff HEAD..upstream/copilot/edburns1682-java-tool-ergonomics --stat - -Local is a strict superset — the reverse diff only shows deletions (content local has that the PR branch doesn't). Nothing would be lost. This needs a force push since the histories diverged. - -Analyzing your answers... - -Ran terminal command: git push upstream edburns/1682-java-tool-ergonomics:copilot/edburns1682-java-tool-ergonomics --force-with-lease - -Done. PR #1763 now shows the commits at `https://github.com/github/copilot-sdk/pull/1763/commits`. - -Created 2 todos - -### 19:24 Prompt - -Now there are ton of files in the **Files changed** tab that are not at all related to this PR: - -https://github.com/github/copilot-sdk/pull/1763 - -Please fix that so only the files relevant to our work are in there. - -Updated todo list - -Ran terminal command: git log --oneline upstream/main..HEAD - -Ran terminal command: git diff upstream/main..HEAD --stat | tail -40 - -Ran terminal command: git show 5c360d56 --stat - -Ran terminal command: git fetch upstream main && git log --oneline upstream/main -3 - -The branch has a duplicate of commit `a3dbdd19` (already on main) with a different hash (`5c360d56`). Rebasing onto current `upstream/main` will drop it: - -Ran terminal command: git stash && git rebase upstream/main - -Git skipped the duplicate. Let me verify and push: - -Ran terminal command: git diff upstream/main..HEAD --stat && echo "---" && git log --oneline upstream/main..HEAD - -Only Java and prompt files now — no dotnet/go/nodejs/python/rust files. Pushing: - -Ran terminal command: git push upstream edburns/1682-java-tool-ergonomics:copilot/edburns1682-java-tool-ergonomics --force-with-lease && git stash pop - -Done. The PR's "Files changed" tab should now show only the 19 relevant files — all Java source, prompts, and plan files. The unrelated dotnet/go/nodejs/python/rust changes from the duplicate commit are gone. - -Created 2 todos - -### 19:29 Prompt - -Now we come back to the most important thing to fix, that `NONE` field in `java/src/main/java/com/github/copilot/rpc/ToolDefer.java`. - -Read this comment from a reviewer: https://github.com/github/copilot-sdk/pull/1763#discussion_r3455952418 - -And recall what we elicited earlier: - -> Let's take .NET. What is the behavior of the tool in the "absence" case? And what does that serialize as? - -> There's the answer: - -> **In .NET, when `Defer` is `null`, the key is simply omitted from the JSON.** -> -> The serializer uses `DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull`, so any `null`-valued property (like `Defer`, `OverridesBuiltInTool`, `SkipPermission`) is not written to the wire at all. The CLI receives a `ToolDefinition` JSON object with no `defer` key — meaning "no preference, use server default." -> -> So the wire behavior for "absence" is: **the `defer` field does not appear in the JSON-RPC message**. -> -> --- -> -> For Java's `NONE("")`: if Jackson serializes it, it would emit `"defer": ""` — which is **not the same** as omitting the key. The Java code must treat `NONE` as "do not serialize this field" (i.e., pass `null` to `ToolDefinition.create()` for the defer parameter). The `@JsonValue` of `""` is a landmine if it ever leaks to the wire. - -It seems to me at this point the best thing is to update the Javadoc so that the future work to implement the annotation processor doesn't cause the `defer` to leak onto the wire. - -Is there any other better way at this point? - -### 19:31 Response - -Updated todo list - -Read [](file:///Users/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefer.java#1-1), lines 1 to 70 - -Read [](file:///Users/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefer.java#70-70), lines 70 to 76 - - - -Fetched https://github.com/github/copilot-sdk/pull/1763#discussion_r3455952418 - -There's a better option than Javadoc alone: make `NONE` serialize as `null` instead of `""`. This provides a safety net at the Jackson level: - -```java -@JsonValue -public String getValue() { - return this == NONE ? null : value; -} -``` - -With this change, even if `ToolDefer.NONE` accidentally reaches a `ToolDefinition`, Jackson serializes it as `null`, and `@JsonInclude(NON_NULL)` (already on `ToolDefinition`) omits it from the wire. The bug becomes impossible regardless of what the processor does. - -The internal `""` string is still needed because Java annotations require compile-time constants (`null` can't be used as an annotation element default), but the `@JsonValue` doesn't have to expose it. - -**Tradeoff:** - -| Approach | Prevents wire leak? | Relies on downstream code? | -|----------|---------------------|---------------------------| -| Javadoc only | No — still serializes as `""` if leaked | Yes — processor must map to `null` | -| `@JsonValue` returns `null` for NONE | Yes — impossible to emit `""` | No — defense in depth | - -I'd recommend doing **both**: the `@JsonValue` fix for safety, plus a Javadoc update explaining `NONE` is an annotation-only sentinel. Want me to make those changes? - -Created 1 todo - -### 19:31 Prompt - -Yes. Please do that and commit it. But do not push it. - -Updated todo list - -Read [](file:///Users/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefer.java#22-22), lines 22 to 55 - -Replacing 14 lines with 28 lines in [](file:///Users/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefer.java) - - -``` - -``` - - -Ran terminal command: cd /Users/edburns/workareas/copilot-sdk/java && export JAVA_HOME="/Library/Java/JavaVirtualMachines/microsoft-25.jdk/Contents/Home" && export M2_HOME=${HOME}/Downloads/apache-maven-3.9.8 && export PATH=${M2_HOME}/bin:${JAVA_HOME}/bin:${PATH} && mvn spotless:apply -q - -Ran terminal command: cd /Users/edburns/workareas/copilot-sdk && git add java/src/main/java/com/github/copilot/rpc/ToolDefer.java && git commit -m "fix(java): make ToolDefer.NONE serialize as null to prevent wire leak - -NONE is an annotation-only sentinel for @CopilotTool(defer=...) defaults. -Its @JsonValue now returns null so @JsonInclude(NON_NULL) omits it from -the JSON-RPC payload, matching the nullable/optional semantics used by -all other SDKs (.NET CopilotToolDefer?, Node defer?, Go omitempty, -Python | None, Rust Option)." - -Committed as `b8ad8264`. Spotless check passed. Not pushed. - -Created 1 todo - -Made changes. diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/add-tests-that-use-low_level_tool_definition.yaml.md b/1682-java-tool-ergonomics-prompts-remove-before-merge/add-tests-that-use-low_level_tool_definition.yaml.md deleted file mode 100644 index b60937cd2..000000000 --- a/1682-java-tool-ergonomics-prompts-remove-before-merge/add-tests-that-use-low_level_tool_definition.yaml.md +++ /dev/null @@ -1,151 +0,0 @@ -## Goal - -Port the Java integration test behavior from: - -- `java/src/test/java/com/github/copilot/LowLevelToolDefinitionIT.java` -- test method: `lowLevelToolDefinition` -- snapshot: `test/snapshots/tools/low_level_tool_definition.yaml` - -to the following non-Java SDKs, using each language's native E2E test infrastructure: - -1. `dotnet` -2. `go` -3. `nodejs` -4. `python` -5. `rust` - -The new/updated tests in each language must use the **same snapshot scenario** (`tools/low_level_tool_definition`) and validate the same behavior. - ---- - -## Required test behavior to port - -From a test perspective, replicate this behavior: - -1. Define a `set_current_phase` tool that accepts a `phase` argument (string, enum: `["searching", "analyzing", "done"]`) and returns `"Phase set to {phase}"`. The tool handler must also store the phase value in test-local state. -2. Define a `search_items` tool that accepts a `keyword` argument (string) and returns `"Found: item_alpha, item_beta"`. -3. Define a `grep` override tool (using whatever "override" mechanism the language provides) that accepts a `query` argument (string) and returns `"CUSTOM_GREP: {query}"`. -4. Create a session with: - - Permission handler that auto-approves all requests. - - Available tools: all custom tools (`*`) plus built-in `web_fetch`. - - The three tool definitions registered on the session. -5. Send prompt: `"First, set the current phase to 'analyzing'. Then search for items with keyword 'copilot'. Report the phase and search results."` -6. Assert: - - The assistant response is non-null/non-empty. - - The response content (case-insensitive) contains `"analyzing"`. - - The response content contains `"item_alpha"` or `"item_beta"`. - - The test-local phase state equals `"analyzing"` (verifying the tool handler was actually invoked). - -Do not weaken these assertions. - ---- - -## Critical execution constraint (must follow exactly) - -Proceed through languages **one-at-a-time** in this exact order: - -1. `dotnet` -2. `go` -3. `nodejs` -4. `python` -5. `rust` - -❌❌ **Do not continue to the next language unless and until the current language gets a clean run with the new test in isolation.** ❌❌ - -Do **not** run full cross-language or full-repo test suites. Let CI/CD handle broad runs. - ---- - -## Snapshot/name mapping requirements - -Ensure each language's test naming/harness maps to: - -- snapshot folder: `tools` -- snapshot file: `low_level_tool_definition.yaml` - -Do not create alternate snapshot names for this scenario. - ---- - -## Per-language isolated run commands - -Use these commands for isolated validation while iterating. - -### 1) dotnet - -Implement in dotnet E2E tests (preferred: new `LowLevelToolDefinitionE2ETests` class or add to existing `ToolsE2ETests` class using snapshot category `tools`, test method `Low_Level_Tool_Definition`). - -Isolated run: - -```bash -cd dotnet && dotnet test test/GitHub.Copilot.SDK.Test.csproj --filter "FullyQualifiedName~Low_Level_Tool_Definition" -``` - -### 2) go - -Implement in Go E2E tests with snapshot mapping to `tools/low_level_tool_definition` (preferred: add to existing `go/internal/e2e/tools_e2e_test.go` or create new file, subtest name exactly `low_level_tool_definition`). - -Isolated run: - -```bash -cd go && go test ./internal/e2e -run 'TestToolsE2E/low_level_tool_definition$' -count=1 -``` - -### 3) nodejs - -Implement in Node E2E Vitest (preferred: add to existing `nodejs/test/e2e/tools.e2e.test.ts` or create new file, test name mapping to `low_level_tool_definition`). - -Isolated run: - -```bash -cd nodejs && npm test -- test/e2e/tools.e2e.test.ts -t "low_level_tool_definition" -``` - -### 4) python - -Implement in Python E2E pytest (preferred: add to existing `python/e2e/test_tools_e2e.py` or create new file, test function `test_low_level_tool_definition`). - -Isolated run: - -```bash -cd python && uv run pytest e2e/test_tools_e2e.py::test_low_level_tool_definition -``` - -### 5) rust - -Implement in Rust E2E tests (preferred: add to existing `rust/tests/e2e/tools.rs`; use `with_e2e_context("tools", "low_level_tool_definition", ...)`). - -Isolated run: - -```bash -cd rust && cargo test --features test-support --test e2e tools::low_level_tool_definition -- --exact -``` - ---- - -## Implementation notes - -1. Reuse existing per-language E2E harness helpers and style conventions. -2. Keep changes scoped to test code and required wiring. -3. Do not hand-edit generated code. -4. ❌❌❌ DO NOT CHANGE ANY non-test CODE.❌❌❌ -5. ✅✅Put the test in the "right place" for each language. That means put it "near" any similar existing tests. The existing tools E2E test files are: - - `dotnet/test/E2E/ToolsE2ETests.cs` - - `go/internal/e2e/tools_e2e_test.go` - - `nodejs/test/e2e/tools.e2e.test.ts` - - `python/e2e/test_tools_e2e.py` - - `rust/tests/e2e/tools.rs` - Put the new test near those. ✅✅ -6. The snapshot `test/snapshots/tools/low_level_tool_definition.yaml` involves **two conversations**: one where tool calls are made without prior tool results, and one full round-trip (tool calls → tool results → final assistant message). Each language's replay proxy handles this; just ensure the test sends the right prompt and processes tool invocations correctly. -7. The `grep` override tool uses whatever "tool override" mechanism exists in each language (e.g., `ToolDefinition.createOverride` in Java, or the equivalent in each SDK). If a language has no override concept, define it as a regular custom tool named `grep`. - ---- - -## Deliverable - -When done, provide: - -1. files changed per language, -2. isolated command used per language, -3. pass/fail result per language (must be passing before moving to next), -4. any blockers (if any language cannot be completed). diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md b/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md deleted file mode 100644 index cb4b6cd4e..000000000 --- a/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md +++ /dev/null @@ -1,504 +0,0 @@ -# Implementation plan: `@CopilotTool` ergonomics (issue #1682) - -Human DRI: Ed Burns -ADR: `java/docs/adr/adr-005-tool-definition.md` -Issue: https://github.com/github/copilot-sdk/issues/1682 - ---- - -## Completed phases - -### Phase 1 ✅ — Define the problem and architectural decision - -- ADR-005 evaluates three options (status quo, record-as-schema, annotation-on-method). -- Decision: annotation-on-method with compile-time JSR 269 processor (langchain4j-style API, Micronaut-style implementation). - -### Phase 2 ✅ — Verify the existing low-level path works in Java - -- `test/snapshots/tools/low_level_tool_definition.yaml` created. -- `LowLevelToolDefinitionIT` passes with explicit `ToolDefinition.create()` / `createOverride()`. -- This proves the low-level API is correct and will serve as the foundation that the high-level API delegates to. - ---- - -## Phase 3 — Ignorance reduction: questions to answer before writing code - -This phase is about eliminating unknowns. Each item is a question or spike. Resolve these **before** writing production code. - -### 3.1 — Package placement - -**Question:** Where do `@CopilotTool` and `@Param` live? - -Current SDK structure is a single module (`copilot-sdk-java`). Two options: - -| Option | Location | Trade-off | -|--------|----------|-----------| -| A | `com.github.copilot.rpc` (alongside `ToolDefinition`) | Keeps everything together but the `rpc` package is already dense (40+ classes). | -| B | New package `com.github.copilot.tool` | Cleaner separation; the `tool` package holds annotations, processor, and `ToolDefinition.fromObject()`. But `ToolDefinition` itself stays in `rpc` (it's a JSON-RPC type). | - -**Recommendation:** Option B — new `com.github.copilot.tool` package for annotations + processor + schema generation. `ToolDefinition` stays in `rpc` and gets a new static method `fromObject(Object)` that delegates to `tool` package internals. - -**Action:** Decide; update `module-info.java` exports if new package is added. - -**Resolution:** Select Option B. - -### 3.2 — `@CopilotTool` annotation design - -**Question:** What attributes does `@CopilotTool` need? - -Based on ADR-005 and the C#/langchain4j comparisons: - -```java -@Documented -@Retention(RetentionPolicy.SOURCE) // only needed at compile time for processor -@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; -} -``` - -**Open questions:** - -1. Should `@CopilotTool` have `@Retention(SOURCE)` (processor-only, like Dagger) or `RUNTIME` (fallback reflection path, like langchain4j)? ADR-005 says "compile-time preferred, runtime fallback" — if we want a fallback path, we need `RUNTIME`. If we commit to processor-only, `SOURCE` is sufficient. - -2. Is `ToolDefer` (the defer config from `ToolDefinition`) needed on the annotation, or is that too niche for v1? - -**Recommendation:** Start with `RUNTIME` retention so the reflection fallback works. Defer `ToolDefer` support to a follow-up. - -**Resolution:** Select `RUNTIME` and `ToolDefer` support. - -### 3.3 — `@Param` annotation design - -**Question:** What attributes does `@Param` need? - -```java -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.PARAMETER) -public @interface Param { - /** Parameter description (sent to the model). */ - String value() default ""; - - /** Parameter name override. Defaults to the actual parameter name. */ - String name() default ""; - - /** Whether this parameter is required. Default true. */ - boolean required() default true; - - /** Optional default value when the argument is omitted. */ - String defaultValue() default ""; -} -``` - -**Resolution:** Support `defaultValue()` in v1 (langchain4j parity) and make it behaviorally effective, not docs-only. - -Implementation rules: -- Emit JSON Schema defaults at `properties..default` for model guidance. -- Apply defaults at invocation time when an argument key is missing, then do normal coercion/casting and method invocation. -- Forbid `required=true` together with a non-empty `defaultValue()` (compile-time error in processor, matching runtime reflection fallback validation). -- Parse and validate `defaultValue()` against the Java parameter type (fail fast on mismatch). -- Ensure compile-time generated path and runtime reflection fallback use identical defaulting semantics. - -### 3.4 — Type-to-JSON-Schema mapping - -**Question:** What Java types do we need to map to JSON Schema, and how? - -Minimum viable set (from langchain4j's `JsonSchemaElementUtils`): - -| Java type | JSON Schema | -|-----------|-------------| -| `String` | `{"type": "string"}` | -| `int`, `Integer`, `long`, `Long` | `{"type": "integer"}` | -| `double`, `Double`, `float`, `Float` | `{"type": "number"}` | -| `boolean`, `Boolean` | `{"type": "boolean"}` | -| `enum` types | `{"type": "string", "enum": ["V1", "V2", ...]}` | -| `List`, `Collection` | `{"type": "array", "items": }` | -| `Map` | `{"type": "object"}` (opaque — no inner schema) | -| Records / POJOs | `{"type": "object", "properties": {...}, "required": [...]}` | - -**Key design decision:** The annotation processor must generate this schema at compile time from `javax.lang.model` types (`TypeMirror`, `DeclaredType`, etc.), NOT from `java.lang.reflect` types. This is different from langchain4j which does it at runtime. - -**Spike needed:** Write a small proof-of-concept that maps `TypeMirror` → JSON Schema `Map` literal in generated source code. The tricky cases are: -- Enum constants (processor can see them via `ElementKind.ENUM_CONSTANT`) -- Generic type arguments on `List` (processor sees them via `DeclaredType.getTypeArguments()`) -- Recursive/nested records (need `$defs` or just go one level deep for v1) - -**Recommendation:** Start with the flat types (primitives, String, enums) and `List`. Defer nested records and polymorphic types to a follow-up. - -**Resolution:** - -Here's the **maximum viable set** derived from every type actually used across `com.github.copilot.rpc` and `com.github.copilot.generated.rpc`: - -| Java type | JSON Schema | SDK usage examples | -|-----------|-------------|-------------------| -| `String` | `{"type": "string"}` | Pervasive (IDs, names, paths, messages) | -| `int`, `Integer` | `{"type": "integer"}` | `ProviderConfig`, `ModelLimits`, `McpServerConfig` | -| `long`, `Long` | `{"type": "integer"}` | Timestamps in hook inputs, JSON-RPC IDs | -| `double`, `Double` | `{"type": "number"}` | `ModelBilling.multiplier`, `InfiniteSessionConfig` thresholds | -| `float`, `Float` | `{"type": "number"}` | Not used in SDK today, but natural complement | -| `boolean`, `Boolean` | `{"type": "boolean"}` | Extensively in `SessionConfig`, hook outputs, options | -| `String[]` | `{"type": "array", "items": {"type": "string"}}` | `CopilotClientOptions.cliArgs` | -| `enum` types | `{"type": "string", "enum": ["V1", ...]}` | 92 enums (`ToolDefer`, `AgentMode`, 85 generated) | -| `UUID` | `{"type": "string", "format": "uuid"}` | Generated record ID fields | -| `OffsetDateTime` | `{"type": "string", "format": "date-time"}` | Generated record timestamp fields | -| `JsonNode` | `{}` (any) | `ToolInvocation.argumentsNode`, hook args/results | -| `Object` | `{}` (any) | `ToolDefinition.parameters`, `JsonRpcRequest.params` | -| `List`, `Collection` | `{"type": "array", "items": }` | 50+ distinct `T` across the API | -| `Map` | `{"type": "object", "additionalProperties": {"type": "string"}}` | `ProviderConfig.headers`, env maps | -| `Map` | `{"type": "object", "additionalProperties": {"type": "boolean"}}` | Generated records | -| `Map` | `{"type": "object", "additionalProperties": {"type": "integer"}}` | Generated records | -| `Map` | `{"type": "object"}` (opaque) | `ToolInvocation.getArguments()`, telemetry, extensionData | -| `Map` (typed `T`) | `{"type": "object", "additionalProperties": }` | `Map`, `Map`, etc. | -| `Map>` | `{"type": "object", "additionalProperties": {"type": "array", "items": {"type": "string"}}}` | Generated records | -| Records / POJOs | `{"type": "object", "properties": {...}, "required": [...]}` | ~690 types (26 handwritten records, ~65 POJOs, ~600 generated) | -| Sealed / `@JsonSubTypes` | `{"oneOf": [...]}` with discriminator | `McpServerConfig`, `MessageAttachment`, `SlashCommandInvocationResult` | -| `Optional` | Schema of `T`, not in `required` array | `Optional` in `SessionConfig` | -| `OptionalInt` | `{"type": "integer"}`, not in `required` | `CopilotClientOptions.getSessionIdleTimeoutSeconds()` | -| `OptionalDouble` | `{"type": "number"}`, not in `required` | `ModelBilling.getMultiplierOpt()` | - -**Types intentionally excluded** (not sensible as tool parameter types): - -| Java type | Reason | -|-----------|--------| -| `CompletableFuture` | Return type only, not a parameter type | -| `Consumer`, `Supplier`, `Function`, `Executor` | Callback/functional types, not serializable | -| `ObjectMapper`, `TypeReference` | Infrastructure, not data | - -The minimum viable set from the plan covers 8 rows. This maximum viable set covers **23 rows** — adding `String[]`, `UUID`, `OffsetDateTime`, `JsonNode`/`Object` (any), typed `Map` variants, sealed/polymorphic types, and `Optional` variants. These all have established Jackson serialization semantics in the existing SDK. - -### 3.5 — Generated code shape - -**Question:** What exactly does the processor generate? - -ADR-005 proposes: - -```java -// GENERATED — do not edit -final class MyTools$$CopilotToolMeta { - static List definitions(MyTools instance) { - return List.of( - new ToolDefinition("set_current_phase", "Sets the current phase", - Map.of("type", "object", - "properties", Map.of("phase", Map.of("type", "string", - "description", "The phase to transition to")), - "required", List.of("phase")), - invocation -> { - String phase = (String) invocation.getArguments().get("phase"); - return CompletableFuture.completedFuture( - instance.setCurrentPhase(phase)); - }, null, null, null) - ); - } -} -``` - -**Open questions:** - -1. **Method invocation in generated code:** The generated lambda calls `instance.setCurrentPhase(phase)` directly — no reflection at runtime. But this requires the method to be accessible (not private). What access levels do we support? langchain4j uses `method.setAccessible(true)` at runtime. We'd need to either: - - Require `public` or package-private methods, OR - - Generate a helper that uses `MethodHandles.Lookup` to access private methods (complex), OR - - Just require non-private. - - **Recommendation:** Require at least package-private. The generated `$$CopilotToolMeta` class is in the same package, so package-private and above work. Emit a compile error for `private` methods annotated with `@CopilotTool`. - -2. **Return type handling:** What does the generated code do with the method's return value? - - | Return type | Generated behavior | - |-------------|-------------------| - | `String` | Wrap in `CompletableFuture.completedFuture(result)` | - | `CompletableFuture` | Use as-is (native async) | - | `CompletableFuture` | `.thenApply(objectMapper::writeValueAsString)` | - | `void` | `CompletableFuture.completedFuture("Success")` | - | Other `T` | JSON-serialize via Jackson `ObjectMapper` | - - **Recommendation:** Support `String`, `void`, `CompletableFuture`, and `CompletableFuture` for v1. Other return types get JSON-serialized (since Jackson is already a dependency). - -3. **Argument deserialization in generated code:** How does the generated lambda extract and coerce arguments? - - For simple types, the generated code can cast directly from the `Map` returned by `invocation.getArguments()`: - ```java - String city = (String) invocation.getArguments().get("city"); - int count = ((Number) invocation.getArguments().get("count")).intValue(); - ``` - - For complex types (records, enums), use `invocation.getArgumentsAs()` or Jackson's `ObjectMapper.convertValue()`: - ```java - Phase phase = objectMapper.convertValue(invocation.getArguments().get("phase"), Phase.class); - ``` - - **Recommendation:** Generate direct casts for primitives/String, and `ObjectMapper.convertValue()` for enums, records, and complex types. The `ObjectMapper` instance can come from a static field in the generated class. - -**Resolution:** - -The generated `$$CopilotToolMeta` class produces lambdas identical to what `LowLevelToolDefinitionIT` writes by hand. Decisions: - -1. **Access levels:** Require at least package-private. The generated `$$CopilotToolMeta` lives in the same package as the user's class, so package-private and above work without reflection. Emit a compile error for `private` methods annotated with `@CopilotTool`. - -2. **Return type handling:** - - | Return type | Generated code | - |-------------|----------------| - | `String` | `CompletableFuture.completedFuture(instance.method(...))` | - | `CompletableFuture` | `instance.method(...)` (use as-is) | - | `void` | `instance.method(...); return CompletableFuture.completedFuture("Success")` | - | `CompletableFuture` (non-String) | `instance.method(...).thenApply(objectMapper::writeValueAsString)` | - | Other `T` | `CompletableFuture.completedFuture(objectMapper.writeValueAsString(instance.method(...)))` | - -3. **Argument deserialization:** Follow the two patterns proven in `LowLevelToolDefinitionIT`: - - **Simple types** (`String`, primitives, boxed): generate direct cast from `invocation.getArguments().get("name")` (e.g., `(String) args.get("city")`, `((Number) args.get("count")).intValue()`). - - **Complex types** (enums, records, POJOs): generate `objectMapper.convertValue(invocation.getArguments().get("name"), TargetType.class)`. - - **Single-record-parameter shortcut:** When a method has exactly one parameter that is a record/POJO (matching the full argument set), generate `invocation.getArgumentsAs(RecordType.class)` — the same pattern `LowLevelToolDefinitionIT.setPhaseTool` uses. - - The `ObjectMapper` instance: use a `private static final` field in the generated `$$CopilotToolMeta` class, initialized from `com.github.copilot.rpc.RpcMapper.INSTANCE` (already exists in the SDK). - -### 3.6 — `ToolDefinition.fromObject(Object)` registration API - -**Question:** How does the user get from "an object with `@CopilotTool` methods" to a `List`? - -```java -// Primary API — loads generated $$CopilotToolMeta class -List tools = ToolDefinition.fromObject(myToolsInstance); - -// Variant: from class (for static tools) -List tools = ToolDefinition.fromClass(MyTools.class); -``` - -**Implementation:** - -```java -public static List fromObject(Object instance) { - Class clazz = instance.getClass(); - String metaClassName = clazz.getName() + "$$CopilotToolMeta"; - try { - Class metaClass = Class.forName(metaClassName); - Method defs = metaClass.getMethod("definitions", clazz); - return (List) defs.invoke(null, instance); - } catch (ClassNotFoundException e) { - // Fallback: runtime reflection (if we support it) - return fromObjectReflective(instance); - } -} -``` - -**Open question:** Do we want the reflection fallback? It's nice for users who don't run the processor (e.g., scripting, prototyping), but it adds code and the `-parameters` concern. - -**Recommendation:** Implement the reflection fallback but mark it `@CopilotExperimental` separately. The primary path is the generated `$$CopilotToolMeta`. - -**Resolution:** we only want the processor approach. - -### 3.7 — `module-info.java` impact - -The SDK uses JPMS. The processor generates classes into the user's module, not the SDK's. But `fromObject()` uses `Class.forName()` which needs the generated class to be accessible. - -**Question:** Does the generated `$$CopilotToolMeta` class in the user's module need to be exported for `fromObject()` to find it? - -**Answer:** No. `Class.forName()` with the caller's classloader works within the same module. And in the typical unnamed-module (classpath) case, everything is accessible. If the user has a named module, the generated class is in the same package as their tools class, so it's accessible. - -**Action:** Verify this works in a simple named-module test. - -**Resolution:** - -See `1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/pom.xml` and files - -``` -src/main/java/com/github/dd3021192/MyTools$$CopilotToolMeta.java -src/main/java/com/github/dd3021192/MyTools.java -src/main/java/com/github/dd3021192/Main.java -``` - -For verificiation. - -### 3.8 — Processor registration - -**Question:** How is the new `@CopilotTool` processor registered alongside `CopilotExperimentalProcessor`? - -The existing `META-INF/services/javax.annotation.processing.Processor` lists `CopilotExperimentalProcessor`. Add the new processor to the same file: - -``` -com.github.copilot.CopilotExperimentalProcessor -com.github.copilot.tool.CopilotToolProcessor -``` - -And in `module-info.java`: -```java -provides javax.annotation.processing.Processor - with CopilotExperimentalProcessor, CopilotToolProcessor; -``` - -**Resolution:** **No issues expected here** — this is standard JSR 269 multi-processor registration. - ---- - -## Phase 4 — Implementation (the build order) - -After Phase 3 questions are resolved, implement in this order. Each step should be a separately testable commit. - -### 4.1 — Annotations (`@CopilotTool`, `@Param`) - -**What:** Create the two annotation classes. - -**Files to create:** -- `java/src/main/java/com/github/copilot/tool/CopilotTool.java` -- `java/src/main/java/com/github/copilot/tool/Param.java` - -**Tests:** -- Compile-only: ensure they compile, can be applied to methods/parameters, and are annotated with `@CopilotExperimental`. -- No runtime behavior yet. - -**Gating criteria:** `mvn clean compile` passes. - -### 4.2 — Schema generation utility (compile-time) - -**What:** A utility class that, given `javax.lang.model` types, produces the `Map` JSON Schema as a Java source code literal. - -**Files to create:** -- `java/src/main/java/com/github/copilot/tool/SchemaGenerator.java` (compile-time, works with `TypeMirror`) - -**Tests:** -- Unit tests that exercise the type-to-schema mapping with mock `TypeMirror` instances (or integration tests via the annotation processor in a test compilation). - -**Gating criteria:** Can generate correct schema `Map` source code for: `String`, `int`, `boolean`, `double`, `enum`, `List`, a simple record. - -### 4.3 — Annotation processor (`CopilotToolProcessor`) - -**What:** JSR 269 processor that finds `@CopilotTool` methods and generates `$$CopilotToolMeta` classes. - -**Files to create:** -- `java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java` - -**Files to modify:** -- `java/src/main/resources/META-INF/services/javax.annotation.processing.Processor` — add the new processor -- `java/src/main/java/module-info.java` — add `provides` clause and `exports com.github.copilot.tool` - -**Tests:** -- **Compilation tests:** Compile test source files with `@CopilotTool` methods and verify: - - `$$CopilotToolMeta` class is generated - - Generated schema matches expected JSON Schema - - Compile errors emitted for: private methods, unsupported parameter types, duplicate tool names -- Use `javax.tools.JavaCompiler` programmatically (same pattern langchain4j uses for testing annotation processors). - -**Gating criteria:** Processor generates correct `$$CopilotToolMeta` for a class with 2-3 `@CopilotTool` methods. - -### 4.4 — `ToolDefinition.fromObject(Object)` - -**What:** The runtime bridge that loads generated metadata and returns `List`. - -**Files to modify:** -- `java/src/main/java/com/github/copilot/rpc/ToolDefinition.java` — add `fromObject(Object)` and `fromClass(Class)` static methods - -**Tests:** -- Unit test: create a test class with `@CopilotTool` methods, compile it (processor generates metadata), call `fromObject()`, verify the returned `List` has correct names, descriptions, schemas, and working handlers. - -**Gating criteria:** `ToolDefinition.fromObject(new MyTestTools())` returns a list with working tool definitions. - -### 4.5 — E2E integration test - -**What:** An E2E failsafe IT that uses `@CopilotTool` + `ToolDefinition.fromObject()` against the replay proxy. - -**Files to create:** -- `test/snapshots/tools/ergonomic_tool_definition.yaml` — new snapshot (may be identical to `low_level_tool_definition.yaml` since the wire format is the same) -- `java/src/test/java/com/github/copilot/ErgonomicToolDefinitionIT.java` - -**The test will look like:** - -```java -class MyTestTools { - String currentPhase; - - @CopilotTool("Sets the current phase of the agent") - String setCurrentPhase(@Param("The phase to transition to") String phase) { - currentPhase = phase; - return "Phase set to " + phase; - } - - @CopilotTool("Search for items by keyword") - String searchItems(@Param("Search keyword") String keyword) { - return "Found: item_alpha, item_beta"; - } - - @CopilotTool(value = "Custom grep override", name = "grep", overridesBuiltInTool = true) - String grepOverride(@Param("Search query") String query) { - return "CUSTOM_GREP: " + query; - } -} - -@Test -void ergonomicToolDefinition() throws Exception { - MyTestTools tools = new MyTestTools(); - List toolDefs = ToolDefinition.fromObject(tools); - - // ... create session with toolDefs, send prompt, assert same behavior - // as LowLevelToolDefinitionIT -} -``` - -**Gating criteria:** Test passes with the same assertions as `LowLevelToolDefinitionIT` — proving the ergonomic API produces identical behavior to the explicit API. - -### 4.6 — Reflection fallback (optional, can defer) - -Per resolution 3.6, we have decided to defer item 4.6. - -**What:** `fromObject()` falls back to runtime reflection when `$$CopilotToolMeta` is not found. - -**Files to create/modify:** -- `java/src/main/java/com/github/copilot/tool/ReflectiveToolScanner.java` -- Modify `ToolDefinition.fromObject()` to call this on `ClassNotFoundException` - -**Tests:** -- Compile a test class WITHOUT the annotation processor, call `fromObject()`, verify it still works (with `-parameters` flag). - -**Gating criteria:** Fallback path produces the same `List` as the processor-generated path. - ---- - -## Phase 5 — Documentation and examples - -- Update `java/README.md` with the ergonomic tool definition example. -- Add a "Tools" section showing both the low-level and high-level APIs. -- Reference ADR-005 for design rationale. - ---- - -## Phase 6 — Port to `add-tests-that-use-ergonomic_tool_definition.yaml.md` - -Same cycle as Phase 2 → `add-tests-that-use-low_level_tool_definition.yaml.md`: once the Java E2E test passes with the ergonomic API, create a prompt to port the test to dotnet/go/nodejs/python/rust. - -**Note:** This may not be applicable — the ergonomic API (`@CopilotTool`) is Java-specific. The other SDKs already have their own ergonomic paths. The snapshot can be shared, but the test code is language-specific by nature. Evaluate whether this phase is needed after Phase 5. - ---- - -## Reference: how langchain4j and Micronaut do it - -### langchain4j (runtime reflection) - -- `@Tool` on methods, `@P` on parameters. -- `ToolSpecifications.toolSpecificationsFrom(Object)` scans methods at runtime. -- `JsonSchemaElementUtils` maps `java.lang.reflect.Type` → JSON Schema. -- `DefaultToolExecutor.executeWithContext()` invokes via `Method.invoke()` with argument coercion. -- Requires `-parameters` javac flag or explicit `@P(name="...")`. -- Source: `langchain4j-core/src/main/java/dev/langchain4j/agent/tool/` - -### Micronaut (compile-time annotation processor) - -- `AbstractInjectAnnotationProcessor` (extends `AbstractProcessor`) is the JSR 269 entry point. -- `TypeElementVisitor` SPI pattern: visitors registered via SPI walk the AST. -- `BeanDefinitionWriter` generates bytecode companion classes (`$Definition`, `$Definition$Exec`). -- `ParameterElement.getName()` at compile time — no `-parameters` flag needed. -- Source: `inject-java/src/main/java/io/micronaut/annotation/processing/` - -### Our approach: langchain4j's API + Micronaut's implementation strategy - -- **User-facing API** matches langchain4j: `@CopilotTool` on methods, `@Param` on parameters, `fromObject()` to discover. -- **Implementation** matches Micronaut: JSR 269 processor generates companion classes at compile time, no runtime reflection in the happy path, no `-parameters` requirement. -- **Fallback** path uses langchain4j-style runtime reflection for users who don't run the processor (prototyping, scripting). diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/dependency-reduced-pom.xml b/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/dependency-reduced-pom.xml deleted file mode 100644 index 75214f174..000000000 --- a/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/dependency-reduced-pom.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - 4.0.0 - com.github - dd3021192 - 1.0.0-SNAPSHOT - - - - maven-jar-plugin - 3.3.0 - - - - com.github.dd3021192.Main - - - - - - maven-shade-plugin - 3.5.1 - - - package - - shade - - - - - - - - UTF-8 - 17 - 17 - - diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/pom.xml b/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/pom.xml deleted file mode 100644 index 3d2b26a0e..000000000 --- a/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/pom.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - 4.0.0 - - com.github - dd3021192 - 1.0.0-SNAPSHOT - jar - - - 17 - 17 - UTF-8 - - - - - com.github - copilot-sdk-java - 1.0.2-SNAPSHOT - - - - - - - org.apache.maven.plugins - maven-jar-plugin - 3.3.0 - - - - com.github.dd3021192.Main - - - - - - org.apache.maven.plugins - maven-shade-plugin - 3.5.1 - - - package - - shade - - - - - - - diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/Main.java b/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/Main.java deleted file mode 100644 index a26845d10..000000000 --- a/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/Main.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.github.dd3021192; - -import java.lang.reflect.Method; -import java.util.List; -import java.util.Map; - -/** - * Named-module JPMS test for issue #1682, Phase 3.7. - * - * Proves that ToolDefinition.fromObject() pattern works in a named module: - * - Class.forName() can locate the generated $$CopilotToolMeta companion class - * - The companion class is accessible (same package, same module) - * - Method invocation on the companion works without extra JPMS exports - */ -public class Main { - - public static void main(String[] args) throws Exception { - System.out.println("=== JPMS Named-Module Test for §3.7 ==="); - System.out.println("Module: " + Main.class.getModule().getName()); - System.out.println(); - - MyTools instance = new MyTools(); - Class toolsClass = instance.getClass(); - - // This is exactly what ToolDefinition.fromObject() will do: - String metaClassName = toolsClass.getName() + "$$CopilotToolMeta"; - System.out.println("Looking up generated meta class: " + metaClassName); - - // Step 1: Class.forName() — the critical JPMS question - Class metaClass = Class.forName(metaClassName); - System.out.println("[PASS] Class.forName() found: " + metaClass.getName()); - - // Step 2: Get the 'definitions' method - Method defsMethod = metaClass.getMethod("definitions", toolsClass); - System.out.println("[PASS] Found method: " + defsMethod); - - // Step 3: Invoke it - @SuppressWarnings("unchecked") - List> definitions = - (List>) defsMethod.invoke(null, instance); - System.out.println("[PASS] Invoked definitions(), got " + definitions.size() + " tool(s)"); - - // Step 4: Verify content - Map tool = definitions.get(0); - assert "set_current_phase".equals(tool.get("name")) - : "Expected tool name 'set_current_phase', got: " + tool.get("name"); - assert "Sets the current phase".equals(tool.get("description")) - : "Expected description mismatch"; - System.out.println("[PASS] Tool definition correct: " + tool.get("name") - + " — \"" + tool.get("description") + "\""); - - System.out.println(); - System.out.println("=== ALL CHECKS PASSED ==="); - System.out.println("Conclusion: Class.forName() works within a named JPMS module"); - System.out.println("for locating $$CopilotToolMeta in the same package. No extra"); - System.out.println("exports or opens directives are needed."); - } -} diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/MyTools$$CopilotToolMeta.java b/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/MyTools$$CopilotToolMeta.java deleted file mode 100644 index 3c39f42b9..000000000 --- a/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/MyTools$$CopilotToolMeta.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.github.dd3021192; - -import java.lang.reflect.Method; -import java.util.List; -import java.util.Map; - -/** - * Simulates the generated companion class that the annotation processor would produce. - * In real usage, this class is auto-generated at compile time. - * - * The key point: it lives in the SAME package as MyTools, so it can call - * package-private methods and is discoverable via Class.forName() from the same module. - */ -public final class MyTools$$CopilotToolMeta { - - private MyTools$$CopilotToolMeta() {} - - /** - * Returns tool definitions for the given MyTools instance. - * This mirrors what the real generated code will produce. - */ - public static List> definitions(MyTools instance) { - return List.of( - Map.of( - "name", "set_current_phase", - "description", "Sets the current phase", - "parameters", Map.of( - "type", "object", - "properties", Map.of( - "phase", Map.of( - "type", "string", - "description", "The phase to transition to" - ) - ), - "required", List.of("phase") - ) - ) - ); - } -} diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/MyTools.java b/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/MyTools.java deleted file mode 100644 index 7105a5a8f..000000000 --- a/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/MyTools.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.github.dd3021192; - -/** - * Simulates a user's tool class annotated with @CopilotTool methods. - * In real usage, the annotation processor would generate MyTools$$CopilotToolMeta. - */ -public class MyTools { - - private String currentPhase = "init"; - - // This would be annotated with @CopilotTool("Sets the current phase") - public String setCurrentPhase(String phase) { - this.currentPhase = phase; - return "Phase set to " + phase; - } - - public String getCurrentPhase() { - return currentPhase; - } -} diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/module-info.java b/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/module-info.java deleted file mode 100644 index 1b1d803c7..000000000 --- a/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/module-info.java +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Named-module test for issue #1682 Phase 3.7. - * - * Demonstrates that Class.forName() can locate a generated $$CopilotToolMeta - * companion class from within a named JPMS module, without requiring extra exports. - */ -module com.github.dd3021192 { - requires com.github.copilot.java; -} From 529c898ebd2e743c2034141e060aa4dd204c4d9c Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Mon, 22 Jun 2026 20:05:50 -0400 Subject: [PATCH 3/5] fix(java): correct ToolDefer.NONE Javadoc on @JsonValue null semantics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clarify that @JsonValue returning null does not cause field omission by @JsonInclude(NON_NULL) — it only changes the leak from "" to null. The primary protection is mapping NONE to a null field reference before constructing ToolDefinition (responsibility of the annotation processor and ToolDefinition.fromObject()). --- .../com/github/copilot/rpc/ToolDefer.java | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/java/src/main/java/com/github/copilot/rpc/ToolDefer.java b/java/src/main/java/com/github/copilot/rpc/ToolDefer.java index 5b9e115c7..ba888ca97 100644 --- a/java/src/main/java/com/github/copilot/rpc/ToolDefer.java +++ b/java/src/main/java/com/github/copilot/rpc/ToolDefer.java @@ -27,13 +27,18 @@ public enum ToolDefer { *

* This constant must not be passed to {@link ToolDefinition} factory * methods. The annotation processor and {@code ToolDefinition.fromObject()} - * must map {@code NONE} to {@code null} so the {@code defer} field is omitted - * from the JSON-RPC wire payload (matching the nullable/optional semantics used - * by all other SDKs). + * 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). *

- * As a safety net, {@link #getValue()} returns {@code null} for this constant, - * so {@code @JsonInclude(NON_NULL)} will omit it even if it accidentally - * reaches serialization. + * As a secondary safety net, {@link #getValue()} returns {@code null} for this + * constant. Note that this alone does not 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(""), @@ -52,8 +57,10 @@ public enum ToolDefer { /** * Returns the JSON value for this deferral mode. *

- * Returns {@code null} for {@link #NONE} so that {@code @JsonInclude(NON_NULL)} - * omits it from the wire payload. + * 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, or {@code null} for * {@link #NONE} From 89b209defb6b0fc6cb36d7c722ea99f0a214a4ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Jun 2026 00:15:10 +0000 Subject: [PATCH 4/5] fix(java): address three review comments Co-authored-by: edburns <75821+edburns@users.noreply.github.com> --- .../com/github/copilot/tool/CopilotTool.java | 2 +- .../java/com/github/copilot/tool/Param.java | 2 +- .../tool/CopilotToolAnnotationTest.java | 19 +++++++++++++------ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/java/src/main/java/com/github/copilot/tool/CopilotTool.java b/java/src/main/java/com/github/copilot/tool/CopilotTool.java index 92347259f..9cde49b20 100644 --- a/java/src/main/java/com/github/copilot/tool/CopilotTool.java +++ b/java/src/main/java/com/github/copilot/tool/CopilotTool.java @@ -27,7 +27,7 @@ * } * * - * @since 1.0.0 + * @since 1.0.2 */ @Documented @Retention(RetentionPolicy.RUNTIME) diff --git a/java/src/main/java/com/github/copilot/tool/Param.java b/java/src/main/java/com/github/copilot/tool/Param.java index 56d513d84..75f359dd8 100644 --- a/java/src/main/java/com/github/copilot/tool/Param.java +++ b/java/src/main/java/com/github/copilot/tool/Param.java @@ -25,7 +25,7 @@ * } * * - * @since 1.0.0 + * @since 1.0.2 */ @Documented @Retention(RetentionPolicy.RUNTIME) diff --git a/java/src/test/java/com/github/copilot/tool/CopilotToolAnnotationTest.java b/java/src/test/java/com/github/copilot/tool/CopilotToolAnnotationTest.java index 56b64b19f..9052c6b1c 100644 --- a/java/src/test/java/com/github/copilot/tool/CopilotToolAnnotationTest.java +++ b/java/src/test/java/com/github/copilot/tool/CopilotToolAnnotationTest.java @@ -6,12 +6,14 @@ import static org.junit.jupiter.api.Assertions.*; +import java.io.InputStream; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; import java.lang.reflect.Parameter; +import java.nio.charset.StandardCharsets; import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.Test; @@ -41,12 +43,7 @@ void copilotToolTargetsMethod() { } @Test - void copilotToolIsAnnotatedWithCopilotExperimental() { - // @CopilotExperimental has CLASS retention so it is not visible via - // reflection at runtime. However, we can confirm: - // 1. The annotation type targets TYPE (which includes @interface declarations). - // 2. Compilation succeeded with @CopilotExperimental on @CopilotTool - // (the CopilotExperimentalProcessor would reject usage otherwise). + void copilotExperimentalTargetsTypeForAnnotationDeclarations() { Target expTarget = CopilotExperimental.class.getAnnotation(Target.class); assertNotNull(expTarget); boolean includesType = false; @@ -59,6 +56,16 @@ void copilotToolIsAnnotatedWithCopilotExperimental() { assertTrue(includesType, "@CopilotExperimental must target TYPE to be applicable to annotation declarations"); } + @Test + void copilotToolDeclaresCopilotExperimentalInClassFile() throws Exception { + String classFileResourcePath = "/" + CopilotTool.class.getName().replace('.', '/') + ".class"; + try (InputStream classFile = CopilotTool.class.getResourceAsStream(classFileResourcePath)) { + assertNotNull(classFile, "CopilotTool class file must be readable as a resource"); + String classFileText = new String(classFile.readAllBytes(), StandardCharsets.ISO_8859_1); + assertTrue(classFileText.contains("com/github/copilot/CopilotExperimental")); + } + } + @Test void copilotToolDefaultValues() throws Exception { Method nameMethod = CopilotTool.class.getDeclaredMethod("name"); From f56ee207b267b9752224178240a9c84b0ee2f7b2 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Mon, 22 Jun 2026 20:28:22 -0400 Subject: [PATCH 5/5] Revert "Remove prompts, pre-merge" This reverts commit a4fe9b270e0a796a2510d37ded05581d13c8c746. --- .../1682-low-level-tool-definition.md | 94 ++ .../20260615-prompts.md | 613 ++++++++++ .../20260616-prompts.md | 218 ++++ .../20260617-prompts.md | 374 ++++++ .../20260618-prompts.md | 135 +++ .../20260622-prompts.md | 1010 +++++++++++++++++ ...that-use-low_level_tool_definition.yaml.md | 151 +++ ...rance-reduction-for-implementation-plan.md | 504 ++++++++ .../dd3021192/dependency-reduced-pom.xml | 39 + .../dd3021192/pom.xml | 55 + .../main/java/com/github/dd3021192/Main.java | 58 + .../dd3021192/MyTools$$CopilotToolMeta.java | 40 + .../java/com/github/dd3021192/MyTools.java | 20 + .../dd3021192/src/main/java/module-info.java | 9 + 14 files changed, 3320 insertions(+) create mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/1682-low-level-tool-definition.md create mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/20260615-prompts.md create mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/20260616-prompts.md create mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/20260617-prompts.md create mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/20260618-prompts.md create mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/20260622-prompts.md create mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/add-tests-that-use-low_level_tool_definition.yaml.md create mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md create mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/dependency-reduced-pom.xml create mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/pom.xml create mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/Main.java create mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/MyTools$$CopilotToolMeta.java create mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/MyTools.java create mode 100644 1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/module-info.java diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/1682-low-level-tool-definition.md b/1682-java-tool-ergonomics-prompts-remove-before-merge/1682-low-level-tool-definition.md new file mode 100644 index 000000000..34fb2ee91 --- /dev/null +++ b/1682-java-tool-ergonomics-prompts-remove-before-merge/1682-low-level-tool-definition.md @@ -0,0 +1,94 @@ +# Plan: Add E2E test for non-ergonomic (low-level) tool definition + +## Goal + +Add a failsafe IT test that exercises the **current explicit** `ToolDefinition.create()` / `ToolDefinition.createOverride()` API — the "non-ergonomic" approach — with multiple tools, `ToolSet` with `addCustom`/`addBuiltIn`, `getArgumentsAs()` deserialization into a record, and a tool handler that mutates application state. This establishes baseline test coverage before issue #1682 adds the annotation-driven ergonomic API. + +## Instructions + +Read `java.instructions.md` in my User level Copilot instructions. This session is about Java. + +Use the `new-java-e2e-test-yaml-and-test` skill to create a new failsafe IT test that exercises the non-ergonomic-tool-definition approach to tool definition. + +### What the test must exercise + +The test class should be `LowLevelToolDefinitionIT.java` in `java/src/test/java/com/github/copilot/`. It must demonstrate **all** of the following in a single session: + +1. **`ToolDefinition.create(name, description, schema, handler)`** — define at least two custom tools explicitly with `Map` schemas. +2. **`ToolDefinition.createOverride(name, description, schema, handler)`** — define one tool that overrides a built-in tool. +3. **`invocation.getArgumentsAs(SomeRecord.class)`** — at least one handler must deserialize arguments into a Java record (not `getArguments()` returning raw Map). +4. **`invocation.getArguments()`** — at least one handler must use the raw `Map` accessor. +5. **`ToolSet` with `addCustom("*").addBuiltIn("web_fetch")`** — pass `setAvailableTools(...)` on the `SessionConfig`. +6. **Handler mutates state** — one tool handler should mutate a field on the test class and the test should assert that the field was updated after the response. +7. **Handler returns `CompletableFuture.completedFuture(...)`** — all handlers return completed futures (as is the current pattern). + +### Concrete test design + +#### Snapshot category + +`tools` (reuse the existing category under `test/snapshots/tools/`). + +#### Snapshot file + +`test/snapshots/tools/low_level_tool_definition.yaml` + +#### Java test method name + +`lowLevelToolDefinition` (converts to `low_level_tool_definition` for snapshot lookup). + +#### Tool definitions for the test + +| Tool | Factory | Name | Description | Schema | Handler behavior | +|------|---------|------|-------------|--------|-----------------| +| Set Phase | `create` | `set_current_phase` | "Sets the current phase of the agent" | `{ type: object, properties: { phase: { type: string, enum: [searching, analyzing, done] } }, required: [phase] }` | Deserializes via `getArgumentsAs(PhaseArgs.class)` where `record PhaseArgs(String phase) {}`. Mutates a `currentPhase` field on the test. Returns `"Phase set to " + phase`. | +| Search | `create` | `search_items` | "Search for items by keyword" | `{ type: object, properties: { keyword: { type: string } }, required: [keyword] }` | Uses `getArguments()` raw Map. Returns a fixed string like `"Found: item_alpha, item_beta"`. | +| Override grep | `createOverride` | `grep` | "Custom grep override" | `{ type: object, properties: { query: { type: string } }, required: [query] }` | Uses `getArguments()`. Returns `"CUSTOM_GREP: " + query`. | + +#### Prompt + +``` +First, set the current phase to 'analyzing'. Then search for items with keyword 'copilot'. Report the phase and search results. +``` + +#### YAML snapshot structure + +Two conversations (one for the tool-call turn, one for the final response turn after tool results are provided): + +- **Conversation 1** (tool call turn): system `${system}` + user prompt → assistant with `tool_calls` for `set_current_phase` and `search_items`. +- **Conversation 2** (final response turn): full history including tool results → assistant final content mentioning "analyzing", "item_alpha", "item_beta". + +Study the existing snapshot files in `test/snapshots/tools/` carefully. In particular, study the snapshot file for the `testInvokesCustomTool` test in `ToolsTest.java` (`test/snapshots/tools/invokes_custom_tool.yaml`). It shows how tool call and tool result conversations are structured. Additionally, study `test/snapshots/tools/should_execute_multiple_custom_tools_in_parallel_single_turn.yaml` which shows multiple parallel tool calls in a single turn. + +#### Assertions + +1. `response` is not null. +2. Response content contains `"analyzing"` (confirming the phase tool was called). +3. Response content contains `"item_alpha"` or `"item_beta"` (confirming search tool was called). +4. The `currentPhase` field on the test class equals `"analyzing"` (confirming handler mutated state). + +#### Session config + +```java +new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setAvailableTools(new ToolSet().addCustom("*").addBuiltIn("web_fetch")) + .setTools(List.of(setPhaseTool, searchTool, grepOverrideTool)) +``` + +### Step-by-step execution + +1. Create the YAML snapshot file at `test/snapshots/tools/low_level_tool_definition.yaml`. +2. Create the Java IT file at `java/src/test/java/com/github/copilot/LowLevelToolDefinitionIT.java`. +3. Run `mvn spotless:apply` from the `java/` directory (using the background + log pattern from `java.instructions.md`). +4. Run the test in isolation: + ```sh + cd java + LOG="$(date +%Y%m%d-%H%M)-job-logs.txt" && mvn failsafe:integration-test -Dit.test="LowLevelToolDefinitionIT#lowLevelToolDefinition" -Denforcer.skip=true > "$LOG" 2>&1 & tail -f "$LOG" + ``` +5. Fix any failures. Iterate until the isolated test passes cleanly. +6. Run the full build: + ```sh + cd java + LOG="$(date +%Y%m%d-%H%M)-job-logs.txt" && mvn clean verify > "$LOG" 2>&1 & tail -f "$LOG" + ``` +7. Fix any failures from the full build. Iterate until `mvn clean verify` passes cleanly. diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/20260615-prompts.md b/1682-java-tool-ergonomics-prompts-remove-before-merge/20260615-prompts.md new file mode 100644 index 000000000..6d2f3ec1a --- /dev/null +++ b/1682-java-tool-ergonomics-prompts-remove-before-merge/20260615-prompts.md @@ -0,0 +1,613 @@ +### 18:28 Prompt + +Consider this C# usage of our API: + +```C# + [DisplayName("set_current_phase")] + [Description("Sets the current phase of the agent. Use this to report progress.")] + private void SetCurrentPhase(Phase phase) + { + Phase = phase; + updateUi(); + } + +... + + Session = await client.CreateSessionAsync(new() + { + OnPermissionRequest = PermissionHandler.ApproveAll, + SystemMessage = systemMessageConfig, + AvailableTools = new ToolSet().AddCustom("*").AddBuiltIn("web_fetch"), + Tools = [ + CopilotTool.DefineTool(SetCurrentPhase), + CopilotTool.DefineTool(ReportIntent, new() { OverridesBuiltInTool = true }), + CopilotTool.DefineTool(database.SearchProperties), + ], + }); +``` + +How would one do this with the Java API? + +### 18:30 Response + +Searched for regex `public static ToolDefinition create` (`**/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java`), 4 results + +Searched for regex `class ToolSet|addCustom|addBuiltIn|AddCustom|AddBuiltIn` (`**/java/src/main/java/com/github/copilot/rpc/ToolSet.java`), 8 results + +Searched for regex `setAvailableTools|setTools|List` (`**/java/src/main/java/com/github/copilot/rpc/SessionConfig.java`), 5 results + +Here's the Java equivalent: + +```java +var session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSystemMessage(systemMessageConfig) + .setAvailableTools(new ToolSet().addCustom("*").addBuiltIn("web_fetch")) + .setTools(List.of( + ToolDefinition.create("set_current_phase", "Sets the current phase of the agent. Use this to report progress.", + Map.of("type", "object", + "properties", Map.of("phase", Map.of("type", "string", "enum", List.of("searching", "analyzing", "done"))), + "required", List.of("phase")), + invocation -> { + Phase phase = invocation.getArgumentsAs(PhaseArgs.class).phase(); + this.phase = phase; + updateUi(); + return CompletableFuture.completedFuture("Phase set to " + phase); + }), + ToolDefinition.createOverride("report_intent", "Reports the agent's intent", + Map.of("type", "object", "properties", Map.of(/* ... */)), + invocation -> { /* ... */ }), + ToolDefinition.create("search_properties", "Search property listings", + Map.of("type", "object", "properties", Map.of(/* ... */)), + invocation -> database.searchProperties(invocation.getArguments())) + ))).get(30, TimeUnit.SECONDS); +``` + +The key differences from the C# version: + +| C# | Java | +|---|---| +| `CopilotTool.DefineTool(SetCurrentPhase)` — infers name, description, and schema from the method's attributes via reflection | `ToolDefinition.create(name, description, schema, handler)` — all explicit | +| `new() { OverridesBuiltInTool = true }` (options object) | `ToolDefinition.createOverride(...)` (dedicated factory) | +| `[DisplayName]` / `[Description]` attributes on the method | No annotation-based discovery; you provide the strings directly | +| Implicit JSON Schema generation from parameter types | You supply the `Map` schema manually | + +The Java SDK does not currently have a reflection/annotation-based `DefineTool` that can inspect a method and auto-generate the name, description, and parameter schema. Everything is explicit. + +### 18:31 Prompt + +Oof. That is very ugly. So you're saying there is no `DefineTool` analog in Java? + +### 18:34 Response + +Searched for regex `defineTool|DefineTool|@Tool|@CopilotTool|annotation.*tool|reflect.*tool|schema.*generat` (`**/java/src/main/java/**`), no results + +Correct — there is no `DefineTool` analog in the Java SDK today. No reflection-based discovery, no annotations, no automatic JSON Schema generation from parameter types. The only API is `ToolDefinition.create(name, description, schema, handler)` where you supply everything manually. + +The C# version gets its ergonomics from `CopilotTool.DefineTool(MethodGroup)` which uses .NET reflection to pull `[DisplayName]`, `[Description]`, and parameter types to auto-generate the schema. The Go SDK has `DefineTool[T, U](name, desc, handler)` which uses generics to derive the schema from the input type `T`. Python has `@define_tool` which introspects the decorated function's signature and docstring. + +Java *could* support something similar — e.g. a `DefineTool.from(instance, "methodName")` that reflects on the method's parameters and annotations, or a generic approach using a record type for args (similar to Go). But that doesn't exist yet. + +### 18:34 Prompt + +Given that `ToolDefinition` is already a record, run with that fact and sketch out what a `DefineTool` analog might look like. + +### 18:38 Response + +Here's a sketch leveraging records as the args type (similar to Go's `DefineTool[T, U]`): + +```java +// ─── User defines args as a record ─────────────────────────────────────────── + +record WeatherArgs( + @Param("City name") String location, + @Param(value = "Temperature unit", required = false) String unit +) {} + +// ─── Usage: one-liner tool definition ──────────────────────────────────────── + +var tool = ToolDefinition.define("get_weather", "Get current weather for a location", + WeatherArgs.class, + (args, invocation) -> { + String weather = fetchWeather(args.location(), args.unit()); + return CompletableFuture.completedFuture(weather); + }); +``` + +The handler receives the **deserialized record** directly — no more `invocation.getArgumentsAs(...)` boilerplate. + +--- + +### Implementation sketch + +```java +// ─── New annotation ────────────────────────────────────────────────────────── + +package com.github.copilot.rpc; + +import java.lang.annotation.*; + +@Target(ElementType.RECORD_COMPONENT) +@Retention(RetentionPolicy.RUNTIME) +public @interface Param { + /** Description of this parameter. */ + String value(); + /** Whether this parameter is required. Default true. */ + boolean required() default true; +} + +// ─── New typed handler functional interface ────────────────────────────────── + +@FunctionalInterface +public interface TypedToolHandler { + CompletableFuture handle(T args, ToolInvocation invocation); +} + +// ─── New factory method on ToolDefinition ──────────────────────────────────── + +public record ToolDefinition(...) { + + // Existing factories... + + /** + * Defines a tool with automatic JSON Schema generation from a record type. + * + *

Record components become the schema's {@code properties}. Use + * {@link Param} to annotate descriptions and optional flags. + * + * @param the args record type + * @param name tool name + * @param description tool description + * @param argsType the record class (e.g. {@code WeatherArgs.class}) + * @param handler receives the deserialized record + invocation context + * @return a fully constructed ToolDefinition with auto-generated schema + */ + public static ToolDefinition define( + String name, + String description, + Class argsType, + TypedToolHandler handler) { + + Map schema = SchemaGenerator.fromRecord(argsType); + + ToolHandler rawHandler = invocation -> { + T args = invocation.getArgumentsAs(argsType); + return handler.handle(args, invocation); + }; + + return new ToolDefinition(name, description, schema, rawHandler, null, null, null); + } +} +``` + +--- + +### Schema generation from record components + +```java +package com.github.copilot.rpc; + +import java.lang.reflect.RecordComponent; +import java.util.*; + +final class SchemaGenerator { + + static Map fromRecord(Class type) { + RecordComponent[] components = type.getRecordComponents(); + Map properties = new LinkedHashMap<>(); + List required = new ArrayList<>(); + + for (RecordComponent comp : components) { + Map prop = new LinkedHashMap<>(); + prop.put("type", jsonType(comp.getType())); + + Param param = comp.getAnnotation(Param.class); + if (param != null) { + prop.put("description", param.value()); + if (param.required()) { + required.add(comp.getName()); + } + } else { + // No annotation → required by default + required.add(comp.getName()); + } + + properties.put(comp.getName(), prop); + } + + Map schema = new LinkedHashMap<>(); + schema.put("type", "object"); + schema.put("properties", properties); + if (!required.isEmpty()) { + schema.put("required", required); + } + return schema; + } + + private static String jsonType(Class type) { + if (type == String.class) return "string"; + if (type == int.class || type == Integer.class + || type == long.class || type == Long.class) return "integer"; + if (type == double.class || type == Double.class + || type == float.class || type == Float.class) return "number"; + if (type == boolean.class || type == Boolean.class) return "boolean"; + if (type.isArray() || List.class.isAssignableFrom(type)) return "array"; + return "object"; + } +} +``` + +--- + +### The BRK206 example rewritten + +```java +record PhaseArgs(@Param("The phase to transition to") Phase phase) {} + +record SearchArgs(@Param("Search query") String query, + @Param(value = "Max results", required = false) Integer limit) {} + +var session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSystemMessage(systemMessageConfig) + .setAvailableTools(new ToolSet().addCustom("*").addBuiltIn("web_fetch")) + .setTools(List.of( + ToolDefinition.define("set_current_phase", + "Sets the current phase of the agent. Use this to report progress.", + PhaseArgs.class, + (args, inv) -> { + this.phase = args.phase(); + updateUi(); + return CompletableFuture.completedFuture("Phase set to " + args.phase()); + }), + ToolDefinition.define("report_intent", + "Reports the agent's intent", + ReportIntentArgs.class, + (args, inv) -> { /* ... */ }) + .withOverridesBuiltInTool(true), // would need a wither + ToolDefinition.define("search_properties", + "Search property listings", + SearchArgs.class, + (args, inv) -> database.searchProperties(args.query(), args.limit())) + ))).get(30, TimeUnit.SECONDS); +``` + +--- + +### Trade-offs + +| Pro | Con | +|-----|-----| +| Schema auto-generated from record components | Requires runtime reflection (one-time, at definition) | +| Type-safe handler — no manual `getArgumentsAs()` | Nested/complex schemas (arrays of objects) need more work | +| `@Param` keeps description co-located with the field | New annotation + new interface = new public API surface | +| Records are already idiomatic for value objects in Java 17+ | Enums in records (like `Phase`) need custom mapping logic | +| Very close to Go's `DefineTool[T, U]` ergonomics | Can't infer name/description from the method itself (unlike C#) | + +The main limitation vs C# is that Java doesn't have method-group-to-delegate conversion, so you can't just pass `this::setCurrentPhase` and have it figure out the name and schema. You still provide the name and description strings — but the schema becomes free. + +### 18:41 Prompt + +I have added the folder `langchain4j-00-main` to this VS Code workspace. + +Look at the design of `./langchain4j-core/src/main/java/dev/langchain4j/agent/tool/Tool.java`. Explain how that design differs from the record based one you proposed. + +### 18:46 Response + +Searched for files matching `**/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/Tool.java`, 1 match + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/Tool.java#1-1), lines 1 to 50 + +Searched for text `@interface P` (`**/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/P.java`), 1 result + +Searched for regex `class ToolSpecification|record ToolSpecification|String name|String description|JsonObjectSchema` (`**/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/ToolSpecification.java`), 14 results + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/Tool.java#50-50), lines 50 to 60 + +Searched for regex `toolSpecificationFrom|toolSpecificationsFrom|class ToolSpecifications` (`**/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/ToolSpecifications.java`), 6 results + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/P.java#1-1), lines 1 to 60 + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/ToolSpecifications.java#100-100), lines 100 to 160 + +Here's how the two designs differ: + +--- + +### LangChain4j: **Annotation-on-method** approach + +```java +@Tool("Get current weather for a location") +String getWeather(@P("City name") String location, + @P(value = "Unit", required = false) String unit) { + return fetchWeather(location, unit); +} +``` + +The framework discovers tools by scanning an **object's methods** for `@Tool`. It then: +1. Reflects on the `Method` to get the name (from `@Tool(name=...)` or the method name itself) +2. Reflects on each `Parameter` to build the JSON Schema (using `@P` for description/required) +3. Generates a `ToolSpecification` (name + description + schema) automatically +4. At invocation time, dispatches to the annotated method with deserialized args passed as method parameters + +**The user never writes a schema or a handler lambda** — the method IS the handler. + +--- + +### My record-based proposal: **Record-as-schema + explicit lambda** approach + +```java +record WeatherArgs(@Param("City name") String location, + @Param(value = "Unit", required = false) String unit) {} + +var tool = ToolDefinition.define("get_weather", "Get current weather", + WeatherArgs.class, + (args, invocation) -> CompletableFuture.completedFuture(fetchWeather(args.location(), args.unit()))); +``` + +The record defines the schema shape; the lambda is the handler. + +--- + +### Key differences + +| Aspect | LangChain4j (`@Tool` on method) | Record-based proposal | +|--------|--------------------------------|----------------------| +| **Discovery** | Automatic — scan object for `@Tool` methods | Manual — you explicitly call `define(...)` | +| **Tool name** | Derived from method name or `@Tool(name=...)` | Provided as a string argument | +| **Description** | `@Tool("description")` on the method | Provided as a string argument | +| **Parameter schema** | Generated from method parameter types + `@P` annotations | Generated from record components + `@Param` annotations | +| **Handler** | The annotated method itself — no lambda | Explicit `TypedToolHandler` lambda | +| **Arg delivery** | Each parameter is a separate method argument | Single deserialized record object | +| **Return type** | Any (String sent as-is, others serialized to JSON) | Always `CompletableFuture` | +| **Registration** | Implicit (pass object to framework, it finds all `@Tool` methods) | Explicit (build a `List`) | +| **Async** | Synchronous method (framework handles threading) | Explicitly async via `CompletableFuture` | + +--- + + + +### The fundamental philosophical difference + +LangChain4j treats **the method as the tool**. You annotate existing methods and the framework does everything else. This is closest to C#'s `CopilotTool.DefineTool(SetCurrentPhase)`. + +My record-based proposal treats **the record as the schema** but still requires explicit wiring. It's closer to Go's `DefineTool[T, U]` — you get schema generation for free but still wire things manually. + +If you wanted the LangChain4j-style ergonomics in the Copilot Java SDK, it would look something like: + +```java +class MyTools { + @CopilotTool("Sets the current phase of the agent") + CompletableFuture setCurrentPhase(@Param("The phase") Phase phase) { + this.phase = phase; + updateUi(); + return CompletableFuture.completedFuture("Done"); + } +} + +// Registration: scan an object for @CopilotTool methods +var tools = ToolDefinition.fromObject(myToolsInstance); +``` + +That's the most ergonomic option but requires the most framework machinery (method scanning, reflective invocation, argument marshaling per-parameter rather than per-record). + +### 18:46 Prompt + +Yes, I like the langchain4j approach much better. Let's start by filing an issue in `copilot-sdk`. + +Title + +Java: Ergonomics: Defining tools + +Description + +The current way of defining tools in Java is quite awful. + + +```java +var session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSystemMessage(systemMessageConfig) + .setAvailableTools(new ToolSet().addCustom("*").addBuiltIn("web_fetch")) + .setTools(List.of( + ToolDefinition.create("set_current_phase", "Sets the current phase of the agent. Use this to report progress.", + Map.of("type", "object", + "properties", Map.of("phase", Map.of("type", "string", "enum", List.of("searching", "analyzing", "done"))), + "required", List.of("phase")), + invocation -> { + Phase phase = invocation.getArgumentsAs(PhaseArgs.class).phase(); + this.phase = phase; + updateUi(); + return CompletableFuture.completedFuture("Phase set to " + phase); + }), + ToolDefinition.createOverride("report_intent", "Reports the agent's intent", + Map.of("type", "object", "properties", Map.of(/* ... */)), + invocation -> { /* ... */ }), + ToolDefinition.create("search_properties", "Search property listings", + Map.of("type", "object", "properties", Map.of(/* ... */)), + invocation -> database.searchProperties(invocation.getArguments())) + ))).get(30, TimeUnit.SECONDS); +``` + +Compare that with what you get in langchain4j: + +```java +@Tool("Get current weather for a location") +String getWeather(@P("City name") String location, + @P(value = "Unit", required = false) String unit) { + return fetchWeather(location, unit); +} +``` + +We simply must do better. + + + +### 18:51 Response + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && gh issue create \ + --repo github/copilot-sdk \ + --title "Java: Ergonomics: Defining tools" \ + --body 'The current way of defining tools in Java is quite awful. + +```java +var session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSystemMessage(systemMessageConfig) + .setAvailableTools(new ToolSet().addCustom("*").addBuiltIn("web_fetch")) + .setTools(List.of( + ToolDefinition.create("set_current_phase", "Sets the current phase of the agent. Use this to report progress.", + Map.of("type", "object", + "properties", Map.of("phase", Map.of("type", "string", "enum", List.of("searching", "analyzing", "done"))), + "required", List.of("phase")), + invocation -> { + Phase phase = invocation.getArgumentsAs(PhaseArgs.class).phase(); + this.phase = phase; + updateUi(); + return CompletableFuture.completedFuture("Phase set to " + phase); + }), + ToolDefinition.createOverride("report_intent", "Reports the agent'''s intent", + Map.of("type", "object", "properties", Map.of(/* ... */)), + invocation -> { /* ... */ }), + ToolDefinition.create("search_properties", "Search property listings", + Map.of("type", "object", "properties", Map.of(/* ... */)), + invocation -> database.searchProperties(invocation.getArguments())) + ))).get(30, TimeUnit.SECONDS); +``` + +Compare that with what you get in [langchain4j](https://github.com/langchain4j/langchain4j): + +```java +@Tool("Get current weather for a location") +String getWeather(@P("City name") String location, + @P(value = "Unit", required = false) String unit) { + return fetchWeather(location, unit); +} +``` + +We simply must do better.' + +Filed: [#1682](https://github.com/github/copilot-sdk/issues/1682) — Java: Ergonomics: Defining tools. + +### 18:52 Prompt + +Define a new ADR in `java/docs/adr/adr-005-tool-definition.md`. Look at the existing ADRs in there, and follow ADR best practice. Create the new ADR describing the approaches we evaluated and how we ended up choosing the langchain4j approach as shown in https://github.com/langchain4j/langchain4j/blob/main/docs/docs/tutorials/tools.md#high-level-tool-api . + +Be explicit about how we looked at the record approach, the current approach, and chose the langchain4j approach for its Java idiomatic qualities. + +### 19:02 Response + +Searched for files matching `**/java/docs/adr/*.md`, 4 matches + +Read [](file:///home/edburns/workareas/copilot-sdk/java/docs/adr/adr-004-copilotexperimental.md#1-1), lines 1 to 80 + +Now let me fetch the langchain4j tools tutorial for reference: + + + +Fetched https://github.com/langchain4j/langchain4j/blob/main/docs/docs/tutorials/tools.md + +Now I have all the context. Let me create the ADR: + +Created [](file:///home/edburns/workareas/copilot-sdk/java/docs/adr/adr-005-tool-definition.md) + +Created adr-005-tool-definition.md. It documents all three options evaluated (status quo, record-as-schema, annotation-on-method), the rationale for choosing the langchain4j-style approach, and links to the issue and reference implementations. + +Made changes. + + +### 19:02 Prompt + + + +Is there a way around this? + +----- +- Build requirement: recommend `-parameters` javac flag in documentation. Without it, developers must use `@Param(name="...")` explicitly (same trade-off as langchain4j, Spring, etc.). +----- + +We did an automatic annotation processor for `@CopilotExperimental`. Can't we do something similar here? + +### 19:06 Response + +Yes — this is completely solvable with an annotation processor. + +The key insight: **`javax.lang.model.element.VariableElement.getSimpleName()` always returns the real parameter name at compile time**, regardless of whether `-parameters` is passed to `javac`. The `-parameters` flag only controls whether those names survive into the `.class` bytecode for runtime reflection. An annotation processor sees the source-level names unconditionally. + +### How it would work + +The processor runs at compile time, finds all `@CopilotTool`-annotated methods, and generates a metadata class (or resource) that bakes in the parameter names, types, and descriptions: + +```java +// GENERATED — do not edit +final class MyTools$$CopilotToolMeta { + static List definitions(MyTools instance) { + return List.of( + new ToolDefinition("set_current_phase", + "Sets the current phase of the agent.", + Map.of("type", "object", + "properties", Map.of("phase", Map.of("type", "string", "description", "The phase to transition to")), + "required", List.of("phase")), + invocation -> { + Phase phase = invocation.getArgumentsAs(Phase.class); + return CompletableFuture.completedFuture( + instance.setCurrentPhase(phase)); + }, null, null, null) + ); + } +} +``` + +At runtime, `ToolDefinition.fromObject(myTools)` just loads the generated `$$CopilotToolMeta` class — zero reflection, zero dependency on `-parameters`. + +### Precedent in the ecosystem + +| Framework | Approach | +|-----------|----------| +| **Micronaut** | Annotation processor generates all DI metadata at compile time — no runtime reflection, no `-parameters` needed | +| **Dagger 2** | Processor generates `_Factory` / `_MembersInjector` classes | +| **MapStruct** | Processor generates mapper implementations from interface method signatures | +| **Our own `@CopilotExperimental`** | Processor walks declared elements via JSR 269 | + +### Advantages over the `-parameters` fallback + +1. **No build configuration requirement** — works out of the box with any `javac` invocation. +2. **No runtime reflection** — the generated code calls the method directly. Faster, GraalVM-native-friendly, no `setAccessible` issues. +3. **Compile-time validation** — the processor can emit errors if `@Param` is missing on a parameter, if a type isn't serializable, if two tools have the same name, etc. (same as we did for `@CopilotExperimental`). +4. **Schema correctness guaranteed** — the schema is computed once from the source AST, not guessed at runtime from erased generics. + +### Trade-off vs. pure runtime reflection (langchain4j default) + +| | Annotation processor | Runtime reflection | +|---|---|---| +| Requires `-parameters`? | No | Yes (or `@P(name=...)`) | +| GraalVM native-image friendly? | Yes | Needs reflection config | +| Compile-time error checking? | Yes | Fails at runtime | +| Extra generated source files? | Yes | None | +| Works without running the processor? | No — but fails loudly at compile time | Yes (degraded) | + +So yes: we can follow the same JSR 269 pattern as `@CopilotExperimental` to eliminate the `-parameters` requirement entirely. The processor sees the real names from source, bakes them into generated code, and the runtime never needs to call `Parameter.getName()`. + + + +Want me to update the ADR to reflect this? + +### 19:07 Prompt + +Yes please. + +### 19:14 Response + +Read [](file:///home/edburns/workareas/copilot-sdk/java/docs/adr/adr-005-tool-definition.md#195-195), lines 195 to 220 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/docs/adr/adr-005-tool-definition.md#218-218), lines 218 to 240 + +Replacing 15 lines with 10 lines in [](file:///home/edburns/workareas/copilot-sdk/java/docs/adr/adr-005-tool-definition.md) + + +``` + +``` +Done. Added the `@CopilotExperimental` decision as a consequence bullet — the new annotations and utility methods will be gated behind experimental opt-in until the API stabilizes. + +Made changes. diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/20260616-prompts.md b/1682-java-tool-ergonomics-prompts-remove-before-merge/20260616-prompts.md new file mode 100644 index 000000000..931f02d08 --- /dev/null +++ b/1682-java-tool-ergonomics-prompts-remove-before-merge/20260616-prompts.md @@ -0,0 +1,218 @@ +# DAY: 2026-06-16 + +### 19:31 Prompt + +Consider this C# usage of our API: + +```C# + [DisplayName("set_current_phase")] + [Description("Sets the current phase of the agent. Use this to report progress.")] + private void SetCurrentPhase(Phase phase) + { + Phase = phase; + updateUi(); + } + +... + + Session = await client.CreateSessionAsync(new() + { + OnPermissionRequest = PermissionHandler.ApproveAll, + SystemMessage = systemMessageConfig, + AvailableTools = new ToolSet().AddCustom("*").AddBuiltIn("web_fetch"), + Tools = [ + CopilotTool.DefineTool(SetCurrentPhase), + CopilotTool.DefineTool(ReportIntent, new() { OverridesBuiltInTool = true }), + CopilotTool.DefineTool(database.SearchProperties), + ], + }); +``` + +According to our chat yesterday, here's how you'd do it with our Java API. For discussion call this the non-ergonomic-tool-definition. + +```java +var session = client.createSession(new SessionConfig() + .setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setSystemMessage(systemMessageConfig) + .setAvailableTools(new ToolSet().addCustom("*").addBuiltIn("web_fetch")) + .setTools(List.of( + ToolDefinition.create("set_current_phase", "Sets the current phase of the agent. Use this to report progress.", + Map.of("type", "object", + "properties", Map.of("phase", Map.of("type", "string", "enum", List.of("searching", "analyzing", "done"))), + "required", List.of("phase")), + invocation -> { + Phase phase = invocation.getArgumentsAs(PhaseArgs.class).phase(); + this.phase = phase; + updateUi(); + return CompletableFuture.completedFuture("Phase set to " + phase); + }), + ToolDefinition.createOverride("report_intent", "Reports the agent's intent", + Map.of("type", "object", "properties", Map.of(/* ... */)), + invocation -> { /* ... */ }), + ToolDefinition.create("search_properties", "Search property listings", + Map.of("type", "object", "properties", Map.of(/* ... */)), + invocation -> database.searchProperties(invocation.getArguments())) + ))).get(30, TimeUnit.SECONDS); +``` + +Also according to our chat yesterday: + +----- + +The key differences from the C# version: + +| C# | Java | +|---|---| +| `CopilotTool.DefineTool(SetCurrentPhase)` — infers name, description, and schema from the method's attributes via reflection | `ToolDefinition.create(name, description, schema, handler)` — all explicit | +| `new() { OverridesBuiltInTool = true }` (options object) | `ToolDefinition.createOverride(...)` (dedicated factory) | +| `[DisplayName]` / `[Description]` attributes on the method | No annotation-based discovery; you provide the strings directly | +| Implicit JSON Schema generation from parameter types | You supply the `Map` schema manually | + +The Java SDK does not currently have a reflection/annotation-based `DefineTool` that can inspect a method and auto-generate the name, description, and parameter schema. Everything is explicit. + +------ + +We are working on issue https://github.com/github/copilot-sdk/issues/1682 that addresses this gap. + +But first, I want to add test coverage for the way to do it that apparently currently does work. + +Write a plan to `1682-java-tool-ergonomics-prompts-remove-before-merge/1682-low-level-tool-definition.md` to instruct a `copilot --yolo` session to: + +- Read `java.instructions.md` in my User level Copilot instructions. This session is about Java. +- Use the `new-java-e2e-test-yaml-and-test` skill to create a new failsafe IT test that exercises the non-ergonomic-tool-definition approach to tool definition. +- Run the test in isolation until you get a clean run of just that test. +- Run all the automated tests according to the `java.instructions.md` and `mvn clean verify`. + + + +### 19:40 Prompt + +Right now, the most important thing we did in this session was learn how to create a net new snapshot YAML, `test/snapshots/system_message_sections/should_use_replaced_identity_section_in_response.yaml` and use it in a net-new failsafe IT test. + +This is an important skill. + +I've never used skills before. + +Help me package up this knowledge as a reusable skill. + +I guess skills have names, right? Well, let's call this `new-java-e2e-test-yaml-and-test`, using whatever naming convention one uses for skills. + +Explain what you're donig step by step. I have a need for this skill right now. + +### 19:53 Prompt + +▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ +◐ The user wants to create a reusable skill for creating new E2E test YAML snapshots and corresponding failsafe IT tests. Let me first understand how skills work in this repo - I can see from the environment that there's already a `java-coding-skill` defined. Let me look at how skills are structured. + +● Let me look at how skills are structured in this repo so I can explain and create one for you. + +● Find existing skills (shell) + │ find /home/edburns/workareas/copilot-sdk/.github/skills -type f | sort + └ 4 lines... + +● List directory .github/skills/java-coding-skill + └ 1 file found + +● List directory .github/skills/rust-coding-skill + └ 2 files found + +● Read SKILL.md + │ .github/skills/rust-coding-skill/SKILL.md + └ L1:30 (30 lines read) + +● Read SKILL.md + │ .github/skills/java-coding-skill/SKILL.md + └ L1:10 (10 lines read) + +● Good. Here's what I'm seeing and what I'll do: + + How Skills Work (Step by Step) + + 1. Skills live in .github/skills// — each skill is a directory containing at minimum a SKILL.md file + 2. SKILL.md has YAML frontmatter with name and description — the description tells Copilot when to invoke the skill + 3. The body of SKILL.md is the instruction content — the "knowledge" that gets loaded when the skill is invoked + 4. Optional extra files (like examples.md) can be included for reference material + + What I'll Create + + For your new-e2e-test-yaml-and-test skill, I'll create: + + - .github/skills/new-e2e-test-yaml-and-test/SKILL.md — the main skill with step-by-step instructions on how to create a new YAML snapshot and corresponding failsafe IT test + - .github/skills/new-e2e-test-yaml-and-test/examples.md — the actual example files we created this session as reference + + Let me create it now: + +✗ Create skill directory (shell) + │ mkdir -p /home/edburns/workareas/copilot-sdk/.github/skills/new-e2e-test-yaml-and-test + └ The user rejected this tool call. User feedback: I named it wrong. The skill should be called + `new-java-e2e-test-yaml-and-test` + +● Create skill directory (shell) + │ mkdir -p /home/edburns/workareas/copilot-sdk/.github/skills/new-java-e2e-test-yaml-and-test + └ 1 line... + +● IDE connection lost: Visual Studio Code - Insiders closed + +● Now let me create the main SKILL.md: + +● Create SKILL.md +222 + └ .github/skills/new-java-e2e-test-yaml-and-test/SKILL.md + +● Now let me add the examples file with the actual artifacts from this session: + +● Get example YAML (shell) + │ cat /home/edburns/workareas/copilot-sdk/test/snapshots/system_message_sections/should_use_replaced_identity_section_in + │ _response.yaml + └ 16 lines... + +● All permissions are now enabled. Tool, path, and URL requests will be automatically approved. + +● Get example test (shell) + │ cat /home/edburns/workareas/copilot-sdk/java/src/test/java/com/github/copilot/SystemMessageSectionsIT.java + └ 193 lines... + +● Create examples.md +178 + └ .github/skills/new-java-e2e-test-yaml-and-test/examples.md + +● Done. Here's what I created: + + .github/skills/new-java-e2e-test-yaml-and-test/ + ├── SKILL.md ← Main instructions (when/how to create new E2E tests) + └── examples.md ← Real examples from this session (simple + tool-call) + + How it works: + + - The description in the frontmatter tells Copilot to invoke this skill whenever you're creating a new Java E2E integration test with a YAML snapshot + - SKILL.md covers: YAML format, proxy matching logic, the Java test template, common pitfalls, and the key constraint that you must handcraft YAMLs (can't record via Java) + - examples.md has two working examples — the simple Botanica test and the multi-turn tool-call test + + The skill is ready to use. Next time you (or Copilot) need to add a new E2E test, the skill will be available as new-java-e2e-test-yaml-and-test. + +### 19:54 Prompt + +Ok, now, in a subsequent session, I will want to use the new skill `new-java-e2e-test-yaml-and-test`. + +How do I instruct the agent to use this skill to do its job? + +Is it something like + +`Use the skill new-java-e2e-test-yaml-and-test to create a new test and yaml that exercises the feature we just created.` + +Or something else? + +### 19:56 Response + + Based on the documentation, here's how skills work: + + You don't need to explicitly invoke skills. Skills are automatically loaded based on their description field in the frontmatter. The agent sees the skill listed in and invokes it when the task matches the description. + + However, you can be explicit if you want. Any of these would work: + + 1. Implicit (recommended): Just describe what you want — the skill's description matches "creating a new Java E2E integration test that requires a new replay proxy YAML snapshot": + Create a new failsafe IT test with a YAML snapshot that exercises the new feature. + 2. Explicit mention: Reference the skill by name to make sure it's invoked: + Use the new-java-e2e-test-yaml-and-test skill to create a test and YAML snapshot for the new feature. + 3. Check available skills: Use /skills in the CLI to see what's loaded, or /env to see the full environment including skills. + + Both approaches work. The agent sees the skill in its available tools and will call it before generating code. The explicit mention is a good safety net if you want to be sure. + diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/20260617-prompts.md b/1682-java-tool-ergonomics-prompts-remove-before-merge/20260617-prompts.md new file mode 100644 index 000000000..8926ad586 --- /dev/null +++ b/1682-java-tool-ergonomics-prompts-remove-before-merge/20260617-prompts.md @@ -0,0 +1,374 @@ +## dd-3018003-implement-tools + +Review the chat history in `copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/20260615-prompts.md`. + +Most important is the content within the XML element `` + +Also important is the content within the XML element `` + +Use `gh` to open and read and understand any issue or PR links to https://github.com/github/copilot-sdk. + +Read and understand the ADR in `java/docs/adr/adr-005-tool-definition.md`. + +We're not yet ready to ask you to write a `copilot --yolo` ready plan to do the work. We need to break it down better. + +Write a markdown file to `1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md` that is a human-targeted plan for how to proceed with the implementation of the `@CopilotTool` ergonomics improvement. + +- At a high level, I know we'll need to define the + + - `@CopilotTool` annotation, with `@CopilotExperimental`. + + - The annotation processor that makes the `@CopilotTool` annotation work. + + - Some way to "register" a tool. A Java idiomatic way of this in C# + + ```C# + CopilotTool.DefineTool(SetCurrentPhase), + CopilotTool.DefineTool(ReportIntent, new() { OverridesBuiltInTool = true }), + CopilotTool.DefineTool(database.SearchProperties), + ``` + + - **MAYBE** something like this: + + ``` + // Registration: scan an object for @CopilotTool methods + var tools = ToolDefinition.fromObject(myToolsInstance); + ``` + + - I say maybe, because I'm not exactly sure how LangChain4J does it. Also, I've been told that micronaut also has a similar thing. I have added the folders `langchain4j-00-main` and `micronaut-core-00` to this VS Code project for reference. + +Break the work down into phases. Note, we've already done these first two phases. + +1. ✅ Define the problem and make an architectural decision, recorded in an ADR. + +2. ✅ Verify the existing way works in Java. (This work is proceeding but as the human DRI, I am confident it will complete successfully.) + +### 13:41 Response + +Updated todo list + +Let me gather all the necessary context first. + +Read [](file:///home/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/20260615-prompts.md#1-1), lines 1 to 100 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/docs/adr/adr-005-tool-definition.md#1-1), lines 1 to 200 + +Read [](file:///home/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/20260615-prompts.md#100-100), lines 100 to 400 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/docs/adr/adr-005-tool-definition.md#200-200), lines 200 to 236 + +Read [](file:///home/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/20260615-prompts.md#400-400), lines 400 to 600 + +Read [](file:///home/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/20260616-prompts.md#1-1), lines 1 to 100 + +Read [](file:///home/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/20260616-prompts.md#100-100), lines 100 to 200 + +Now let me look at how LangChain4J and Micronaut handle tool registration to give good guidance in the plan. + +Research LangChain4J and Micronaut tool registration + +Read [](file:///home/edburns/workareas/copilot-sdk/test/snapshots/tools/low_level_tool_definition.yaml) + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/test/java/com/github/copilot/LowLevelToolDefinitionIT.java#1-1), lines 1 to 100 + +Read [](file:///home/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/add-tests-that-use-low_level_tool_definition.yaml.md) + +Searched for files matching `langchain4j-00-main` · `langchain4j-core/src/main/java/dev/langchain4j/agent/tool/*.java`, 9 matches + +Searched for files matching `micronaut-core-00` · `core-processor/src/main/java/**/*Processor.java`, 1 match + +No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. +If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). +Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. + +Searched for files matching `micronaut-core-00` · `core-processor/src/**/*.java`, 30 matches + +No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. +If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). +Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/Tool.java) + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/ToolSpecifications.java#1-1), lines 1 to 150 + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/P.java#1-1), lines 1 to 100 + +No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. +If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). +Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. + +Searched for text `ToolExecutor` (`langchain4j-00-main`), 39 results + +Searched for files matching `micronaut-core-00` · `core-processor/src/main/java/io/micronaut/inject/writer/**`, 20 matches + +Searched for files matching `langchain4j-00-main` · `langchain4j/src/main/java/dev/langchain4j/service/tool/*.java`, 18 matches + +Searched for text `BeanDefinitionWriter` (`micronaut-core-00` · `core-processor/src/main/java/io/micronaut/inject/**`), 49 results + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j/src/main/java/dev/langchain4j/service/tool/ToolExecutor.java) + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j/src/main/java/dev/langchain4j/service/tool/DefaultToolExecutor.java#1-1), lines 1 to 120 + +No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. +If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). +Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. + +Read [](file:///home/edburns/workareas/micronaut-core-00/core-processor/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java#1-1), lines 1 to 150 + +Searched for files matching `micronaut-core-00` · `core-processor/src/main/java/io/micronaut/inject/processing/**/*.java`, 17 matches + +Read [](file:///home/edburns/workareas/micronaut-core-00/core-processor/src/main/java/io/micronaut/inject/visitor/TypeElementVisitor.java#1-1), lines 1 to 80 + +No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. +If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). +Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. + +Read [](file:///home/edburns/workareas/micronaut-core-00/core-processor/src/main/java/io/micronaut/inject/visitor/BeanElementVisitor.java) + +Searched for files matching `micronaut-core-00` · `core-processor/src/main/java/io/micronaut/inject/*.java`, no matches + +Read [](file:///home/edburns/workareas/micronaut-core-00/core-processor/src/main/java/io/micronaut/inject/processing/AbstractBeanElementCreator.java) + +No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. +If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). +Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. + +Read [](file:///home/edburns/workareas/micronaut-core-00/core-processor/src/main/java/io/micronaut/inject/processing/definition/DefaultElementBeanDefinitionBuilderFactory.java#1-1), lines 1 to 80 + +No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. +If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). +Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. + +No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. +If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). +Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. + +Read [](file:///home/edburns/workareas/micronaut-core-00/core-processor/src/main/java/io/micronaut) + +Searched for files matching `micronaut-core-00` · `core-processor/src/main/java/io/micronaut/inject/*.java`, no matches + +Read [](file:///home/edburns/workareas/micronaut-core-00/core-processor/src/main/java/io/micronaut/inject) + +No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. +If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). +Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j/src/main/java/dev/langchain4j/service/tool/DefaultToolExecutor.java#120-120), lines 120 to 250 + +Searched for text `Processor extends AbstractProcessor` (`micronaut-core-00`), 2 results + +Searched for files matching `micronaut-core-00` · `**/*Processor.java`, 5 matches + +Read [](file:///home/edburns/workareas/micronaut-core-00/inject-java/src/main/java/io/micronaut/annotation/processing/AbstractInjectAnnotationProcessor.java#1-1), lines 1 to 150 + +Read [](file:///home/edburns/workareas/micronaut-core-00/inject-java/src/main/java/io/micronaut/annotation/processing/AbstractInjectAnnotationProcessor.java#150-150), lines 150 to 250 + +Created memory file [](file:///home/edburns/.vscode-server-insiders/data/User/workspaceStorage/3f1fe06cc8e5cdbce2aa19a5a3bd183d/GitHub.copilot-chat/memory-tool/memories/ZGI5Y2E3ZTMtNDc2My00MTdkLWFjMDEtMDlmY2ViM2M0NTcy/tool-registration-research.md) + +Now let me read the reference prompt file and the existing LowLevelToolDefinitionIT test fully: + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/test/java/com/github/copilot/LowLevelToolDefinitionIT.java#100-100), lines 100 to 112 + +Read [](file:///home/edburns/workareas/copilot-sdk/temporary-prompts/add-tests-that-use-should_use_replaced_identity_section_in_response.md) + +Let me look at the existing `@CopilotExperimental` annotation processor for the pattern we'll follow: + +Explore CopilotExperimental processor pattern + +Searched for text `@CopilotExperimental` (`copilot-sdk` · `java/**`), 101 results + +Searched for files matching `copilot-sdk` · `java/**/CopilotExperimental.java`, 1 match + +Searched for files matching `copilot-sdk` · `java/**/AbstractProcessor.java`, no matches + +Searched for files matching `copilot-sdk` · `java/**/Processor.java`, no matches + +Searched for files matching `copilot-sdk` · `java/**/ToolDefinition.java`, 1 match + +Searched for files matching `copilot-sdk` · `java/**/ToolHandler.java`, 1 match + +Searched for files matching `copilot-sdk` · `java/**/ToolInvocation.java`, 1 match + +Searched for files matching `copilot-sdk` · `java/**/ToolSet.java`, 1 match + +Searched for text `AbstractProcessor` (`copilot-sdk` · `java/**`), 4 results + +Searched for text `process(Set annotations` (`copilot-sdk` · `java/**`), 1 result + +Searched for text `Processor` (`copilot-sdk` · `java/**/META-INF/services/**`), 2 results + +Read [](file:///home/edburns/workareas/copilot-sdk/java/pom.xml#1-1), lines 1 to 100 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/CopilotExperimental.java) + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/CopilotExperimentalProcessor.java#1-1), lines 1 to 150 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java#1-1), lines 1 to 100 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolHandler.java) + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolInvocation.java#1-1), lines 1 to 80 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolSet.java#1-1), lines 1 to 100 + +Searched for files matching `copilot-sdk` · `java/**/AllowCopilotExperimental.java`, 1 match + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/CopilotExperimentalProcessor.java#140-140), lines 140 to 165 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/pom.xml#100-100), lines 100 to 200 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/docs/adr/adr-004-copilotexperimental.md) + +Read [](file:///home/edburns/workareas/copilot-sdk/java) + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot) + +Read [](file:///home/edburns/workareas/copilot-sdk/java/pom.xml#1-1), lines 1 to 600 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/AllowCopilotExperimental.java) + +Searched for files matching `copilot-sdk` · `java/src/test/java/**/CopilotExperimentalProcessorTest.java`, 1 match + +Searched for text `CopilotExperimental` (`copilot-sdk` · `java/src/test/**`), 13 results + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolInvocation.java#80-80), lines 80 to 172 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolSet.java#100-100), lines 100 to 123 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/resources/META-INF/services) + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java#80-80), lines 80 to 150 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/test/java/com/github/copilot/CopilotExperimentalProcessorTest.java) + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/resources/META-INF/services/javax.annotation.processing.Processor) + +Searched for files matching `copilot-sdk` · `java/**/module-info.java`, 1 match + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc) + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/module-info.java) + +Read memory + +Created memory file [](file:///home/edburns/.vscode-server-insiders/data/User/workspaceStorage/3f1fe06cc8e5cdbce2aa19a5a3bd183d/GitHub.copilot-chat/memory-tool/memories/ZGI5Y2E3ZTMtNDc2My00MTdkLWFjMDEtMDlmY2ViM2M0NTcy/copilot-experimental-annotation-exploration.md) + +Read [](file:///home/edburns/.vscode-server-insiders/data/User/workspaceStorage/3f1fe06cc8e5cdbce2aa19a5a3bd183d/GitHub.copilot-chat/chat-session-resources/db9ca7e3-4763-417d-ac01-09fceb3c4572/toolu_vrtx_01XThiJD8h5cYY6QPnjSbxVy__vscode-1781713597255/content.txt#1-1), lines 1 to 200 + +Now let me look at the specific LangChain4J implementation for reference on schema generation and tool execution: + +LangChain4J tool schema generation details + +Searched for files matching `langchain4j-00-main` · `langchain4j-core/src/main/java/dev/langchain4j/agent/tool/ToolSpecifications.java`, 1 match + +Searched for files matching `langchain4j-00-main` · `langchain4j-core/src/main/java/dev/langchain4j/agent/tool/Tool.java`, 1 match + +Searched for files matching `langchain4j-00-main` · `langchain4j-core/src/main/java/dev/langchain4j/agent/tool/P.java`, 1 match + +Searched for files matching `langchain4j-00-main` · `langchain4j-core/src/main/java/dev/langchain4j/agent/tool/*Executor*.java`, no matches + +Searched for files matching `langchain4j-00-main` · `langchain4j-core/src/main/java/dev/langchain4j/model/chat/request/json/**`, 13 matches + +No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. +If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). +Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/ToolSpecifications.java) + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/Tool.java) + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/P.java) + +Searched for files matching `langchain4j-00-main` · `langchain4j-core/src/main/java/dev/langchain4j/agent/tool/*.java`, 9 matches + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/ToolSpecifications.java#224-224), lines 224 to 224 + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/model/chat/request/json/JsonSchema.java) + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/model/chat/request/json/JsonSchemaElement.java) + +Searched for regex `class.*JsonSchemaElementUtils|jsonSchemaElementFrom` (`langchain4j-00-main` · `langchain4j-core/src/main/java/dev/langchain4j/**`), 10 results + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/ToolExecutionRequest.java) + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/ToolSpecification.java#1-1), lines 1 to 200 + +No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. +If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). +Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/internal/JsonSchemaElementUtils.java#1-1), lines 1 to 500 + +Searched for regex `Method\.invoke|coerceArguments|parseArguments|jsonToObjects` (`langchain4j-00-main`), 23 results + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/internal/JsonSchemaElementUtils.java#100-100), lines 100 to 500 + +Searched for regex `execute.*Tool|invoke.*request|class.*Handler.*Tool` (`langchain4j-00-main` · `langchain4j-core/**`), 22 results + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-skills/src/main/java/dev/langchain4j/skills/AbstractSkillToolExecutor.java) + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/model/chat/request/json/JsonObjectSchema.java#1-1), lines 1 to 200 + +Searched for regex `class.*AiService|AiServiceImpl` (`langchain4j-00-main` · `langchain4j-core/src/main/java/dev/langchain4j/**`), 28 results + +No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. +If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). +Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/service/tool) + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/service) + +Searched for files matching `langchain4j-00-main` · `langchain4j-core/src/main/java/dev/langchain4j/**/ToolExecutor.java`, no matches + +No matches found. Your search pattern might be excluded completely by either the search.exclude settings or .*ignore files. +If you believe that it should have results, you can check into the .*ignore files and the exclude setting (here are some excluded patterns for reference:[**/node_modules,**/bower_components,**/*.code-search]). +Then if you want to include those files you can call the tool again by setting "includeIgnoredFiles" to true. + +Searched for regex `interface ToolExecutor` (`langchain4j-00-main`), 1 result + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-skills/src/main/java/dev/langchain4j/skills/ActivateSkillToolExecutor.java) + +Searched for regex `.*` (`langchain4j-00-main` · `langchain4j/src/main/java/dev/langchain4j/service/tool/**`), 101 results + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j/src/main/java/dev/langchain4j/service/tool/ToolExecutor.java) + +Searched for regex `class.*ToolInvoker|class.*DefaultToolExecutor|executeToolMethod|invokeToolMethod` (`langchain4j-00-main`), 2 results + +Searched for files matching `langchain4j-00-main` · `langchain4j/src/main/java/dev/langchain4j/service/tool/ToolExecutionResult.java`, 1 match + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j/src/main/java/dev/langchain4j/service/tool/DefaultToolExecutor.java#1-1), lines 1 to 500 + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j/src/main/java/dev/langchain4j/service/tool/ToolExecutionResult.java#1-1), lines 1 to 100 + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j/src/main/java/dev/langchain4j/service/tool/ToolExecutionRequestUtil.java) + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/model/chat/request/json/JsonArraySchema.java) + +Read [](file:///home/edburns/workareas/langchain4j-00-main/langchain4j-core/src/main/java/dev/langchain4j/model/chat/request/json/JsonEnumSchema.java) + +Created memory file [](file:///home/edburns/.vscode-server-insiders/data/User/workspaceStorage/3f1fe06cc8e5cdbce2aa19a5a3bd183d/GitHub.copilot-chat/memory-tool/memories/ZGI5Y2E3ZTMtNDc2My00MTdkLWFjMDEtMDlmY2ViM2M0NTcy/langchain4j-tool-implementation-analysis.md) + +Created 6 todos + +Now I have comprehensive context from all sources. Let me create the plan: + +Created [](file:///home/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md) + +Created dd-3018003-ignorance-reduction-for-implementation-plan.md. + +The plan has 6 phases: + +1. **✅ ADR** — Done (ADR-005). +2. **✅ Low-level path verified** — `LowLevelToolDefinitionIT` passes. +3. **Ignorance reduction** — 8 open questions to resolve before coding (package placement, annotation design, type mapping, generated code shape, `fromObject()` API, module-info, processor registration, access level requirements). +4. **Implementation** — 6 ordered steps: annotations → schema generator → processor → `fromObject()` → E2E test → reflection fallback (optional). +5. **Documentation** — README updates. +6. **Cross-language port** — Evaluate whether the ergonomic API snapshot needs porting (may not — other SDKs have their own ergonomic paths). + +Key architectural decision captured: **langchain4j's API** (annotations on methods) + **Micronaut's implementation strategy** (JSR 269 compile-time code generation, no `-parameters` flag needed). + +Completed: *Write implementation plan for @CopilotTool ergonomics* (1/6) + +Made changes. + diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/20260618-prompts.md b/1682-java-tool-ergonomics-prompts-remove-before-merge/20260618-prompts.md new file mode 100644 index 000000000..03a1b5af8 --- /dev/null +++ b/1682-java-tool-ergonomics-prompts-remove-before-merge/20260618-prompts.md @@ -0,0 +1,135 @@ +### 18:56 Prompt + +Consider `copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md`. + +I am going through the open questions and answering them. + +I am at this one: + +**Open questions:** + +> 1. Should `@CopilotTool` have `@Retention(SOURCE)` (processor-only, like Dagger) or `RUNTIME` (fallback reflection path, like langchain4j)? ADR-005 says "compile-time preferred, runtime fallback" — if we want a fallback path, we need `RUNTIME`. If we commit to processor-only, `SOURCE` is sufficient. +> +> 2. Is `ToolDefer` (the defer config from `ToolDefinition`) needed on the annotation, or is that too niche for v1? +> +> **Recommendation:** Start with `RUNTIME` retention so the reflection fallback works. Defer `ToolDefer` support to a follow-up. + +I need to know what `ToolDefer` is. Make me smart. + +### 19:00 Response + + I’ll trace  ToolDefer  in the SDK and summarize what it does, where it’s used, and why it matters for your annotation design decision. + + ● Search "\bToolDefer\b" 21 lines found + ● Search "defer" in **/*.java (java) 28 lines found + + JV Read ToolDefer.java 69 lines read + JV Read ToolDefinition.java 166 lines read + JV Read ToolDefinitionTest.java 62 lines read + GO Read types.go L1120:1175 (55 lines read) + MD Read README.md L1:260 (155 lines read) + MD Read dd-3018003-ignorance-reduction-for-implementation-plan.md L1:220 (220 lines read) + + ● Search ""defer"|ToolDefer|deferred tools|tool search" (docs, java/docs, java/README.md) No matches found + + MD Read adr-005-tool-definition.md L1:260 (235 lines read) + + ●  ToolDefer  is the per-tool loading policy in  ToolDefinition —it tells Copilot whether to preload a tool definition or allow lazy loading via tool search. + + In this repo it has exactly two values: + + 1.  AUTO  ( "auto" ): tool may be deferred and surfaced through tool search. + 2.  NEVER  ( "never" ): tool is always pre-loaded. + + If unset ( null  / omitted JSON), runtime decides. In Java, this is  com.github.copilot.rpc.ToolDefer  and is wired via  ToolDefinition.createWithDefer(...) ; same concept exists in Go ( Tool.Defer ). + + For your open question: this is an advanced optimization knob (prompt/tool catalog size vs immediate availability), not core ergonomics. So “defer  ToolDefer  support on  @CopilotTool  for v1” is a reasonable cut, as long as low-level  ToolDefinition.createWithDefer(...)  remains available for power users. + + +### 18:59 Prompt + +Now to this one + +> ### 3.3 — `@Param` annotation design + +> **Question:** What attributes does `@Param` need? +> +> ```java +> @Documented +> @Retention(RetentionPolicy.RUNTIME) +> @Target(ElementType.PARAMETER) +> public @interface Param { +> /** Parameter description (sent to the model). */ +> String value() default ""; +> +> /** Parameter name override. Defaults to the actual parameter name. */ +> String name() default ""; +> +> /** Whether this parameter is required. Default true. */ +> boolean required() default true; +> } +> ``` +> +> **Open question:** LangChain4J's `@P` also has `defaultValue()`. Do we need that? Probably not for v1 — the model doesn't use default values; it's for documentation. + +Does the Zod schema that dictates tool definition have a concept of a default value? We can't add one in the Java SDK if it doesn't exist in the lower level Copilot CLI. + +### 19:28 Response + + ● Short answer: you’re not blocked by the lower-level schema. + + The wire-level tool shape treats  parameters  as an open JSON object, not a closed schema with fixed keys: + + • Node RPC type:  Tool.parameters?: { [k: string]: unknown }  ( nodejs/src/generated/rpc.ts ) + • Java generated RPC type:  @JsonProperty("parameters") Map  ( java/src/generated/java/com/github/copilot/generated/rpc/Tool.java ) + • Go type:  Parameters map[string]any  ( go/types.go ) + + So JSON Schema keywords like  default  can flow through. There is just no dedicated first-class  defaultValue  field in  ToolDefinition ; it would have to be emitted into the JSON schema for that parameter ( properties..default ). + + Practically: adding  @Param(defaultValue=...)  is compatible at the transport layer, but likely documentation/hint only unless you also implement SDK/runtime behavior to apply defaults when args are missing. + +### 19:29 Prompt + +Well, of course, if the user specifies a default, then we need to make it so that default does flow through to the tool invocation. Is that implementable? I don't see why not. + +### 19:31 Response + + ● Yes—it’s implementable, and you should do it in the Java SDK layer. + + The CLI/tool wire contract already passes raw argument JSON through; Java can apply defaults before invoking the method. A solid design is: + + 1. Emit  default  into the generated JSON Schema ( properties..default ) for model guidance. + 2. In generated/reflection invocation code, merge defaults into the incoming args map only when a key is absent (not when present with explicit  null , unless you define that behavior). + 3. Then run normal coercion/casting and method invocation. + + Key policy choices to lock down: + + •  required  +  defaultValue : either forbid this combo, or allow it and treat default as satisfying missing input. + • Type parsing:  defaultValue  is annotation text, so processor/reflection path must parse/validate it against param type (fail fast on mismatch). + • Path parity: compile-time generated path and runtime reflection fallback must apply identical defaulting semantics. + + So your instinct is right: no lower-level CLI blocker; it’s a Java API/runtime behavior decision. + +### 19:32 Prompt + +Consider your response. + +> The CLI/tool wire contract already passes raw argument JSON through; Java can apply defaults before invoking the method. A solid design is: +> +> 1. Emit  default  into the generated JSON Schema ( properties..default ) for model guidance. +> 2. In generated/reflection invocation code, merge defaults into the incoming args map only when a key is absent (not when present with explicit  null , unless you define that behavior). +> 3. Then run normal coercion/casting and method invocation. + +> •  required  +  defaultValue : either forbid this combo, or allow it and treat default as satisfying missing input. + +We must forbid this. + +> • Type parsing:  defaultValue  is annotation text, so processor/reflection path must parse/validate it against param type (fail fast on mismatch). + +Yes, we must have this. + +> • Path parity: compile-time generated path and runtime reflection fallback must apply identical defaulting semantics. + +Yes, we must have this. + +Yes, I want to support `defaultValue()` like LangChain4J's `@P`. I am annotating `copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md` as I go. Capture this decision, and the very useful aspects of your previous response and edit `dd-3018003-ignorance-reduction-for-implementation-plan.md` to state this as the Resolution to question 3.3. diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/20260622-prompts.md b/1682-java-tool-ergonomics-prompts-remove-before-merge/20260622-prompts.md new file mode 100644 index 000000000..5f453e841 --- /dev/null +++ b/1682-java-tool-ergonomics-prompts-remove-before-merge/20260622-prompts.md @@ -0,0 +1,1010 @@ +### 15:51 Prompt + +#### Context + +Read `copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md` to get the context. + +In this session, we are working on "Phase 3 — Ignorance reduction: questions to answer before writing code". + +We have completed and answered up to, but not including, "3.4 — Type-to-JSON-Schema mapping". + +I will proceed after this prompt. + +### 15:57 Response + +Updated todo list + +Read [](file:///home/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md#1-1), lines 1 to 200 + +Read [](file:///home/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md#200-200), lines 200 to 400 + +I've read the full implementation plan. We're at **Phase 3, section 3.4 — Type-to-JSON-Schema mapping**. The remaining Phase 3 items to resolve are: + +- **3.4** — Type-to-JSON-Schema mapping (compile-time `TypeMirror` → JSON Schema) +- **3.5** — Generated code shape (access levels, return types, argument deserialization) +- **3.6** — `ToolDefinition.fromObject(Object)` registration API +- **3.7** — `module-info.java` impact +- **3.8** — Processor registration + +Ready when you are. + +### 15:59 Prompt + +Consider: "3.4 — Type-to-JSON-Schema mapping" + +> **Question:** What Java types do we need to map to JSON Schema, and how? + +> Minimum viable set (from langchain4j's `JsonSchemaElementUtils`): +> +> | Java type | JSON Schema | +> |-----------|-------------| +> | `String` | `{"type": "string"}` | +> | `int`, `Integer`, `long`, `Long` | `{"type": "integer"}` | +> | `double`, `Double`, `float`, `Float` | `{"type": "number"}` | +> | `boolean`, `Boolean` | `{"type": "boolean"}` | +> | `enum` types | `{"type": "string", "enum": ["V1", "V2", ...]}` | +> | `List`, `Collection` | `{"type": "array", "items": }` | +> | `Map` | `{"type": "object"}` (opaque — no inner schema) | +> | Records / POJOs | `{"type": "object", "properties": {...}, "required": [...]}` | +> + +The entire GitHub Copilot SDK for Java is really a JSON RPC wrapper to communicate with the GitHub Copilot CLI. As such, it already has established user expectations for type mappings. The reasonable answer to question 3.4 is: all of the ones mapped elsewhere in the API. + +You suggested a minimum viable set. Looking at the code in packages `com.github.copilot.rpc` and `com.github.copilot.generated.rpc`, produce the maximum viable set in a similar table format. + +### 16:09 Response + +Updated todo list + +Catalog Java types in rpc packages + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc) + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/generated/java/com/github/copilot/generated/rpc) + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java#1-1), lines 1 to 200 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefinition.java) + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolHandler.java#1-1), lines 1 to 50 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolInvocation.java#1-1), lines 1 to 100 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/SessionConfig.java#1-1), lines 1 to 200 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java#200-200), lines 200 to 400 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/SessionConfig.java#200-200), lines 200 to 700 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolInvocation.java#100-100), lines 100 to 172 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/SessionConfig.java#700-700), lines 700 to 1200 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java#400-400), lines 400 to 600 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/SessionConfig.java#1200-1200), lines 1200 to 1700 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java#600-600), lines 600 to 700 + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && find java/src/main/java/com/github/copilot/rpc/ -name "*.java" -exec wc -l {} + | sort -n | tail -5 + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rn "private\|public\|protected" java/src/main/java/com/github/copilot/rpc/*.java | grep -E "(private|public|protected)\s+(static\s+)?(final\s+)?\S+\s+\w+" | grep -v "return\|this\|void\|class\|interface\|enum\|record\|package\|import\|@\|//\|/\*\|\*" | grep -oP '(private|public|protected)\s+(static\s+)?(final\s+)?(\S+(<[^>]+>)?)\s+\w+' | grep -oP '(private|public|protected)\s+(static\s+)?(final\s+)?\K\S+(<[^>]+>)?' | sort -u + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rn "CompletableFuture" java/src/main/java/com/github/copilot/rpc/*.java | grep -oP 'CompletableFuture<[^>]+>' | sort -u + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rn "Map<" java/src/main/java/com/github/copilot/rpc/*.java | grep -oP 'Map<[^>]+>' | sort -u + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rl "^public enum\|^public record\|^public class\|^public interface\|^public final class\|^public sealed\|^public abstract" java/src/main/java/com/github/copilot/rpc/*.java | xargs grep -l "^public enum" 2>/dev/null + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rl "^public record" java/src/main/java/com/github/copilot/rpc/*.java + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rl "^public interface\|^public @FunctionalInterface\|@FunctionalInterface" java/src/main/java/com/github/copilot/rpc/*.java + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && for f in ProviderConfig NamedProviderConfig ProviderModelConfig ModelInfo ModelCapabilitiesOverride ModelCapabilities ModelSupports ModelLimits ModelVisionLimits ModelBilling ModelPolicy McpServerConfig McpStdioServerConfig McpHttpServerConfig CustomAgentConfig DefaultAgentConfig InfiniteSessionConfig SystemMessageConfig MemoryConfiguration LargeToolOutputConfig TelemetryConfig; do echo "=== $f ==="; grep -n "private\|public record\|public class\|public enum\|public sealed" java/src/main/java/com/github/copilot/rpc/${f}.java 2>/dev/null | head -30; done + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && for f in SessionHooks CommandDefinition CommandContext CommandWireDefinition ElicitationSchema ElicitationContext ElicitationParams ElicitationResult SectionOverride PermissionInvocation PermissionRequest PermissionRequestResult PermissionRequestResultKind UserInputRequest UserInputResponse UserInputInvocation AutoModeSwitchInvocation AutoModeSwitchRequest ExitPlanModeInvocation ExitPlanModeRequest ExitPlanModeResult HookInvocation PreToolUseHookInput PostToolUseHookInput PostToolUseFailureHookInput PreMcpToolCallHookInput SessionStartHookInput SessionEndHookInput Attachment BlobAttachment MessageAttachment InputOptions; do echo "=== $f ==="; grep -n "private\|public record\|public class\|public enum\|public sealed" java/src/main/java/com/github/copilot/rpc/${f}.java 2>/dev/null | head -20; done + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && for f in AzureOptions ModelBillingTokenPrices ToolBinaryResult ToolResultObject ToolSet BuiltInTools SessionCapabilities SessionContext SendMessageRequest SendMessageResponse MessageOptions CreateSessionRequest CreateSessionResponse ResumeSessionRequest ResumeSessionConfig ResumeSessionResponse AgentInfo SessionLifecycleEvent SessionLifecycleEventTypes SessionLifecycleEventMetadata SessionListFilter SessionMetadata GetAuthStatusResponse GetStatusResponse GetModelsResponse GetMessagesResponse PingResponse CloudSessionOptions CloudSessionRepository SessionUiApi SessionUiCapabilities; do echo "=== $f ==="; grep -n "private\|public record\|public class\|public enum\|public sealed\|public interface" java/src/main/java/com/github/copilot/rpc/${f}.java 2>/dev/null | head -30; done + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && for f in PermissionRequestResultKind PostToolUseFailureHookOutput PreMcpToolCallHookOutput PostToolUseHookOutput PreToolUseHookOutput SessionStartHookOutput SessionEndHookOutput UserPromptSubmittedHookInput UserPromptSubmittedHookOutput JsonRpcError JsonRpcRequest JsonRpcResponse ListSessionsResponse DeleteSessionResponse GetForegroundSessionResponse GetLastSessionIdResponse GetSessionMetadataResponse SetForegroundSessionRequest SetForegroundSessionResponse; do echo "=== $f ==="; head -50 java/src/main/java/com/github/copilot/rpc/${f}.java 2>/dev/null | grep -E "private |public record|public class|JsonProperty"; done + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/SendMessageResponse.java) + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/SessionUiApi.java) + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/CreateSessionResponse.java) + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ModelBilling.java) + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && find java/src/generated/java/com/github/copilot/generated/rpc/ -name "*.java" | wc -l + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rn "private\|public record\|public enum\|public class\|public interface\|public sealed" java/src/generated/java/com/github/copilot/generated/rpc/*.java | grep -E "^\S+:\d+:\s+(private|public)\s+" | grep -oP '(private|public)\s+(static\s+)?(final\s+)?(\S+(<[^>]+>)?)\s+\w+' | grep -oP '(private|public)\s+(static\s+)?(final\s+)?\K\S+(<[^>]+>)?' | sort -u + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "private " java/src/generated/java/com/github/copilot/generated/rpc/*.java | sed 's/^[[:space:]]*//' | grep "^private " | sed 's/private //' | sed 's/ [a-zA-Z_][a-zA-Z0-9_]*;.*//' | sed 's/ [a-zA-Z_][a-zA-Z0-9_]* =.*//' | sort -u + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rPoh '(?<=private\s)\S+(?:\s*<[^>]+>)?' java/src/generated/java/com/github/copilot/generated/rpc/*.java | sort -u + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/generated/java/com/github/copilot/generated/rpc/SessionSendParams.java) + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/generated/java/com/github/copilot/generated/rpc/SessionRpc.java#1-1), lines 1 to 80 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/generated/java/com/github/copilot/generated/rpc/Model.java) + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/generated/java/com/github/copilot/generated/rpc/ServerRpc.java#1-1), lines 1 to 80 + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "^public enum" java/src/generated/java/com/github/copilot/generated/rpc/*.java | sed 's/public enum //' | sed 's/ {.*//' | sort -u + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "^public record\|^public class\|^public interface\|^public sealed\|^public final class" java/src/generated/java/com/github/copilot/generated/rpc/*.java | sed 's/public record //' | sed 's/public class //' | sed 's/public interface //' | sed 's/public sealed interface //' | sed 's/public final class //' | sed 's/(.*//' | sed 's/ {.*//' | sed 's/ extends.*//' | sed 's/ implements.*//' | sort -u + +Read [](file:///home/edburns/.vscode-server-insiders/data/User/workspaceStorage/0a47833b2fec2df1980b294571e4f51d/GitHub.copilot-chat/chat-session-resources/e01ff3e8-22ab-4df6-878d-6311894eaf0a/toolu_vrtx_011124MhaCgPR2gvRHwJ4Ndy__vscode-1782158068142/content.txt#1-1), lines 1 to 300 + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "^public record" java/src/generated/java/com/github/copilot/generated/rpc/*.java | grep -oP '\b\w+(?:<[^>]+>)?' | grep -v "^public\|^record\|^@\|^String\|^void\|^JsonProperty\|^JsonInclude\|^JsonIgnoreProperties\|^Generated\|^CopilotExperimental" | sort -u | head -80 + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rPoh '(?<=@JsonProperty\("[^"]*"\)\s)\S+(?:<[^>]+>)?' java/src/generated/java/com/github/copilot/generated/rpc/*.java | sort -u + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "@JsonProperty" java/src/generated/java/com/github/copilot/generated/rpc/*.java | grep -oP '@JsonProperty\("[^"]*"\)\s+\K\S+' | sort -u + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "Map<" java/src/generated/java/com/github/copilot/generated/rpc/*.java | grep -oP 'Map<[^>]+>' | sort -u + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "CompletableFuture<" java/src/generated/java/com/github/copilot/generated/rpc/*.java | grep -oP 'CompletableFuture<[^>]+>' | sort -u + +Read [](file:///home/edburns/.vscode-server-insiders/data/User/workspaceStorage/0a47833b2fec2df1980b294571e4f51d/GitHub.copilot-chat/chat-session-resources/e01ff3e8-22ab-4df6-878d-6311894eaf0a/toolu_vrtx_011q19D6rVhGooBNSoryk2X8__vscode-1782158068148/content.txt) + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "import java\.\|import com.fasterxml\|import com.github" java/src/generated/java/com/github/copilot/generated/rpc/*.java | grep -v "^import com.github.copilot.generated.rpc\.\|^import com.github.copilot.CopilotExperimental" | sort -u + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "import java\.\|import com.fasterxml\|import com.github" java/src/main/java/com/github/copilot/rpc/*.java | grep -v "^import com.github.copilot.rpc\.\|^import com.github.copilot.CopilotExperimental\|^import com.github.copilot.SystemMessageMode\|^import com.github.copilot.generated.SessionEvent" | sort -u + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "sealed\|@JsonSubTypes\|@JsonTypeInfo" java/src/generated/java/com/github/copilot/generated/rpc/*.java | head -30 + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rl "sealed" java/src/generated/java/com/github/copilot/generated/rpc/*.java + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rn "sealed" java/src/main/java/com/github/copilot/rpc/*.java + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -n "private\|public class\|public record" java/src/main/java/com/github/copilot/rpc/SystemMessageSections.java java/src/main/java/com/github/copilot/rpc/SystemPromptSections.java java/src/main/java/com/github/copilot/rpc/SystemMessageConfig.java | head -30 + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rn "int\|long\|double\|boolean\|float\|byte\|short\|char" java/src/generated/java/com/github/copilot/generated/rpc/*.java | grep "@JsonProperty" | grep -oP '@JsonProperty\("[^"]*"\)\s+\K(int|long|double|boolean|float|byte|short|char)\b' | sort -u + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rn "int\b\|long\b\|double\b\|boolean\b" java/src/main/java/com/github/copilot/rpc/*.java | grep "private " | grep -oP 'private\s+(int|long|double|boolean)\b' | sort | uniq -c | sort -rn + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rn "Consumer<\|Function<\|Supplier<\|Executor\b" java/src/main/java/com/github/copilot/rpc/*.java | grep "private\|public" | grep -oP '(Consumer<[^>]+>|Function<[^>]+>|Supplier<[^>]+>|Executor)\b' | sort -u + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rh "import com.github.copilot" java/src/main/java/com/github/copilot/rpc/*.java | grep -v "^import com.github.copilot.rpc\.\|^import com.github.copilot.generated.rpc\." | sort -u + +Ran terminal command: cd /home/edburns/workareas/copilot-sdk && grep -rn "SessionMetadataContextInfoResultContextInfo\|SessionMetadataSnapshotResultWorkspace\|SessionWorkspacesGetWorkspaceResultWorkspace\|SessionWorkspacesSaveLargePasteResultSaved\|SessionToolsUpdateSubagentSettingsParamsSubagents" java/src/generated/java/com/github/copilot/generated/rpc/*.java | head -10 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/McpServerConfig.java#1-1), lines 1 to 60 + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/Attachment.java) + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolResultObject.java#1-1), lines 1 to 60 + +Read [](file:///home/edburns/.vscode-server-insiders/data/User/workspaceStorage/0a47833b2fec2df1980b294571e4f51d/GitHub.copilot-chat/chat-session-resources/e01ff3e8-22ab-4df6-878d-6311894eaf0a/toolu_vrtx_01UxE8XUJogukPbL4Wd7rWHr__vscode-1782158068103/content.txt) + +Here's the **maximum viable set** derived from every type actually used across `com.github.copilot.rpc` and `com.github.copilot.generated.rpc`: + +| Java type | JSON Schema | SDK usage examples | +|-----------|-------------|-------------------| +| `String` | `{"type": "string"}` | Pervasive (IDs, names, paths, messages) | +| `int`, `Integer` | `{"type": "integer"}` | `ProviderConfig`, `ModelLimits`, `McpServerConfig` | +| `long`, `Long` | `{"type": "integer"}` | Timestamps in hook inputs, JSON-RPC IDs | +| `double`, `Double` | `{"type": "number"}` | `ModelBilling.multiplier`, `InfiniteSessionConfig` thresholds | +| `float`, `Float` | `{"type": "number"}` | Not used in SDK today, but natural complement | +| `boolean`, `Boolean` | `{"type": "boolean"}` | Extensively in `SessionConfig`, hook outputs, options | +| `String[]` | `{"type": "array", "items": {"type": "string"}}` | `CopilotClientOptions.cliArgs` | +| `enum` types | `{"type": "string", "enum": ["V1", ...]}` | 92 enums (`ToolDefer`, `AgentMode`, 85 generated) | +| `UUID` | `{"type": "string", "format": "uuid"}` | Generated record ID fields | +| `OffsetDateTime` | `{"type": "string", "format": "date-time"}` | Generated record timestamp fields | +| `JsonNode` | `{}` (any) | `ToolInvocation.argumentsNode`, hook args/results | +| `Object` | `{}` (any) | `ToolDefinition.parameters`, `JsonRpcRequest.params` | +| `List`, `Collection` | `{"type": "array", "items": }` | 50+ distinct `T` across the API | +| `Map` | `{"type": "object", "additionalProperties": {"type": "string"}}` | `ProviderConfig.headers`, env maps | +| `Map` | `{"type": "object", "additionalProperties": {"type": "boolean"}}` | Generated records | +| `Map` | `{"type": "object", "additionalProperties": {"type": "integer"}}` | Generated records | +| `Map` | `{"type": "object"}` (opaque) | `ToolInvocation.getArguments()`, telemetry, extensionData | +| `Map` (typed `T`) | `{"type": "object", "additionalProperties": }` | `Map`, `Map`, etc. | +| `Map>` | `{"type": "object", "additionalProperties": {"type": "array", "items": {"type": "string"}}}` | Generated records | +| Records / POJOs | `{"type": "object", "properties": {...}, "required": [...]}` | ~690 types (26 handwritten records, ~65 POJOs, ~600 generated) | +| Sealed / `@JsonSubTypes` | `{"oneOf": [...]}` with discriminator | `McpServerConfig`, `MessageAttachment`, `SlashCommandInvocationResult` | +| `Optional` | Schema of `T`, not in `required` array | `Optional` in `SessionConfig` | +| `OptionalInt` | `{"type": "integer"}`, not in `required` | `CopilotClientOptions.getSessionIdleTimeoutSeconds()` | +| `OptionalDouble` | `{"type": "number"}`, not in `required` | `ModelBilling.getMultiplierOpt()` | + +**Types intentionally excluded** (not sensible as tool parameter types): + +| Java type | Reason | +|-----------|--------| +| `CompletableFuture` | Return type only, not a parameter type | +| `Consumer`, `Supplier`, `Function`, `Executor` | Callback/functional types, not serializable | +| `ObjectMapper`, `TypeReference` | Infrastructure, not data | + +The minimum viable set from the plan covers 8 rows. This maximum viable set covers **23 rows** — adding `String[]`, `UUID`, `OffsetDateTime`, `JsonNode`/`Object` (any), typed `Map` variants, sealed/polymorphic types, and `Optional` variants. These all have established Jackson serialization semantics in the existing SDK. + +Created 2 todos + +### 16:11 Prompt + +Now we come to "3.5 — Generated code shape". + +> **Question:** What exactly does the processor generate? + +This is why, before we started even creating the phase plan, we created a test of record that shows the current, non-ergonomic, way works and is functional. + +In the context of question 3.5, look at known-passing test `java/src/test/java/com/github/copilot/LowLevelToolDefinitionIT.java`. Note that it defines these tools + +- `setPhaseTool` +- `searchTool` +- `grepOverrideTool` + +Compare that known-passing code with what you grabbed from ADR-005: + +> ```java +> // GENERATED — do not edit +> final class MyTools$$CopilotToolMeta { +> static List definitions(MyTools instance) { +> return List.of( +> new ToolDefinition("set_current_phase", "Sets the current phase", +> Map.of("type", "object", +> "properties", Map.of("phase", Map.of("type", "string", +> "description", "The phase to transition to")), +> "required", List.of("phase")), +> invocation -> { +> String phase = (String) invocation.getArguments().get("phase"); +> return CompletableFuture.completedFuture( +> instance.setCurrentPhase(phase)); +> }, null, null, null) +> ); +> } +> } +> ``` + +And then ask yourself the questions from the plan + +> 1. **Method invocation in generated code:** The generated lambda calls `instance.setCurrentPhase(phase)` directly — no reflection at runtime. But this requires the method to be accessible (not private). What access levels do we support? langchain4j uses `method.setAccessible(true)` at runtime. We'd need to either: +> - Require `public` or package-private methods, OR +> - Generate a helper that uses `MethodHandles.Lookup` to access private methods (complex), OR +> - Just require non-private. + +> 2. **Return type handling:** What does the generated code do with the method's return value? + +> 3. **Argument deserialization in generated code:** How does the generated lambda extract and coerce arguments? + +Does the known-passing test code even ask these questions? Does it answer them? How? + +### 16:18 Response + +Updated todo list + +Read [](file:///home/edburns/workareas/copilot-sdk/java/src/test/java/com/github/copilot/LowLevelToolDefinitionIT.java) + +The known-passing test **doesn't ask** these questions — it **bypasses** them entirely. Here's why: + +--- + +**Q1 (Access levels):** Not applicable. The test uses inline lambdas, not method references: + +```java +invocation -> { + currentPhase = args.phase(); + return CompletableFuture.completedFuture("Phase set to " + currentPhase); +} +``` + +There's no method to invoke — the tool logic IS the lambda. The access-level question only arises when a processor needs to generate code that calls `instance.someMethod()`. + +--- + +**Q2 (Return type handling):** Already resolved by fiat. Every handler explicitly returns `CompletableFuture`: + +```java +return CompletableFuture.completedFuture("Phase set to " + currentPhase); +return CompletableFuture.completedFuture("Found: item_alpha, item_beta"); +return CompletableFuture.completedFuture("CUSTOM_GREP: " + query); +``` + +The low-level API's handler type signature forces the user to produce `CompletableFuture` (or `Object`). No wrapping, no serialization — that's the user's problem. + +--- + +**Q3 (Argument deserialization):** Answered — it shows **both** patterns that the generated code would need: + +| Pattern | Test usage | +|---------|-----------| +| Jackson deserialization to a record | `invocation.getArgumentsAs(PhaseArgs.class)` for `setPhaseTool` | +| Direct map access + cast | `(String) invocation.getArguments().get("keyword")` for `searchTool` and `grepOverrideTool` | + +--- + +**Summary:** The low-level test is the **specification** of what the generated code must emit — it shows the exact lambdas the processor must synthesize. The test proves the wire format works. The three questions from 3.5 are about automating what the test author does manually: + +1. The processor must generate `instance.method()` calls → needs at least package-private access +2. The processor must wrap arbitrary return types into `CompletableFuture` → needs the return-type table +3. The processor must generate either `getArgumentsAs(Record.class)` or `getArguments().get("name")` + cast → depending on parameter complexity + +The test doesn't need to answer these questions because the human already made all three decisions by hand-writing the lambdas. + +### 16:19 Prompt + +I have been going through the plan `copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md` and filling in **Resolution** for each question. Absorb what we learned and output what I should record as the **Resolution** for question 3.5. + +### 16:21 Response + +Updated todo list + +**Resolution:** + +The generated `$$CopilotToolMeta` class produces lambdas identical to what `LowLevelToolDefinitionIT` writes by hand. Decisions: + +1. **Access levels:** Require at least package-private. The generated `$$CopilotToolMeta` lives in the same package as the user's class, so package-private and above work without reflection. Emit a compile error for `private` methods annotated with `@CopilotTool`. + +2. **Return type handling:** + + | Return type | Generated code | + |-------------|----------------| + | `String` | `CompletableFuture.completedFuture(instance.method(...))` | + | `CompletableFuture` | `instance.method(...)` (use as-is) | + | `void` | `instance.method(...); return CompletableFuture.completedFuture("Success")` | + | `CompletableFuture` (non-String) | `instance.method(...).thenApply(objectMapper::writeValueAsString)` | + | Other `T` | `CompletableFuture.completedFuture(objectMapper.writeValueAsString(instance.method(...)))` | + +3. **Argument deserialization:** Follow the two patterns proven in `LowLevelToolDefinitionIT`: + - **Simple types** (`String`, primitives, boxed): generate direct cast from `invocation.getArguments().get("name")` (e.g., `(String) args.get("city")`, `((Number) args.get("count")).intValue()`). + - **Complex types** (enums, records, POJOs): generate `objectMapper.convertValue(invocation.getArguments().get("name"), TargetType.class)`. + - **Single-record-parameter shortcut:** When a method has exactly one parameter that is a record/POJO (matching the full argument set), generate `invocation.getArgumentsAs(RecordType.class)` — the same pattern `LowLevelToolDefinitionIT.setPhaseTool` uses. + + The `ObjectMapper` instance: use a `private static final` field in the generated `$$CopilotToolMeta` class, initialized from `com.github.copilot.rpc.RpcMapper.INSTANCE` (already exists in the SDK). + +### 16:37 Prompt + +Read `copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md` to get the context. + +In this session, we are working on "Phase 3 — Ignorance reduction: questions to answer before writing code". + +We have completed and answered up to, but not including, "3.7 — `module-info.java` impact". + +I will proceed after this prompt. + +### 16:40 Response + +Updated todo list + +Read [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md#1-1), lines 1 to 300 + +Read [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md#300-300), lines 300 to 491 + +I've read the full document. Here's my understanding of where we stand: + +**Phase 3 — completed items (3.1–3.6):** +- **3.1** — Option B: new `com.github.copilot.tool` package for annotations + processor + schema generation; `ToolDefinition` stays in `rpc`. +- **3.2** — `@CopilotTool` with `RUNTIME` retention; include `ToolDefer` support. +- **3.3** — `@Param` with `defaultValue()` that is behaviorally effective; `required=true` + non-empty `defaultValue` is a compile-time error. +- **3.4** — Maximum viable type set (23 rows) including `Optional`, sealed/`@JsonSubTypes`, typed `Map` variants, `UUID`, `OffsetDateTime`, etc. +- **3.5** — Generated `$$CopilotToolMeta` class; package-private minimum access; direct casts for simple types, `ObjectMapper.convertValue()` for complex types; `RpcMapper.INSTANCE` for the `ObjectMapper`. +- **3.6** — Processor-only approach (no reflection fallback). + +**Remaining items:** +- **3.7** — `module-info.java` impact (answer is drafted: no special exports needed since generated class is in user's package, but needs verification) +- **3.8** — Processor registration (add to `META-INF/services` + `module-info.java` `provides` clause) + +Ready for your next instruction. + +## dd-3021192-module-info.java-impact + +### 16:40 Prompt + +Create a self-contained Maven module rooted at `1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/pom.xml`. The `artifactId` is `dd3021192` and the `groupId` is `com.github`. It depends on + +```xml + com.github + copilot-sdk-java + 1.0.2-SNAPSHOT +``` + +It creates a JAR that can be run with java -jar . This is the "simple named-module test." mentioned in "3.7 — `module-info.java` impact". + +When you run the jar, it demonstrates that this works. + +### 17:02 Response + +Updated todo list + +Searched for text `module com.github.copilot` (`**/java/src/main/java/module-info.java`), 1 result + +Read [](file:///Users/edburns/workareas/copilot-sdk/java/src/main/java/module-info.java) + +Now I have the module name. Let me create the test module. The key idea: simulate what the processor will generate (`$$CopilotToolMeta`) and prove that `Class.forName()` can find it from within a named JPMS module. + +Created [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/pom.xml) + +Created [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/module-info.java) + +Created [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/MyTools.java) + +Created 5 todos + +Created [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/MyTools%24%24CopilotToolMeta.java) + +Created [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/Main.java) + +Now let me build and run it. First, I need to ensure `copilot-sdk-java` is installed in the local Maven repo, then build the test module. + +Ran terminal command: export JAVA_HOME="/Library/Java/JavaVirtualMachines/microsoft-25.jdk/Contents/Home" && export M2_HOME=${HOME}/Downloads/apache-maven-3.9.8 && export PATH=${M2_HOME}/bin:${JAVA_HOME}/bin:${PATH} && cd /Users/edburns/workareas/copilot-sdk/java && LOG="$(date +%Y%m%d-%H%M)-job-logs.txt" && mvn install -DskipTests -pl . > "$LOG" 2>&1 & tail -f "$LOG" + +Made changes. + +### 17:46 Prompt + +Read `copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md` (the plan) to get the context. + +✅✅Pay very close attention to each **Resolution:** to the questions in `Phase 3 — Ignorance reduction: questions to answer before writing code`. + +In this session, we are creating issues of type **Task** as children of Epic https://github.com/github/copilot-sdk/issues/1682 . These issues correspond to the sub-sections in `## Phase 4 — Implementation (the build order)`. + +I want you to use `gh` to create issues as sub-issues of 1682. + +- I will dispatch these issues by assigning them to an agent. Leave them unassigned for now. + +- The description you put in the issues you create is the specification for the work the agent assigned to the issue must do. Therefore, you need to be very specific and targeted when writing the description. + +- Set the type to **Task**. + +- Each issue will be worked on and resolved in serial. You can assume: + + - All issues will operate in this topic branch of `upstream`: `edburns/1682-java-tool-ergonomics`. + + - The assignee agent has access to the topic branch and, most importantly, to the prompts in the directory `1682-java-tool-ergonomics-prompts-remove-before-merge`, which includes the plan. Therefore, you can instruct the agent to access the plan by reference, you don't need to reference it by value directly in the issue. + +- The sub-sections of `## Phase 4 — Implementation (the build order)` have essential details for each corresponding **Task**. + +- Instruct the agent to read the entire plan before working. + +- Instruct the agent to carefully re-read the relevant sections of the of the plan. ❌❌You can't just say, "read the relevant sections of the plan". ✅✅In the description of the issue, list exactly what sections of the plan apply to this particular issue. + +- Though the sub-sections of `## Phase 4 — Implementation (the build order)` have essential details for each corresponding **Task**, I want you to carefully consider if there additional gating tests you can add so that as the agent proceeds from one task to the next, the likelihood of rework is reduced. + +### 18:16 Response + +Updated todo list + +Read [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md#1-1), lines 1 to 100 + +Read [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md#101-101), lines 101 to 200 + +Read [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md#201-201), lines 201 to 300 + +Read [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md#301-301), lines 301 to 400 + +Read [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md#401-401), lines 401 to 500 + +Read [](file:///Users/edburns/workareas/copilot-sdk/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md#501-501), lines 501 to 505 + +Now I have the full plan. Let me create the 5 issues (4.1–4.5, skipping 4.6 which is deferred) as sub-issues of #1682. I'll write detailed descriptions with gating tests. + +Ran terminal command: gh issue create --help 2>&1 | head -40 + +`gh issue create` supports `--parent` and `--type` directly. I'll create each issue with a body file. + +Created [](file:///tmp/issue-4.1-body.md) + +Created [](file:///tmp/issue-4.2-body.md) + +Created [](file:///tmp/issue-4.3-body.md) + +Created 5 todos + +Created [](file:///tmp/issue-4.4-body.md) + +Created [](file:///tmp/issue-4.5-body.md) + +Now I'll create the issues using `gh`: + +Ran terminal command: cd /Users/edburns/workareas/copilot-sdk && gh issue create \ + --repo github/copilot-sdk \ + --title "[Java] @CopilotTool ergonomics 4.1: Create @CopilotTool and @Param annotations" \ + --body-file /tmp/issue-4.1-body.md \ + --type "Task" \ + --parent 1682 + +Ran terminal command: cd /Users/edburns/workareas/copilot-sdk && gh issue create \ + --repo github/copilot-sdk \ + --title "[Java] @CopilotTool ergonomics 4.2: Schema generation utility (TypeMirror to JSON Schema)" \ + --body-file /tmp/issue-4.2-body.md \ + --type "Task" \ + --parent 1682 + +Ran terminal command: cd /Users/edburns/workareas/copilot-sdk && gh issue create \ + --repo github/copilot-sdk \ + --title "[Java] @CopilotTool ergonomics 4.3: Annotation processor (CopilotToolProcessor)" \ + --body-file /tmp/issue-4.3-body.md \ + --type "Task" \ + --parent 1682 + +Ran terminal command: cd /Users/edburns/workareas/copilot-sdk && gh issue create \ + --repo github/copilot-sdk \ + --title "[Java] @CopilotTool ergonomics 4.4: ToolDefinition.fromObject(Object) registration API" \ + --body-file /tmp/issue-4.4-body.md \ + --type "Task" \ + --parent 1682 + +Ran terminal command: cd /Users/edburns/workareas/copilot-sdk && gh issue create \ + --repo github/copilot-sdk \ + --title "[Java] @CopilotTool ergonomics 4.5: E2E integration test with replay proxy" \ + --body-file /tmp/issue-4.5-body.md \ + --type "Task" \ + --parent 1682 + +All 5 issues created as sub-issues of #1682: + +| Phase | Issue | Title | +|-------|-------|-------| +| 4.1 | [#1758](https://github.com/github/copilot-sdk/issues/1758) | Create `@CopilotTool` and `@Param` annotations | +| 4.2 | [#1759](https://github.com/github/copilot-sdk/issues/1759) | Schema generation utility (TypeMirror → JSON Schema) | +| 4.3 | [#1760](https://github.com/github/copilot-sdk/issues/1760) | Annotation processor (`CopilotToolProcessor`) | +| 4.4 | [#1761](https://github.com/github/copilot-sdk/issues/1761) | `ToolDefinition.fromObject(Object)` registration API | +| 4.5 | [#1762](https://github.com/github/copilot-sdk/issues/1762) | E2E integration test with replay proxy | + +Each issue includes: +- Explicit list of plan sections to re-read (by section number and title) +- Instruction to read the full plan before starting +- Detailed gating tests beyond what the plan specifies (e.g., reflection-based annotation attribute verification in 4.1, handler invocation tests in 4.4) +- Clear prerequisites (serial dependency chain) +- Branch and constraint information + +Updated todo list + +Made changes. + +### 18:37 Prompt + +Consider this text from the plan + +-------- + +**Question:** What attributes does `@CopilotTool` need? + +Based on ADR-005 and the C#/langchain4j comparisons: + +```java +@Documented +@Retention(RetentionPolicy.SOURCE) // only needed at compile time for processor +@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; +} +``` + + +------ + +I'm concerned about relying on snake_case here. That's a Python thing. Java uses camelCase. + +What would be the changes to the overall design if we said camelCase instead of snake_case? + +### 18:40 Prompt + +Updated todo list + +Let me check what naming convention the existing SDK and wire protocol actually use for tool names. + +Searched for regex `tool.*name` (`**/test/snapshots/tools/*.yaml`), no results + +Searched for regex `snake_case|snakeCase|toSnakeCase|to_snake` (`**/java/**`), 9 results + +Searched for regex `DefineTool|define_tool|defineTool` (`**/nodejs/src/**`), 4 results + +Read [](file:///Users/edburns/workareas/copilot-sdk/test/snapshots/tools/low_level_tool_definition.yaml) + +Searched for regex `DefineTool|tool.*name` (`**/go/*.go`), 10 results + +Searched for regex `set_current_phase|search_items|tool.*name` (`**/java/src/test/**/*LowLevel*"`), no results + +Searched for regex `set_current_phase|search_items` (`**/java/src/test/**"`), no results + +Searched for regex `tool.*name.*snake|tool.*name.*camel` (`**/nodejs/test/**`), no results + +Searched for regex `set_current_phase|search_items` (`**/java/src/test/**`), 2 results + +Searched for regex `defineTool.*name` (`**/nodejs/samples/**`), no results + +Searched for regex `Name:` (`**/go/samples/**`), 1 result + +Searched for regex `defineTool` (`**/nodejs/examples/**`), 2 results + +Searched for regex `DefineTool|tool.*name` (`**/dotnet/samples/**`), 2 results + +Here's the analysis: + +## The wire protocol uses `snake_case` — it's not just a Python thing + +Looking at the evidence: + +1. **Replay snapshot** (low_level_tool_definition.yaml): `set_current_phase`, `search_items` +2. **Node.js examples**: `defineTool("lookup_fact", ...)` +3. **Go samples**: `Name: "manual_resume_status"` +4. **.NET samples**: `"manual_resume_status"` +5. **Java existing test**: `ToolDefinition.create("set_current_phase", ...)` + +Every SDK across all languages uses `snake_case` for tool names on the wire. This is the **Copilot CLI / model-facing convention** — it's how the LLM identifies and calls tools in function-calling. It's not language-internal naming; it's a wire protocol identifier. + +## Impact of switching to `camelCase` + +If you changed the default conversion to camelCase (or just identity — since Java methods are already camelCase): + +| Aspect | Impact | +|--------|--------| +| **Wire compatibility** | Tool names like `setCurrentPhase` instead of `set_current_phase`. The model still works (it's just a string), but diverges from every other SDK's convention. | +| **Cross-SDK consistency** | A tool that's `set_current_phase` in Node/Go/.NET/Python/Rust would be `setCurrentPhase` in Java — confusing for multi-SDK teams. | +| **Snapshot reuse** | Can't share the same test snapshots across SDKs (model expects specific tool names in responses). | +| **Code change** | Actually simplifies the processor — no case conversion logic needed (identity transform). | +| **Override tools** | Built-in Copilot tools use `snake_case` (like `grep`). Overriding them already requires explicit `name="grep"`, so no impact there. | + +## Recommendation + +Keep `snake_case` as the **default** wire name derivation. The rationale is protocol-level consistency, not Java style: + +- The Java **method** stays camelCase (`setCurrentPhase`) — idiomatic Java. +- The **tool name** sent to the model is `set_current_phase` — idiomatic function-calling protocol. +- Users can always override with `@CopilotTool(name="whateverTheyWant")`. + +This is the same pattern Jackson uses: Java fields are camelCase, but `@JsonProperty` / `PropertyNamingStrategies` controls the wire format. The wire convention is independent of the source language. + +If you still prefer dropping the conversion, the only code changes would be: +1. Remove the snake_case converter from the processor (4.3). +2. Update the test in 4.3 that verifies `setCurrentPhase` → `set_current_phase`. +3. Update the E2E snapshot/test (4.5) tool names. + +### 18:41 Prompt + +Look at what the agent changed in commit 8b899e15254b2b539ae585c85d1e7c39ef864194 to `java/src/main/java/com/github/copilot/rpc/ToolDefer.java`. It added NONE. + +Is there a precedent for this NONE thing in the other languages? + +### 18:43 Response + +Updated todo list + +Ran terminal command: git show 8b899e15254b2b539ae585c85d1e7c39ef864194 -- java/src/main/java/com/github/copilot/rpc/ToolDefer.java + +Searched for regex `ToolDefer|toolDefer|tool_defer|defer` (`**/nodejs/src/**`), 20 results + +Searched for regex `ToolDefer|Defer` (`**/go/**/*.go`), 20 results + +Searched for regex `ToolDefer|Defer` (`**/dotnet/src/**`), 20 results + +Searched for regex `CopilotToolDefer` (`**/dotnet/src/**`), 8 results + +Searched for regex `defer` (`**/nodejs/src/types.ts`), 4 results + +Read [](file:///Users/edburns/workareas/copilot-sdk/dotnet/src/CopilotTool.cs#155-155), lines 155 to 170 + +Read [](file:///Users/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefer.java#1-1), lines 1 to 10 + +Read [](file:///Users/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefer.java#20-20), lines 20 to 50 + +**No, there is no precedent for `NONE` in the other languages.** + +- **.NET**: `CopilotToolDefer` has only `Auto` and `Never`. The field is `CopilotToolDefer?` (nullable) — absence is expressed as `null`. +- **Node.js**: `defer?: "auto" | "never"` — absence is expressed as `undefined` (the field is optional). +- **Go**: Would use a pointer or zero value. + +Every other SDK uses **nullability/optionality** to represent "no defer preference." The agent invented `NONE("")` to solve a Java annotation limitation: annotation attributes can't be `null`, so a sentinel value is needed for the default. + +This is a reasonable Java-specific workaround (annotations require compile-time constants), but it introduces a concern: `NONE("")` will serialize as `""` via `@JsonValue` if it's ever accidentally included in a JSON-RPC message. The processor/`fromObject()` code must treat `NONE` as equivalent to `null` (i.e., don't pass it to `ToolDefinition.create()`). You'll want to verify that gate exists in task 4.3/4.4. + +### 18:45 Prompt + +Let's take .NET. What is the behavior of the tool in the "absence" case? And what does that serialize as? + +### 18:47 Response + + +Updated todo list + +Read [](file:///Users/edburns/workareas/copilot-sdk/dotnet/src/CopilotTool.cs#140-140), lines 140 to 160 + +Read [](file:///Users/edburns/workareas/copilot-sdk/dotnet/src/Client.cs#2415-2415), lines 2415 to 2450 + +Now let me see how `ToolDefinition` serializes when `Defer` is null: + +Searched for regex `ToolDefinition.*serial|WriteJson|JsonSerializ|toolDefinition.*json|RegisterTools|registerTools` (`**/dotnet/src/Client.cs`), 20 results + +Read [](file:///Users/edburns/workareas/copilot-sdk/dotnet/src/Client.cs#2117-2117), lines 2117 to 2130 + +There's the answer: + +**In .NET, when `Defer` is `null`, the key is simply omitted from the JSON.** + +The serializer uses `DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull`, so any `null`-valued property (like `Defer`, `OverridesBuiltInTool`, `SkipPermission`) is not written to the wire at all. The CLI receives a `ToolDefinition` JSON object with no `defer` key — meaning "no preference, use server default." + +So the wire behavior for "absence" is: **the `defer` field does not appear in the JSON-RPC message**. + +--- + +For Java's `NONE("")`: if Jackson serializes it, it would emit `"defer": ""` — which is **not the same** as omitting the key. The Java code must treat `NONE` as "do not serialize this field" (i.e., pass `null` to `ToolDefinition.create()` for the defer parameter). The `@JsonValue` of `""` is a landmine if it ever leaks to the wire. + +### 19:11 Prompt + +For all the created issues please find the right place to add the following requirement: + +✅✅ YOU MUST run `mvn spotless:apply` before every commit. + +and add it. + +### 19:13 Response + + +Updated todo list + +Ran terminal command: gh issue view 1758 --repo github/copilot-sdk --json body -q .body | tail -20 + +I'll add the requirement to the Constraints section of each issue. The best placement is at the top of Constraints since it's a workflow mandate: + +Ran terminal command: cd /Users/edburns/workareas/copilot-sdk && for ISSUE in 1758 1759 1760 1761 1762; do + BODY=$(gh issue view "$ISSUE" --repo github/copilot-sdk --json body -q .body) + UPDATED=$(echo "$BODY" | sed 's/## Constraints/## Constraints\n\n- ✅✅ YOU MUST run `mvn spotless:apply` before every commit./') + echo "$UPDATED" | gh issue edit "$ISSUE" --repo github/copilot-sdk --body-file - + echo "Updated issue #$ISSUE" +done + +Done. All 5 issues (#1758–#1762) now have `✅✅ YOU MUST run mvn spotless:apply before every commit.` as the first bullet in their Constraints section. + +Created 1 todo + +### 19:13 Prompt + + +Ugh, I did a git problem. + +``` +edburns@Edwards-MacBook-Pro copilot-sdk % git push upstream +git push upstream +To github.com:github/copilot-sdk.git + ! [rejected] edburns/1682-java-tool-ergonomics -> edburns/1682-java-tool-ergonomics (non-fast-forward) +error: failed to push some refs to 'github.com:github/copilot-sdk.git' +hint: Updates were rejected because the tip of your current branch is behind +hint: its remote counterpart. If you want to integrate the remote changes, +hint: use 'git pull' before pushing again. +``` + +Please fix it and push. Make sure not to overwrite anything. + +### 19:16 Response + +Updated todo list + +Ran terminal command: git log --oneline -5 + +Ran terminal command: git log --oneline -5 upstream/edburns/1682-java-tool-ergonomics + +Ran terminal command: git fetch upstream edburns/1682-java-tool-ergonomics && git log --oneline upstream/edburns/1682-java-tool-ergonomics..HEAD + +Ran terminal command: git log --oneline HEAD..upstream/edburns/1682-java-tool-ergonomics + +Ran terminal command: git merge-base HEAD upstream/edburns/1682-java-tool-ergonomics + +Ran terminal command: git log --oneline d15cfcbe -1 + +Ran terminal command: echo "Local tree (Resolve 3.8):" && git cat-file -p e9c978bb | grep tree && echo "Remote tree (Resolve 3.8):" && git cat-file -p 622e3ae0 | grep tree + +Ran terminal command: git diff upstream/edburns/1682-java-tool-ergonomics..HEAD --stat + +Ran terminal command: git diff HEAD..upstream/edburns/1682-java-tool-ergonomics --stat + +The remote is strictly a subset of local — it just has deletions (all the same files local added). The local branch has everything the remote has plus more. A safe rebase will work: + +Ran terminal command: git rebase upstream/edburns/1682-java-tool-ergonomics + +Ran terminal command: git stash && git rebase upstream/edburns/1682-java-tool-ergonomics + +Ran terminal command: git stash pop && git log --oneline -5 + +Rebase succeeded — local is now 4 commits ahead of remote with no divergence. Now pushing: + +Ran terminal command: git push upstream edburns/1682-java-tool-ergonomics + +Fixed. The rebase detected that your local commits (with different hashes but same content) were duplicates of what was already on the remote, skipped them, and replayed only the unique commits on top. Push succeeded as a fast-forward. + +Created 3 todos + +### 19:16 Prompt + +Did you push it to upstream? + +### 19:17 Response + + +Updated todo list + + +Yes. The output confirms it pushed to `upstream`: + +``` +To github.com:github/copilot-sdk.git + 622e3ae0..170d5aaa edburns/1682-java-tool-ergonomics -> edburns/1682-java-tool-ergonomics +``` + +### 19:17 Prompt + + + +I don't see the commits at https://github.com/github/copilot-sdk/pull/1763/commits . + +Why? + +### 19:18 Response + +Updated todo list + +Ran terminal command: gh pr view 1763 --repo github/copilot-sdk --json headRefName,baseRefName,headRepository + +PR #1763 is tracking branch `copilot/edburns1682-java-tool-ergonomics`, but you pushed to `edburns/1682-java-tool-ergonomics`. They're different branches. + +You either need to: +1. Push to the PR's branch: `git push upstream edburns/1682-java-tool-ergonomics:copilot/edburns1682-java-tool-ergonomics` +2. Or update the PR to point to your branch instead. + +Which would you prefer? + +### 19:18 Prompt + +We should always and only be pushing to the PR's branch. + +Updated todo list + +Ran terminal command: git push upstream edburns/1682-java-tool-ergonomics:copilot/edburns1682-java-tool-ergonomics + +Ran terminal command: git fetch upstream copilot/edburns1682-java-tool-ergonomics && git log --oneline upstream/copilot/edburns1682-java-tool-ergonomics -5 + +Ran terminal command: git log --oneline HEAD -5 + +Ran terminal command: git diff upstream/copilot/edburns1682-java-tool-ergonomics..HEAD --stat | tail -5 + +Ran terminal command: git diff HEAD..upstream/copilot/edburns1682-java-tool-ergonomics --stat + +Local is a strict superset — the reverse diff only shows deletions (content local has that the PR branch doesn't). Nothing would be lost. This needs a force push since the histories diverged. + +Analyzing your answers... + +Ran terminal command: git push upstream edburns/1682-java-tool-ergonomics:copilot/edburns1682-java-tool-ergonomics --force-with-lease + +Done. PR #1763 now shows the commits at `https://github.com/github/copilot-sdk/pull/1763/commits`. + +Created 2 todos + +### 19:24 Prompt + +Now there are ton of files in the **Files changed** tab that are not at all related to this PR: + +https://github.com/github/copilot-sdk/pull/1763 + +Please fix that so only the files relevant to our work are in there. + +Updated todo list + +Ran terminal command: git log --oneline upstream/main..HEAD + +Ran terminal command: git diff upstream/main..HEAD --stat | tail -40 + +Ran terminal command: git show 5c360d56 --stat + +Ran terminal command: git fetch upstream main && git log --oneline upstream/main -3 + +The branch has a duplicate of commit `a3dbdd19` (already on main) with a different hash (`5c360d56`). Rebasing onto current `upstream/main` will drop it: + +Ran terminal command: git stash && git rebase upstream/main + +Git skipped the duplicate. Let me verify and push: + +Ran terminal command: git diff upstream/main..HEAD --stat && echo "---" && git log --oneline upstream/main..HEAD + +Only Java and prompt files now — no dotnet/go/nodejs/python/rust files. Pushing: + +Ran terminal command: git push upstream edburns/1682-java-tool-ergonomics:copilot/edburns1682-java-tool-ergonomics --force-with-lease && git stash pop + +Done. The PR's "Files changed" tab should now show only the 19 relevant files — all Java source, prompts, and plan files. The unrelated dotnet/go/nodejs/python/rust changes from the duplicate commit are gone. + +Created 2 todos + +### 19:29 Prompt + +Now we come back to the most important thing to fix, that `NONE` field in `java/src/main/java/com/github/copilot/rpc/ToolDefer.java`. + +Read this comment from a reviewer: https://github.com/github/copilot-sdk/pull/1763#discussion_r3455952418 + +And recall what we elicited earlier: + +> Let's take .NET. What is the behavior of the tool in the "absence" case? And what does that serialize as? + +> There's the answer: + +> **In .NET, when `Defer` is `null`, the key is simply omitted from the JSON.** +> +> The serializer uses `DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull`, so any `null`-valued property (like `Defer`, `OverridesBuiltInTool`, `SkipPermission`) is not written to the wire at all. The CLI receives a `ToolDefinition` JSON object with no `defer` key — meaning "no preference, use server default." +> +> So the wire behavior for "absence" is: **the `defer` field does not appear in the JSON-RPC message**. +> +> --- +> +> For Java's `NONE("")`: if Jackson serializes it, it would emit `"defer": ""` — which is **not the same** as omitting the key. The Java code must treat `NONE` as "do not serialize this field" (i.e., pass `null` to `ToolDefinition.create()` for the defer parameter). The `@JsonValue` of `""` is a landmine if it ever leaks to the wire. + +It seems to me at this point the best thing is to update the Javadoc so that the future work to implement the annotation processor doesn't cause the `defer` to leak onto the wire. + +Is there any other better way at this point? + +### 19:31 Response + +Updated todo list + +Read [](file:///Users/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefer.java#1-1), lines 1 to 70 + +Read [](file:///Users/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefer.java#70-70), lines 70 to 76 + + + +Fetched https://github.com/github/copilot-sdk/pull/1763#discussion_r3455952418 + +There's a better option than Javadoc alone: make `NONE` serialize as `null` instead of `""`. This provides a safety net at the Jackson level: + +```java +@JsonValue +public String getValue() { + return this == NONE ? null : value; +} +``` + +With this change, even if `ToolDefer.NONE` accidentally reaches a `ToolDefinition`, Jackson serializes it as `null`, and `@JsonInclude(NON_NULL)` (already on `ToolDefinition`) omits it from the wire. The bug becomes impossible regardless of what the processor does. + +The internal `""` string is still needed because Java annotations require compile-time constants (`null` can't be used as an annotation element default), but the `@JsonValue` doesn't have to expose it. + +**Tradeoff:** + +| Approach | Prevents wire leak? | Relies on downstream code? | +|----------|---------------------|---------------------------| +| Javadoc only | No — still serializes as `""` if leaked | Yes — processor must map to `null` | +| `@JsonValue` returns `null` for NONE | Yes — impossible to emit `""` | No — defense in depth | + +I'd recommend doing **both**: the `@JsonValue` fix for safety, plus a Javadoc update explaining `NONE` is an annotation-only sentinel. Want me to make those changes? + +Created 1 todo + +### 19:31 Prompt + +Yes. Please do that and commit it. But do not push it. + +Updated todo list + +Read [](file:///Users/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefer.java#22-22), lines 22 to 55 + +Replacing 14 lines with 28 lines in [](file:///Users/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefer.java) + + +``` + +``` + + +Ran terminal command: cd /Users/edburns/workareas/copilot-sdk/java && export JAVA_HOME="/Library/Java/JavaVirtualMachines/microsoft-25.jdk/Contents/Home" && export M2_HOME=${HOME}/Downloads/apache-maven-3.9.8 && export PATH=${M2_HOME}/bin:${JAVA_HOME}/bin:${PATH} && mvn spotless:apply -q + +Ran terminal command: cd /Users/edburns/workareas/copilot-sdk && git add java/src/main/java/com/github/copilot/rpc/ToolDefer.java && git commit -m "fix(java): make ToolDefer.NONE serialize as null to prevent wire leak + +NONE is an annotation-only sentinel for @CopilotTool(defer=...) defaults. +Its @JsonValue now returns null so @JsonInclude(NON_NULL) omits it from +the JSON-RPC payload, matching the nullable/optional semantics used by +all other SDKs (.NET CopilotToolDefer?, Node defer?, Go omitempty, +Python | None, Rust Option)." + +Committed as `b8ad8264`. Spotless check passed. Not pushed. + +Created 1 todo + +Made changes. diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/add-tests-that-use-low_level_tool_definition.yaml.md b/1682-java-tool-ergonomics-prompts-remove-before-merge/add-tests-that-use-low_level_tool_definition.yaml.md new file mode 100644 index 000000000..b60937cd2 --- /dev/null +++ b/1682-java-tool-ergonomics-prompts-remove-before-merge/add-tests-that-use-low_level_tool_definition.yaml.md @@ -0,0 +1,151 @@ +## Goal + +Port the Java integration test behavior from: + +- `java/src/test/java/com/github/copilot/LowLevelToolDefinitionIT.java` +- test method: `lowLevelToolDefinition` +- snapshot: `test/snapshots/tools/low_level_tool_definition.yaml` + +to the following non-Java SDKs, using each language's native E2E test infrastructure: + +1. `dotnet` +2. `go` +3. `nodejs` +4. `python` +5. `rust` + +The new/updated tests in each language must use the **same snapshot scenario** (`tools/low_level_tool_definition`) and validate the same behavior. + +--- + +## Required test behavior to port + +From a test perspective, replicate this behavior: + +1. Define a `set_current_phase` tool that accepts a `phase` argument (string, enum: `["searching", "analyzing", "done"]`) and returns `"Phase set to {phase}"`. The tool handler must also store the phase value in test-local state. +2. Define a `search_items` tool that accepts a `keyword` argument (string) and returns `"Found: item_alpha, item_beta"`. +3. Define a `grep` override tool (using whatever "override" mechanism the language provides) that accepts a `query` argument (string) and returns `"CUSTOM_GREP: {query}"`. +4. Create a session with: + - Permission handler that auto-approves all requests. + - Available tools: all custom tools (`*`) plus built-in `web_fetch`. + - The three tool definitions registered on the session. +5. Send prompt: `"First, set the current phase to 'analyzing'. Then search for items with keyword 'copilot'. Report the phase and search results."` +6. Assert: + - The assistant response is non-null/non-empty. + - The response content (case-insensitive) contains `"analyzing"`. + - The response content contains `"item_alpha"` or `"item_beta"`. + - The test-local phase state equals `"analyzing"` (verifying the tool handler was actually invoked). + +Do not weaken these assertions. + +--- + +## Critical execution constraint (must follow exactly) + +Proceed through languages **one-at-a-time** in this exact order: + +1. `dotnet` +2. `go` +3. `nodejs` +4. `python` +5. `rust` + +❌❌ **Do not continue to the next language unless and until the current language gets a clean run with the new test in isolation.** ❌❌ + +Do **not** run full cross-language or full-repo test suites. Let CI/CD handle broad runs. + +--- + +## Snapshot/name mapping requirements + +Ensure each language's test naming/harness maps to: + +- snapshot folder: `tools` +- snapshot file: `low_level_tool_definition.yaml` + +Do not create alternate snapshot names for this scenario. + +--- + +## Per-language isolated run commands + +Use these commands for isolated validation while iterating. + +### 1) dotnet + +Implement in dotnet E2E tests (preferred: new `LowLevelToolDefinitionE2ETests` class or add to existing `ToolsE2ETests` class using snapshot category `tools`, test method `Low_Level_Tool_Definition`). + +Isolated run: + +```bash +cd dotnet && dotnet test test/GitHub.Copilot.SDK.Test.csproj --filter "FullyQualifiedName~Low_Level_Tool_Definition" +``` + +### 2) go + +Implement in Go E2E tests with snapshot mapping to `tools/low_level_tool_definition` (preferred: add to existing `go/internal/e2e/tools_e2e_test.go` or create new file, subtest name exactly `low_level_tool_definition`). + +Isolated run: + +```bash +cd go && go test ./internal/e2e -run 'TestToolsE2E/low_level_tool_definition$' -count=1 +``` + +### 3) nodejs + +Implement in Node E2E Vitest (preferred: add to existing `nodejs/test/e2e/tools.e2e.test.ts` or create new file, test name mapping to `low_level_tool_definition`). + +Isolated run: + +```bash +cd nodejs && npm test -- test/e2e/tools.e2e.test.ts -t "low_level_tool_definition" +``` + +### 4) python + +Implement in Python E2E pytest (preferred: add to existing `python/e2e/test_tools_e2e.py` or create new file, test function `test_low_level_tool_definition`). + +Isolated run: + +```bash +cd python && uv run pytest e2e/test_tools_e2e.py::test_low_level_tool_definition +``` + +### 5) rust + +Implement in Rust E2E tests (preferred: add to existing `rust/tests/e2e/tools.rs`; use `with_e2e_context("tools", "low_level_tool_definition", ...)`). + +Isolated run: + +```bash +cd rust && cargo test --features test-support --test e2e tools::low_level_tool_definition -- --exact +``` + +--- + +## Implementation notes + +1. Reuse existing per-language E2E harness helpers and style conventions. +2. Keep changes scoped to test code and required wiring. +3. Do not hand-edit generated code. +4. ❌❌❌ DO NOT CHANGE ANY non-test CODE.❌❌❌ +5. ✅✅Put the test in the "right place" for each language. That means put it "near" any similar existing tests. The existing tools E2E test files are: + - `dotnet/test/E2E/ToolsE2ETests.cs` + - `go/internal/e2e/tools_e2e_test.go` + - `nodejs/test/e2e/tools.e2e.test.ts` + - `python/e2e/test_tools_e2e.py` + - `rust/tests/e2e/tools.rs` + Put the new test near those. ✅✅ +6. The snapshot `test/snapshots/tools/low_level_tool_definition.yaml` involves **two conversations**: one where tool calls are made without prior tool results, and one full round-trip (tool calls → tool results → final assistant message). Each language's replay proxy handles this; just ensure the test sends the right prompt and processes tool invocations correctly. +7. The `grep` override tool uses whatever "tool override" mechanism exists in each language (e.g., `ToolDefinition.createOverride` in Java, or the equivalent in each SDK). If a language has no override concept, define it as a regular custom tool named `grep`. + +--- + +## Deliverable + +When done, provide: + +1. files changed per language, +2. isolated command used per language, +3. pass/fail result per language (must be passing before moving to next), +4. any blockers (if any language cannot be completed). diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md b/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md new file mode 100644 index 000000000..cb4b6cd4e --- /dev/null +++ b/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md @@ -0,0 +1,504 @@ +# Implementation plan: `@CopilotTool` ergonomics (issue #1682) + +Human DRI: Ed Burns +ADR: `java/docs/adr/adr-005-tool-definition.md` +Issue: https://github.com/github/copilot-sdk/issues/1682 + +--- + +## Completed phases + +### Phase 1 ✅ — Define the problem and architectural decision + +- ADR-005 evaluates three options (status quo, record-as-schema, annotation-on-method). +- Decision: annotation-on-method with compile-time JSR 269 processor (langchain4j-style API, Micronaut-style implementation). + +### Phase 2 ✅ — Verify the existing low-level path works in Java + +- `test/snapshots/tools/low_level_tool_definition.yaml` created. +- `LowLevelToolDefinitionIT` passes with explicit `ToolDefinition.create()` / `createOverride()`. +- This proves the low-level API is correct and will serve as the foundation that the high-level API delegates to. + +--- + +## Phase 3 — Ignorance reduction: questions to answer before writing code + +This phase is about eliminating unknowns. Each item is a question or spike. Resolve these **before** writing production code. + +### 3.1 — Package placement + +**Question:** Where do `@CopilotTool` and `@Param` live? + +Current SDK structure is a single module (`copilot-sdk-java`). Two options: + +| Option | Location | Trade-off | +|--------|----------|-----------| +| A | `com.github.copilot.rpc` (alongside `ToolDefinition`) | Keeps everything together but the `rpc` package is already dense (40+ classes). | +| B | New package `com.github.copilot.tool` | Cleaner separation; the `tool` package holds annotations, processor, and `ToolDefinition.fromObject()`. But `ToolDefinition` itself stays in `rpc` (it's a JSON-RPC type). | + +**Recommendation:** Option B — new `com.github.copilot.tool` package for annotations + processor + schema generation. `ToolDefinition` stays in `rpc` and gets a new static method `fromObject(Object)` that delegates to `tool` package internals. + +**Action:** Decide; update `module-info.java` exports if new package is added. + +**Resolution:** Select Option B. + +### 3.2 — `@CopilotTool` annotation design + +**Question:** What attributes does `@CopilotTool` need? + +Based on ADR-005 and the C#/langchain4j comparisons: + +```java +@Documented +@Retention(RetentionPolicy.SOURCE) // only needed at compile time for processor +@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; +} +``` + +**Open questions:** + +1. Should `@CopilotTool` have `@Retention(SOURCE)` (processor-only, like Dagger) or `RUNTIME` (fallback reflection path, like langchain4j)? ADR-005 says "compile-time preferred, runtime fallback" — if we want a fallback path, we need `RUNTIME`. If we commit to processor-only, `SOURCE` is sufficient. + +2. Is `ToolDefer` (the defer config from `ToolDefinition`) needed on the annotation, or is that too niche for v1? + +**Recommendation:** Start with `RUNTIME` retention so the reflection fallback works. Defer `ToolDefer` support to a follow-up. + +**Resolution:** Select `RUNTIME` and `ToolDefer` support. + +### 3.3 — `@Param` annotation design + +**Question:** What attributes does `@Param` need? + +```java +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface Param { + /** Parameter description (sent to the model). */ + String value() default ""; + + /** Parameter name override. Defaults to the actual parameter name. */ + String name() default ""; + + /** Whether this parameter is required. Default true. */ + boolean required() default true; + + /** Optional default value when the argument is omitted. */ + String defaultValue() default ""; +} +``` + +**Resolution:** Support `defaultValue()` in v1 (langchain4j parity) and make it behaviorally effective, not docs-only. + +Implementation rules: +- Emit JSON Schema defaults at `properties..default` for model guidance. +- Apply defaults at invocation time when an argument key is missing, then do normal coercion/casting and method invocation. +- Forbid `required=true` together with a non-empty `defaultValue()` (compile-time error in processor, matching runtime reflection fallback validation). +- Parse and validate `defaultValue()` against the Java parameter type (fail fast on mismatch). +- Ensure compile-time generated path and runtime reflection fallback use identical defaulting semantics. + +### 3.4 — Type-to-JSON-Schema mapping + +**Question:** What Java types do we need to map to JSON Schema, and how? + +Minimum viable set (from langchain4j's `JsonSchemaElementUtils`): + +| Java type | JSON Schema | +|-----------|-------------| +| `String` | `{"type": "string"}` | +| `int`, `Integer`, `long`, `Long` | `{"type": "integer"}` | +| `double`, `Double`, `float`, `Float` | `{"type": "number"}` | +| `boolean`, `Boolean` | `{"type": "boolean"}` | +| `enum` types | `{"type": "string", "enum": ["V1", "V2", ...]}` | +| `List`, `Collection` | `{"type": "array", "items": }` | +| `Map` | `{"type": "object"}` (opaque — no inner schema) | +| Records / POJOs | `{"type": "object", "properties": {...}, "required": [...]}` | + +**Key design decision:** The annotation processor must generate this schema at compile time from `javax.lang.model` types (`TypeMirror`, `DeclaredType`, etc.), NOT from `java.lang.reflect` types. This is different from langchain4j which does it at runtime. + +**Spike needed:** Write a small proof-of-concept that maps `TypeMirror` → JSON Schema `Map` literal in generated source code. The tricky cases are: +- Enum constants (processor can see them via `ElementKind.ENUM_CONSTANT`) +- Generic type arguments on `List` (processor sees them via `DeclaredType.getTypeArguments()`) +- Recursive/nested records (need `$defs` or just go one level deep for v1) + +**Recommendation:** Start with the flat types (primitives, String, enums) and `List`. Defer nested records and polymorphic types to a follow-up. + +**Resolution:** + +Here's the **maximum viable set** derived from every type actually used across `com.github.copilot.rpc` and `com.github.copilot.generated.rpc`: + +| Java type | JSON Schema | SDK usage examples | +|-----------|-------------|-------------------| +| `String` | `{"type": "string"}` | Pervasive (IDs, names, paths, messages) | +| `int`, `Integer` | `{"type": "integer"}` | `ProviderConfig`, `ModelLimits`, `McpServerConfig` | +| `long`, `Long` | `{"type": "integer"}` | Timestamps in hook inputs, JSON-RPC IDs | +| `double`, `Double` | `{"type": "number"}` | `ModelBilling.multiplier`, `InfiniteSessionConfig` thresholds | +| `float`, `Float` | `{"type": "number"}` | Not used in SDK today, but natural complement | +| `boolean`, `Boolean` | `{"type": "boolean"}` | Extensively in `SessionConfig`, hook outputs, options | +| `String[]` | `{"type": "array", "items": {"type": "string"}}` | `CopilotClientOptions.cliArgs` | +| `enum` types | `{"type": "string", "enum": ["V1", ...]}` | 92 enums (`ToolDefer`, `AgentMode`, 85 generated) | +| `UUID` | `{"type": "string", "format": "uuid"}` | Generated record ID fields | +| `OffsetDateTime` | `{"type": "string", "format": "date-time"}` | Generated record timestamp fields | +| `JsonNode` | `{}` (any) | `ToolInvocation.argumentsNode`, hook args/results | +| `Object` | `{}` (any) | `ToolDefinition.parameters`, `JsonRpcRequest.params` | +| `List`, `Collection` | `{"type": "array", "items": }` | 50+ distinct `T` across the API | +| `Map` | `{"type": "object", "additionalProperties": {"type": "string"}}` | `ProviderConfig.headers`, env maps | +| `Map` | `{"type": "object", "additionalProperties": {"type": "boolean"}}` | Generated records | +| `Map` | `{"type": "object", "additionalProperties": {"type": "integer"}}` | Generated records | +| `Map` | `{"type": "object"}` (opaque) | `ToolInvocation.getArguments()`, telemetry, extensionData | +| `Map` (typed `T`) | `{"type": "object", "additionalProperties": }` | `Map`, `Map`, etc. | +| `Map>` | `{"type": "object", "additionalProperties": {"type": "array", "items": {"type": "string"}}}` | Generated records | +| Records / POJOs | `{"type": "object", "properties": {...}, "required": [...]}` | ~690 types (26 handwritten records, ~65 POJOs, ~600 generated) | +| Sealed / `@JsonSubTypes` | `{"oneOf": [...]}` with discriminator | `McpServerConfig`, `MessageAttachment`, `SlashCommandInvocationResult` | +| `Optional` | Schema of `T`, not in `required` array | `Optional` in `SessionConfig` | +| `OptionalInt` | `{"type": "integer"}`, not in `required` | `CopilotClientOptions.getSessionIdleTimeoutSeconds()` | +| `OptionalDouble` | `{"type": "number"}`, not in `required` | `ModelBilling.getMultiplierOpt()` | + +**Types intentionally excluded** (not sensible as tool parameter types): + +| Java type | Reason | +|-----------|--------| +| `CompletableFuture` | Return type only, not a parameter type | +| `Consumer`, `Supplier`, `Function`, `Executor` | Callback/functional types, not serializable | +| `ObjectMapper`, `TypeReference` | Infrastructure, not data | + +The minimum viable set from the plan covers 8 rows. This maximum viable set covers **23 rows** — adding `String[]`, `UUID`, `OffsetDateTime`, `JsonNode`/`Object` (any), typed `Map` variants, sealed/polymorphic types, and `Optional` variants. These all have established Jackson serialization semantics in the existing SDK. + +### 3.5 — Generated code shape + +**Question:** What exactly does the processor generate? + +ADR-005 proposes: + +```java +// GENERATED — do not edit +final class MyTools$$CopilotToolMeta { + static List definitions(MyTools instance) { + return List.of( + new ToolDefinition("set_current_phase", "Sets the current phase", + Map.of("type", "object", + "properties", Map.of("phase", Map.of("type", "string", + "description", "The phase to transition to")), + "required", List.of("phase")), + invocation -> { + String phase = (String) invocation.getArguments().get("phase"); + return CompletableFuture.completedFuture( + instance.setCurrentPhase(phase)); + }, null, null, null) + ); + } +} +``` + +**Open questions:** + +1. **Method invocation in generated code:** The generated lambda calls `instance.setCurrentPhase(phase)` directly — no reflection at runtime. But this requires the method to be accessible (not private). What access levels do we support? langchain4j uses `method.setAccessible(true)` at runtime. We'd need to either: + - Require `public` or package-private methods, OR + - Generate a helper that uses `MethodHandles.Lookup` to access private methods (complex), OR + - Just require non-private. + + **Recommendation:** Require at least package-private. The generated `$$CopilotToolMeta` class is in the same package, so package-private and above work. Emit a compile error for `private` methods annotated with `@CopilotTool`. + +2. **Return type handling:** What does the generated code do with the method's return value? + + | Return type | Generated behavior | + |-------------|-------------------| + | `String` | Wrap in `CompletableFuture.completedFuture(result)` | + | `CompletableFuture` | Use as-is (native async) | + | `CompletableFuture` | `.thenApply(objectMapper::writeValueAsString)` | + | `void` | `CompletableFuture.completedFuture("Success")` | + | Other `T` | JSON-serialize via Jackson `ObjectMapper` | + + **Recommendation:** Support `String`, `void`, `CompletableFuture`, and `CompletableFuture` for v1. Other return types get JSON-serialized (since Jackson is already a dependency). + +3. **Argument deserialization in generated code:** How does the generated lambda extract and coerce arguments? + + For simple types, the generated code can cast directly from the `Map` returned by `invocation.getArguments()`: + ```java + String city = (String) invocation.getArguments().get("city"); + int count = ((Number) invocation.getArguments().get("count")).intValue(); + ``` + + For complex types (records, enums), use `invocation.getArgumentsAs()` or Jackson's `ObjectMapper.convertValue()`: + ```java + Phase phase = objectMapper.convertValue(invocation.getArguments().get("phase"), Phase.class); + ``` + + **Recommendation:** Generate direct casts for primitives/String, and `ObjectMapper.convertValue()` for enums, records, and complex types. The `ObjectMapper` instance can come from a static field in the generated class. + +**Resolution:** + +The generated `$$CopilotToolMeta` class produces lambdas identical to what `LowLevelToolDefinitionIT` writes by hand. Decisions: + +1. **Access levels:** Require at least package-private. The generated `$$CopilotToolMeta` lives in the same package as the user's class, so package-private and above work without reflection. Emit a compile error for `private` methods annotated with `@CopilotTool`. + +2. **Return type handling:** + + | Return type | Generated code | + |-------------|----------------| + | `String` | `CompletableFuture.completedFuture(instance.method(...))` | + | `CompletableFuture` | `instance.method(...)` (use as-is) | + | `void` | `instance.method(...); return CompletableFuture.completedFuture("Success")` | + | `CompletableFuture` (non-String) | `instance.method(...).thenApply(objectMapper::writeValueAsString)` | + | Other `T` | `CompletableFuture.completedFuture(objectMapper.writeValueAsString(instance.method(...)))` | + +3. **Argument deserialization:** Follow the two patterns proven in `LowLevelToolDefinitionIT`: + - **Simple types** (`String`, primitives, boxed): generate direct cast from `invocation.getArguments().get("name")` (e.g., `(String) args.get("city")`, `((Number) args.get("count")).intValue()`). + - **Complex types** (enums, records, POJOs): generate `objectMapper.convertValue(invocation.getArguments().get("name"), TargetType.class)`. + - **Single-record-parameter shortcut:** When a method has exactly one parameter that is a record/POJO (matching the full argument set), generate `invocation.getArgumentsAs(RecordType.class)` — the same pattern `LowLevelToolDefinitionIT.setPhaseTool` uses. + + The `ObjectMapper` instance: use a `private static final` field in the generated `$$CopilotToolMeta` class, initialized from `com.github.copilot.rpc.RpcMapper.INSTANCE` (already exists in the SDK). + +### 3.6 — `ToolDefinition.fromObject(Object)` registration API + +**Question:** How does the user get from "an object with `@CopilotTool` methods" to a `List`? + +```java +// Primary API — loads generated $$CopilotToolMeta class +List tools = ToolDefinition.fromObject(myToolsInstance); + +// Variant: from class (for static tools) +List tools = ToolDefinition.fromClass(MyTools.class); +``` + +**Implementation:** + +```java +public static List fromObject(Object instance) { + Class clazz = instance.getClass(); + String metaClassName = clazz.getName() + "$$CopilotToolMeta"; + try { + Class metaClass = Class.forName(metaClassName); + Method defs = metaClass.getMethod("definitions", clazz); + return (List) defs.invoke(null, instance); + } catch (ClassNotFoundException e) { + // Fallback: runtime reflection (if we support it) + return fromObjectReflective(instance); + } +} +``` + +**Open question:** Do we want the reflection fallback? It's nice for users who don't run the processor (e.g., scripting, prototyping), but it adds code and the `-parameters` concern. + +**Recommendation:** Implement the reflection fallback but mark it `@CopilotExperimental` separately. The primary path is the generated `$$CopilotToolMeta`. + +**Resolution:** we only want the processor approach. + +### 3.7 — `module-info.java` impact + +The SDK uses JPMS. The processor generates classes into the user's module, not the SDK's. But `fromObject()` uses `Class.forName()` which needs the generated class to be accessible. + +**Question:** Does the generated `$$CopilotToolMeta` class in the user's module need to be exported for `fromObject()` to find it? + +**Answer:** No. `Class.forName()` with the caller's classloader works within the same module. And in the typical unnamed-module (classpath) case, everything is accessible. If the user has a named module, the generated class is in the same package as their tools class, so it's accessible. + +**Action:** Verify this works in a simple named-module test. + +**Resolution:** + +See `1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/pom.xml` and files + +``` +src/main/java/com/github/dd3021192/MyTools$$CopilotToolMeta.java +src/main/java/com/github/dd3021192/MyTools.java +src/main/java/com/github/dd3021192/Main.java +``` + +For verificiation. + +### 3.8 — Processor registration + +**Question:** How is the new `@CopilotTool` processor registered alongside `CopilotExperimentalProcessor`? + +The existing `META-INF/services/javax.annotation.processing.Processor` lists `CopilotExperimentalProcessor`. Add the new processor to the same file: + +``` +com.github.copilot.CopilotExperimentalProcessor +com.github.copilot.tool.CopilotToolProcessor +``` + +And in `module-info.java`: +```java +provides javax.annotation.processing.Processor + with CopilotExperimentalProcessor, CopilotToolProcessor; +``` + +**Resolution:** **No issues expected here** — this is standard JSR 269 multi-processor registration. + +--- + +## Phase 4 — Implementation (the build order) + +After Phase 3 questions are resolved, implement in this order. Each step should be a separately testable commit. + +### 4.1 — Annotations (`@CopilotTool`, `@Param`) + +**What:** Create the two annotation classes. + +**Files to create:** +- `java/src/main/java/com/github/copilot/tool/CopilotTool.java` +- `java/src/main/java/com/github/copilot/tool/Param.java` + +**Tests:** +- Compile-only: ensure they compile, can be applied to methods/parameters, and are annotated with `@CopilotExperimental`. +- No runtime behavior yet. + +**Gating criteria:** `mvn clean compile` passes. + +### 4.2 — Schema generation utility (compile-time) + +**What:** A utility class that, given `javax.lang.model` types, produces the `Map` JSON Schema as a Java source code literal. + +**Files to create:** +- `java/src/main/java/com/github/copilot/tool/SchemaGenerator.java` (compile-time, works with `TypeMirror`) + +**Tests:** +- Unit tests that exercise the type-to-schema mapping with mock `TypeMirror` instances (or integration tests via the annotation processor in a test compilation). + +**Gating criteria:** Can generate correct schema `Map` source code for: `String`, `int`, `boolean`, `double`, `enum`, `List`, a simple record. + +### 4.3 — Annotation processor (`CopilotToolProcessor`) + +**What:** JSR 269 processor that finds `@CopilotTool` methods and generates `$$CopilotToolMeta` classes. + +**Files to create:** +- `java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java` + +**Files to modify:** +- `java/src/main/resources/META-INF/services/javax.annotation.processing.Processor` — add the new processor +- `java/src/main/java/module-info.java` — add `provides` clause and `exports com.github.copilot.tool` + +**Tests:** +- **Compilation tests:** Compile test source files with `@CopilotTool` methods and verify: + - `$$CopilotToolMeta` class is generated + - Generated schema matches expected JSON Schema + - Compile errors emitted for: private methods, unsupported parameter types, duplicate tool names +- Use `javax.tools.JavaCompiler` programmatically (same pattern langchain4j uses for testing annotation processors). + +**Gating criteria:** Processor generates correct `$$CopilotToolMeta` for a class with 2-3 `@CopilotTool` methods. + +### 4.4 — `ToolDefinition.fromObject(Object)` + +**What:** The runtime bridge that loads generated metadata and returns `List`. + +**Files to modify:** +- `java/src/main/java/com/github/copilot/rpc/ToolDefinition.java` — add `fromObject(Object)` and `fromClass(Class)` static methods + +**Tests:** +- Unit test: create a test class with `@CopilotTool` methods, compile it (processor generates metadata), call `fromObject()`, verify the returned `List` has correct names, descriptions, schemas, and working handlers. + +**Gating criteria:** `ToolDefinition.fromObject(new MyTestTools())` returns a list with working tool definitions. + +### 4.5 — E2E integration test + +**What:** An E2E failsafe IT that uses `@CopilotTool` + `ToolDefinition.fromObject()` against the replay proxy. + +**Files to create:** +- `test/snapshots/tools/ergonomic_tool_definition.yaml` — new snapshot (may be identical to `low_level_tool_definition.yaml` since the wire format is the same) +- `java/src/test/java/com/github/copilot/ErgonomicToolDefinitionIT.java` + +**The test will look like:** + +```java +class MyTestTools { + String currentPhase; + + @CopilotTool("Sets the current phase of the agent") + String setCurrentPhase(@Param("The phase to transition to") String phase) { + currentPhase = phase; + return "Phase set to " + phase; + } + + @CopilotTool("Search for items by keyword") + String searchItems(@Param("Search keyword") String keyword) { + return "Found: item_alpha, item_beta"; + } + + @CopilotTool(value = "Custom grep override", name = "grep", overridesBuiltInTool = true) + String grepOverride(@Param("Search query") String query) { + return "CUSTOM_GREP: " + query; + } +} + +@Test +void ergonomicToolDefinition() throws Exception { + MyTestTools tools = new MyTestTools(); + List toolDefs = ToolDefinition.fromObject(tools); + + // ... create session with toolDefs, send prompt, assert same behavior + // as LowLevelToolDefinitionIT +} +``` + +**Gating criteria:** Test passes with the same assertions as `LowLevelToolDefinitionIT` — proving the ergonomic API produces identical behavior to the explicit API. + +### 4.6 — Reflection fallback (optional, can defer) + +Per resolution 3.6, we have decided to defer item 4.6. + +**What:** `fromObject()` falls back to runtime reflection when `$$CopilotToolMeta` is not found. + +**Files to create/modify:** +- `java/src/main/java/com/github/copilot/tool/ReflectiveToolScanner.java` +- Modify `ToolDefinition.fromObject()` to call this on `ClassNotFoundException` + +**Tests:** +- Compile a test class WITHOUT the annotation processor, call `fromObject()`, verify it still works (with `-parameters` flag). + +**Gating criteria:** Fallback path produces the same `List` as the processor-generated path. + +--- + +## Phase 5 — Documentation and examples + +- Update `java/README.md` with the ergonomic tool definition example. +- Add a "Tools" section showing both the low-level and high-level APIs. +- Reference ADR-005 for design rationale. + +--- + +## Phase 6 — Port to `add-tests-that-use-ergonomic_tool_definition.yaml.md` + +Same cycle as Phase 2 → `add-tests-that-use-low_level_tool_definition.yaml.md`: once the Java E2E test passes with the ergonomic API, create a prompt to port the test to dotnet/go/nodejs/python/rust. + +**Note:** This may not be applicable — the ergonomic API (`@CopilotTool`) is Java-specific. The other SDKs already have their own ergonomic paths. The snapshot can be shared, but the test code is language-specific by nature. Evaluate whether this phase is needed after Phase 5. + +--- + +## Reference: how langchain4j and Micronaut do it + +### langchain4j (runtime reflection) + +- `@Tool` on methods, `@P` on parameters. +- `ToolSpecifications.toolSpecificationsFrom(Object)` scans methods at runtime. +- `JsonSchemaElementUtils` maps `java.lang.reflect.Type` → JSON Schema. +- `DefaultToolExecutor.executeWithContext()` invokes via `Method.invoke()` with argument coercion. +- Requires `-parameters` javac flag or explicit `@P(name="...")`. +- Source: `langchain4j-core/src/main/java/dev/langchain4j/agent/tool/` + +### Micronaut (compile-time annotation processor) + +- `AbstractInjectAnnotationProcessor` (extends `AbstractProcessor`) is the JSR 269 entry point. +- `TypeElementVisitor` SPI pattern: visitors registered via SPI walk the AST. +- `BeanDefinitionWriter` generates bytecode companion classes (`$Definition`, `$Definition$Exec`). +- `ParameterElement.getName()` at compile time — no `-parameters` flag needed. +- Source: `inject-java/src/main/java/io/micronaut/annotation/processing/` + +### Our approach: langchain4j's API + Micronaut's implementation strategy + +- **User-facing API** matches langchain4j: `@CopilotTool` on methods, `@Param` on parameters, `fromObject()` to discover. +- **Implementation** matches Micronaut: JSR 269 processor generates companion classes at compile time, no runtime reflection in the happy path, no `-parameters` requirement. +- **Fallback** path uses langchain4j-style runtime reflection for users who don't run the processor (prototyping, scripting). diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/dependency-reduced-pom.xml b/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/dependency-reduced-pom.xml new file mode 100644 index 000000000..75214f174 --- /dev/null +++ b/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/dependency-reduced-pom.xml @@ -0,0 +1,39 @@ + + + 4.0.0 + com.github + dd3021192 + 1.0.0-SNAPSHOT + + + + maven-jar-plugin + 3.3.0 + + + + com.github.dd3021192.Main + + + + + + maven-shade-plugin + 3.5.1 + + + package + + shade + + + + + + + + UTF-8 + 17 + 17 + + diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/pom.xml b/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/pom.xml new file mode 100644 index 000000000..3d2b26a0e --- /dev/null +++ b/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + com.github + dd3021192 + 1.0.0-SNAPSHOT + jar + + + 17 + 17 + UTF-8 + + + + + com.github + copilot-sdk-java + 1.0.2-SNAPSHOT + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + com.github.dd3021192.Main + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + package + + shade + + + + + + + diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/Main.java b/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/Main.java new file mode 100644 index 000000000..a26845d10 --- /dev/null +++ b/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/Main.java @@ -0,0 +1,58 @@ +package com.github.dd3021192; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +/** + * Named-module JPMS test for issue #1682, Phase 3.7. + * + * Proves that ToolDefinition.fromObject() pattern works in a named module: + * - Class.forName() can locate the generated $$CopilotToolMeta companion class + * - The companion class is accessible (same package, same module) + * - Method invocation on the companion works without extra JPMS exports + */ +public class Main { + + public static void main(String[] args) throws Exception { + System.out.println("=== JPMS Named-Module Test for §3.7 ==="); + System.out.println("Module: " + Main.class.getModule().getName()); + System.out.println(); + + MyTools instance = new MyTools(); + Class toolsClass = instance.getClass(); + + // This is exactly what ToolDefinition.fromObject() will do: + String metaClassName = toolsClass.getName() + "$$CopilotToolMeta"; + System.out.println("Looking up generated meta class: " + metaClassName); + + // Step 1: Class.forName() — the critical JPMS question + Class metaClass = Class.forName(metaClassName); + System.out.println("[PASS] Class.forName() found: " + metaClass.getName()); + + // Step 2: Get the 'definitions' method + Method defsMethod = metaClass.getMethod("definitions", toolsClass); + System.out.println("[PASS] Found method: " + defsMethod); + + // Step 3: Invoke it + @SuppressWarnings("unchecked") + List> definitions = + (List>) defsMethod.invoke(null, instance); + System.out.println("[PASS] Invoked definitions(), got " + definitions.size() + " tool(s)"); + + // Step 4: Verify content + Map tool = definitions.get(0); + assert "set_current_phase".equals(tool.get("name")) + : "Expected tool name 'set_current_phase', got: " + tool.get("name"); + assert "Sets the current phase".equals(tool.get("description")) + : "Expected description mismatch"; + System.out.println("[PASS] Tool definition correct: " + tool.get("name") + + " — \"" + tool.get("description") + "\""); + + System.out.println(); + System.out.println("=== ALL CHECKS PASSED ==="); + System.out.println("Conclusion: Class.forName() works within a named JPMS module"); + System.out.println("for locating $$CopilotToolMeta in the same package. No extra"); + System.out.println("exports or opens directives are needed."); + } +} diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/MyTools$$CopilotToolMeta.java b/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/MyTools$$CopilotToolMeta.java new file mode 100644 index 000000000..3c39f42b9 --- /dev/null +++ b/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/MyTools$$CopilotToolMeta.java @@ -0,0 +1,40 @@ +package com.github.dd3021192; + +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; + +/** + * Simulates the generated companion class that the annotation processor would produce. + * In real usage, this class is auto-generated at compile time. + * + * The key point: it lives in the SAME package as MyTools, so it can call + * package-private methods and is discoverable via Class.forName() from the same module. + */ +public final class MyTools$$CopilotToolMeta { + + private MyTools$$CopilotToolMeta() {} + + /** + * Returns tool definitions for the given MyTools instance. + * This mirrors what the real generated code will produce. + */ + public static List> definitions(MyTools instance) { + return List.of( + Map.of( + "name", "set_current_phase", + "description", "Sets the current phase", + "parameters", Map.of( + "type", "object", + "properties", Map.of( + "phase", Map.of( + "type", "string", + "description", "The phase to transition to" + ) + ), + "required", List.of("phase") + ) + ) + ); + } +} diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/MyTools.java b/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/MyTools.java new file mode 100644 index 000000000..7105a5a8f --- /dev/null +++ b/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/com/github/dd3021192/MyTools.java @@ -0,0 +1,20 @@ +package com.github.dd3021192; + +/** + * Simulates a user's tool class annotated with @CopilotTool methods. + * In real usage, the annotation processor would generate MyTools$$CopilotToolMeta. + */ +public class MyTools { + + private String currentPhase = "init"; + + // This would be annotated with @CopilotTool("Sets the current phase") + public String setCurrentPhase(String phase) { + this.currentPhase = phase; + return "Phase set to " + phase; + } + + public String getCurrentPhase() { + return currentPhase; + } +} diff --git a/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/module-info.java b/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/module-info.java new file mode 100644 index 000000000..1b1d803c7 --- /dev/null +++ b/1682-java-tool-ergonomics-prompts-remove-before-merge/dd3021192/src/main/java/module-info.java @@ -0,0 +1,9 @@ +/** + * Named-module test for issue #1682 Phase 3.7. + * + * Demonstrates that Class.forName() can locate a generated $$CopilotToolMeta + * companion class from within a named JPMS module, without requiring extra exports. + */ +module com.github.dd3021192 { + requires com.github.copilot.java; +}