Skip to content

Commit 170d5aa

Browse files
committed
spotless
1 parent 2144669 commit 170d5aa

5 files changed

Lines changed: 318 additions & 16 deletions

File tree

1682-java-tool-ergonomics-prompts-remove-before-merge/20260622-prompts.md

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,3 +431,309 @@ Ran terminal command: export JAVA_HOME="/Library/Java/JavaVirtualMachines/micro
431431
432432
Made changes.
433433
434+
### 17:46 Prompt
435+
436+
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.
437+
438+
✅✅Pay very close attention to each **Resolution:** to the questions in `Phase 3Ignorance reduction: questions to answer before writing code`.
439+
440+
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)`.
441+
442+
I want you to use `gh` to create issues as sub-issues of 1682.
443+
444+
- I will dispatch these issues by assigning them to an agent. Leave them unassigned for now.
445+
446+
- 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.
447+
448+
- Set the type to **Task**.
449+
450+
- Each issue will be worked on and resolved in serial. You can assume:
451+
452+
- All issues will operate in this topic branch of `upstream`: `edburns/1682-java-tool-ergonomics`.
453+
454+
- 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.
455+
456+
- The sub-sections of `## Phase 4 — Implementation (the build order)` have essential details for each corresponding **Task**.
457+
458+
- Instruct the agent to read the entire plan before working.
459+
460+
- 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.
461+
462+
- Though the sub-sections of `## Phase 4Implementation (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.
463+
464+
### 18:16 Response
465+
466+
Updated todo list
467+
468+
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
469+
470+
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
471+
472+
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
473+
474+
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
475+
476+
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
477+
478+
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
479+
480+
Now I have the full plan. Let me create the 5 issues (4.14.5, skipping 4.6 which is deferred) as sub-issues of #1682. I'll write detailed descriptions with gating tests.
481+
482+
Ran terminal command: gh issue create --help 2>&1 | head -40
483+
484+
`gh issue create` supports `--parent` and `--type` directly. I'll create each issue with a body file.
485+
486+
Created [](file:///tmp/issue-4.1-body.md)
487+
488+
Created [](file:///tmp/issue-4.2-body.md)
489+
490+
Created [](file:///tmp/issue-4.3-body.md)
491+
492+
Created 5 todos
493+
494+
Created [](file:///tmp/issue-4.4-body.md)
495+
496+
Created [](file:///tmp/issue-4.5-body.md)
497+
498+
Now I'll create the issues using `gh`:
499+
500+
Ran terminal command: cd /Users/edburns/workareas/copilot-sdk && gh issue create \
501+
--repo github/copilot-sdk \
502+
--title "[Java] @CopilotTool ergonomics 4.1: Create @CopilotTool and @Param annotations" \
503+
--body-file /tmp/issue-4.1-body.md \
504+
--type "Task" \
505+
--parent 1682
506+
507+
Ran terminal command: cd /Users/edburns/workareas/copilot-sdk && gh issue create \
508+
--repo github/copilot-sdk \
509+
--title "[Java] @CopilotTool ergonomics 4.2: Schema generation utility (TypeMirror to JSON Schema)" \
510+
--body-file /tmp/issue-4.2-body.md \
511+
--type "Task" \
512+
--parent 1682
513+
514+
Ran terminal command: cd /Users/edburns/workareas/copilot-sdk && gh issue create \
515+
--repo github/copilot-sdk \
516+
--title "[Java] @CopilotTool ergonomics 4.3: Annotation processor (CopilotToolProcessor)" \
517+
--body-file /tmp/issue-4.3-body.md \
518+
--type "Task" \
519+
--parent 1682
520+
521+
Ran terminal command: cd /Users/edburns/workareas/copilot-sdk && gh issue create \
522+
--repo github/copilot-sdk \
523+
--title "[Java] @CopilotTool ergonomics 4.4: ToolDefinition.fromObject(Object) registration API" \
524+
--body-file /tmp/issue-4.4-body.md \
525+
--type "Task" \
526+
--parent 1682
527+
528+
Ran terminal command: cd /Users/edburns/workareas/copilot-sdk && gh issue create \
529+
--repo github/copilot-sdk \
530+
--title "[Java] @CopilotTool ergonomics 4.5: E2E integration test with replay proxy" \
531+
--body-file /tmp/issue-4.5-body.md \
532+
--type "Task" \
533+
--parent 1682
534+
535+
All 5 issues created as sub-issues of #1682:
536+
537+
| Phase | Issue | Title |
538+
|-------|-------|-------|
539+
| 4.1 | [#1758](https://github.com/github/copilot-sdk/issues/1758) | Create `@CopilotTool` and `@Param` annotations |
540+
| 4.2 | [#1759](https://github.com/github/copilot-sdk/issues/1759) | Schema generation utility (TypeMirror → JSON Schema) |
541+
| 4.3 | [#1760](https://github.com/github/copilot-sdk/issues/1760) | Annotation processor (`CopilotToolProcessor`) |
542+
| 4.4 | [#1761](https://github.com/github/copilot-sdk/issues/1761) | `ToolDefinition.fromObject(Object)` registration API |
543+
| 4.5 | [#1762](https://github.com/github/copilot-sdk/issues/1762) | E2E integration test with replay proxy |
544+
545+
Each issue includes:
546+
- Explicit list of plan sections to re-read (by section number and title)
547+
- Instruction to read the full plan before starting
548+
- Detailed gating tests beyond what the plan specifies (e.g., reflection-based annotation attribute verification in 4.1, handler invocation tests in 4.4)
549+
- Clear prerequisites (serial dependency chain)
550+
- Branch and constraint information
551+
552+
Updated todo list
553+
554+
Made changes.
555+
556+
### 18:37 Prompt
557+
558+
Consider this text from the plan
559+
560+
--------
561+
562+
**Question:** What attributes does `@CopilotTool` need?
563+
564+
Based on ADR-005 and the C#/langchain4j comparisons:
565+
566+
```java
567+
@Documented
568+
@Retention(RetentionPolicy.SOURCE) // only needed at compile time for processor
569+
@Target(ElementType.METHOD)
570+
@CopilotExperimental
571+
public @interface CopilotTool {
572+
/** Tool description (sent to the model). */
573+
String value();
574+
575+
/** Tool name. Defaults to method name converted to snake_case. */
576+
String name() default "";
577+
578+
/** Whether this tool overrides a built-in tool. */
579+
boolean overridesBuiltInTool() default false;
580+
581+
/** Whether to skip permission checks. */
582+
boolean skipPermission() default false;
583+
}
584+
```
585+
586+
587+
------
588+
589+
I'm concerned about relying on snake_case here. That's a Python thing. Java uses camelCase.
590+
591+
What would be the changes to the overall design if we said camelCase instead of snake_case?
592+
593+
### 18:40 Prompt
594+
595+
Updated todo list
596+
597+
Let me check what naming convention the existing SDK and wire protocol actually use for tool names.
598+
599+
Searched for regex `tool.*name` (`**/test/snapshots/tools/*.yaml`), no results
600+
601+
Searched for regex `snake_case|snakeCase|toSnakeCase|to_snake` (`**/java/**`), 9 results
602+
603+
Searched for regex `DefineTool|define_tool|defineTool` (`**/nodejs/src/**`), 4 results
604+
605+
Read [](file:///Users/edburns/workareas/copilot-sdk/test/snapshots/tools/low_level_tool_definition.yaml)
606+
607+
Searched for regex `DefineTool|tool.*name` (`**/go/*.go`), 10 results
608+
609+
Searched for regex `set_current_phase|search_items|tool.*name` (`**/java/src/test/**/*LowLevel*"`), no results
610+
611+
Searched for regex `set_current_phase|search_items` (`**/java/src/test/**"`), no results
612+
613+
Searched for regex `tool.*name.*snake|tool.*name.*camel` (`**/nodejs/test/**`), no results
614+
615+
Searched for regex `set_current_phase|search_items` (`**/java/src/test/**`), 2 results
616+
617+
Searched for regex `defineTool.*name` (`**/nodejs/samples/**`), no results
618+
619+
Searched for regex `Name:` (`**/go/samples/**`), 1 result
620+
621+
Searched for regex `defineTool` (`**/nodejs/examples/**`), 2 results
622+
623+
Searched for regex `DefineTool|tool.*name` (`**/dotnet/samples/**`), 2 results
624+
625+
Here's the analysis:
626+
627+
## The wire protocol uses `snake_case` — it's not just a Python thing
628+
629+
Looking at the evidence:
630+
631+
1. **Replay snapshot** (low_level_tool_definition.yaml): `set_current_phase`, `search_items`
632+
2. **Node.js examples**: `defineTool("lookup_fact", ...)`
633+
3. **Go samples**: `Name: "manual_resume_status"`
634+
4. **.NET samples**: `"manual_resume_status"`
635+
5. **Java existing test**: `ToolDefinition.create("set_current_phase", ...)`
636+
637+
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.
638+
639+
## Impact of switching to `camelCase`
640+
641+
If you changed the default conversion to camelCase (or just identity — since Java methods are already camelCase):
642+
643+
| Aspect | Impact |
644+
|--------|--------|
645+
| **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. |
646+
| **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. |
647+
| **Snapshot reuse** | Can't share the same test snapshots across SDKs (model expects specific tool names in responses). |
648+
| **Code change** | Actually simplifies the processor — no case conversion logic needed (identity transform). |
649+
| **Override tools** | Built-in Copilot tools use `snake_case` (like `grep`). Overriding them already requires explicit `name="grep"`, so no impact there. |
650+
651+
## Recommendation
652+
653+
Keep `snake_case` as the **default** wire name derivation. The rationale is protocol-level consistency, not Java style:
654+
655+
- The Java **method** stays camelCase (`setCurrentPhase`) — idiomatic Java.
656+
- The **tool name** sent to the model is `set_current_phase` — idiomatic function-calling protocol.
657+
- Users can always override with `@CopilotTool(name="whateverTheyWant")`.
658+
659+
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.
660+
661+
If you still prefer dropping the conversion, the only code changes would be:
662+
1. Remove the snake_case converter from the processor (4.3).
663+
2. Update the test in 4.3 that verifies `setCurrentPhase` → `set_current_phase`.
664+
3. Update the E2E snapshot/test (4.5) tool names.
665+
666+
### 18:41 Prompt
667+
668+
Look at what the agent changed in commit 8b899e15254b2b539ae585c85d1e7c39ef864194 to `java/src/main/java/com/github/copilot/rpc/ToolDefer.java`. It added NONE.
669+
670+
Is there a precedent for this NONE thing in the other languages?
671+
672+
### 18:43 Response
673+
674+
Updated todo list
675+
676+
Ran terminal command: git show 8b899e15254b2b539ae585c85d1e7c39ef864194 -- java/src/main/java/com/github/copilot/rpc/ToolDefer.java
677+
678+
Searched for regex `ToolDefer|toolDefer|tool_defer|defer` (`**/nodejs/src/**`), 20 results
679+
680+
Searched for regex `ToolDefer|Defer` (`**/go/**/*.go`), 20 results
681+
682+
Searched for regex `ToolDefer|Defer` (`**/dotnet/src/**`), 20 results
683+
684+
Searched for regex `CopilotToolDefer` (`**/dotnet/src/**`), 8 results
685+
686+
Searched for regex `defer` (`**/nodejs/src/types.ts`), 4 results
687+
688+
Read [](file:///Users/edburns/workareas/copilot-sdk/dotnet/src/CopilotTool.cs#155-155), lines 155 to 170
689+
690+
Read [](file:///Users/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefer.java#1-1), lines 1 to 10
691+
692+
Read [](file:///Users/edburns/workareas/copilot-sdk/java/src/main/java/com/github/copilot/rpc/ToolDefer.java#20-20), lines 20 to 50
693+
694+
**No, there is no precedent for `NONE` in the other languages.**
695+
696+
- **.NET**: `CopilotToolDefer` has only `Auto` and `Never`. The field is `CopilotToolDefer?` (nullable) — absence is expressed as `null`.
697+
- **Node.js**: `defer?: "auto" | "never"` — absence is expressed as `undefined` (the field is optional).
698+
- **Go**: Would use a pointer or zero value.
699+
700+
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.
701+
702+
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.
703+
704+
### 18:45 Prompt
705+
706+
Let's take .NET. What is the behavior of the tool in the "absence" case? And what does that serialize as?
707+
708+
### 18:47 Response
709+
710+
711+
Updated todo list
712+
713+
Read [](file:///Users/edburns/workareas/copilot-sdk/dotnet/src/CopilotTool.cs#140-140), lines 140 to 160
714+
715+
Read [](file:///Users/edburns/workareas/copilot-sdk/dotnet/src/Client.cs#2415-2415), lines 2415 to 2450
716+
717+
Now let me see how `ToolDefinition` serializes when `Defer` is null:
718+
719+
Searched for regex `ToolDefinition.*serial|WriteJson|JsonSerializ|toolDefinition.*json|RegisterTools|registerTools` (`**/dotnet/src/Client.cs`), 20 results
720+
721+
Read [](file:///Users/edburns/workareas/copilot-sdk/dotnet/src/Client.cs#2117-2117), lines 2117 to 2130
722+
723+
There's the answer:
724+
725+
**In .NET, when `Defer` is `null`, the key is simply omitted from the JSON.**
726+
727+
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."
728+
729+
So the wire behavior for "absence" is: **the `defer` field does not appear in the JSON-RPC message**.
730+
731+
---
732+
733+
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.
734+
735+
### 19:11 Prompt
736+
737+
For all the created issues please find the right place to add the following requirement:
738+
739+
✅✅ YOU MUST run `mvn spotless:apply` before every commit.

java/src/main/java/com/github/copilot/rpc/ToolDefer.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
public enum ToolDefer {
2323

2424
/**
25-
* No deferral preference set. Used as the default for annotation-based
26-
* tool definitions where the user has not explicitly chosen a mode.
25+
* No deferral preference set. Used as the default for annotation-based tool
26+
* definitions where the user has not explicitly chosen a mode.
2727
*/
2828
NONE(""),
2929

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,8 @@
2222
*
2323
* <pre>
2424
* &#64;CopilotTool("Get weather for a location")
25-
* public CompletableFuture&lt;String&gt; getWeather(
26-
* &#64;Param(value = "City name", required = true) String location) {
27-
* return CompletableFuture.completedFuture("Sunny in " + location);
25+
* public CompletableFuture&lt;String&gt; getWeather(&#64;Param(value = "City name", required = true) String location) {
26+
* return CompletableFuture.completedFuture("Sunny in " + location);
2827
* }
2928
* </pre>
3029
*

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,9 @@
1919
*
2020
* <pre>
2121
* &#64;CopilotTool("Search for issues")
22-
* public CompletableFuture&lt;String&gt; searchIssues(
23-
* &#64;Param(value = "Search query", required = true) String query,
24-
* &#64;Param(value = "Max results", required = false, defaultValue = "10") int limit) {
25-
* // ...
22+
* public CompletableFuture&lt;String&gt; searchIssues(&#64;Param(value = "Search query", required = true) String query,
23+
* &#64;Param(value = "Max results", required = false, defaultValue = "10") int limit) {
24+
* // ...
2625
* }
2726
* </pre>
2827
*

java/src/test/java/com/github/copilot/tool/CopilotToolAnnotationTest.java

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ void copilotToolHasRuntimeRetention() {
3737
void copilotToolTargetsMethod() {
3838
Target target = CopilotTool.class.getAnnotation(Target.class);
3939
assertNotNull(target);
40-
assertArrayEquals(new ElementType[] {ElementType.METHOD}, target.value());
40+
assertArrayEquals(new ElementType[]{ElementType.METHOD}, target.value());
4141
}
4242

4343
@Test
@@ -46,7 +46,7 @@ void copilotToolIsAnnotatedWithCopilotExperimental() {
4646
// reflection at runtime. However, we can confirm:
4747
// 1. The annotation type targets TYPE (which includes @interface declarations).
4848
// 2. Compilation succeeded with @CopilotExperimental on @CopilotTool
49-
// (the CopilotExperimentalProcessor would reject usage otherwise).
49+
// (the CopilotExperimentalProcessor would reject usage otherwise).
5050
Target expTarget = CopilotExperimental.class.getAnnotation(Target.class);
5151
assertNotNull(expTarget);
5252
boolean includesType = false;
@@ -56,8 +56,7 @@ void copilotToolIsAnnotatedWithCopilotExperimental() {
5656
break;
5757
}
5858
}
59-
assertTrue(includesType,
60-
"@CopilotExperimental must target TYPE to be applicable to annotation declarations");
59+
assertTrue(includesType, "@CopilotExperimental must target TYPE to be applicable to annotation declarations");
6160
}
6261

6362
@Test
@@ -88,7 +87,7 @@ void paramHasRuntimeRetention() {
8887
void paramTargetsParameter() {
8988
Target target = Param.class.getAnnotation(Target.class);
9089
assertNotNull(target);
91-
assertArrayEquals(new ElementType[] {ElementType.PARAMETER}, target.value());
90+
assertArrayEquals(new ElementType[]{ElementType.PARAMETER}, target.value());
9291
}
9392

9493
@Test
@@ -112,8 +111,7 @@ void paramDefaultValues() throws Exception {
112111
static class SampleToolHolder {
113112

114113
@CopilotTool(value = "Get weather for a location", name = "get_weather", defer = ToolDefer.AUTO)
115-
public CompletableFuture<String> getWeather(
116-
@Param(value = "City name", required = true) String location,
114+
public CompletableFuture<String> getWeather(@Param(value = "City name", required = true) String location,
117115
@Param(value = "Temperature unit", required = false, defaultValue = "celsius") String unit) {
118116
return CompletableFuture.completedFuture("Sunny in " + location);
119117
}

0 commit comments

Comments
 (0)