From 84414c50892c83cca0a342ef14101f5b2f1b0a23 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Fri, 26 Jun 2026 17:44:29 -0400 Subject: [PATCH 01/13] ADR-006 --- .../adr/adr-006-tool-definition-inline.md | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 java/docs/adr/adr-006-tool-definition-inline.md diff --git a/java/docs/adr/adr-006-tool-definition-inline.md b/java/docs/adr/adr-006-tool-definition-inline.md new file mode 100644 index 000000000..fb2b9bf8b --- /dev/null +++ b/java/docs/adr/adr-006-tool-definition-inline.md @@ -0,0 +1,109 @@ +# ADR-006: Inline tool definition with lambdas + +## Context and problem statement + +ADR-005 introduced an ergonomic Java tools API based on `@CopilotTool` and `ToolDefinition.fromObject(...)`. That model works well when teams define tools as methods on a class. + +The next ergonomics goal is an inline style comparable to C# `CopilotTool.DefineTool(...)`, where developers can define a tool at the call site without creating a separate tool container class. + +For this decision, we evaluated two alternatives: + +* Method-reference registration (`ToolDefinition.from(tools::setCurrentPhase)`) +* Inline lambda registration (`ToolDefinition.from(..., phase -> ...)`) + +The key factor is metadata quality: tool name, description, parameter names, parameter descriptions, required/default semantics, and schema stability. + +## Considered options + +### Option 1: Method-reference API + +Example: + +```java +ToolDefinition setPhase = ToolDefinition.from(tools::setCurrentPhase); +``` + +In this model, metadata is sourced from existing method-level annotations (`@CopilotTool`, `@Param`) on the referenced method. + +Advantages: + +* Closest Java analog to C# method-group ergonomics +* High-quality metadata with minimal additional API surface +* Reuses ADR-005 metadata and invocation behavior directly + +Drawbacks: + +* Not truly inline: still requires a declared method (and usually annotations) elsewhere +* Does not solve the "define the whole tool at the call site" use case +* Method-reference resolution adds runtime/reflection complexity + +### Option 2: Inline lambda API with explicit metadata + +Example: + +```java +ToolDefinition setPhase = ToolDefinition.from( + "set_current_phase", + "Sets the current phase of the agent", + Params.of(ParamDef.string("phase", "The phase to transition to")), + (String phase) -> { + currentPhase = phase; + return "Phase set to " + phase; + }); +``` + +In this model, handler logic is inline, and metadata is provided explicitly through a small parameter-definition DSL. + +Advantages: + +* True inline authoring at the session construction site +* No dependence on lambda parameter-name reflection or `-parameters` +* Deterministic metadata and schema generation +* Independent from annotation processing and generated companion classes + +Drawbacks: + +* Slightly more verbose than method-reference style because metadata is explicit +* Introduces new public API types for parameter definitions and typed lambda overloads +* Requires careful API design to stay concise for common one-parameter tools + +## Decision outcome + +Chosen: **Option 2 for ADR-006 scope** — inline lambda API with explicit metadata. + +Rationale: + +1. The primary requirement for this ADR is inline definition. Option 2 satisfies it directly; Option 1 does not. +1. Metadata quality is the critical requirement. Option 2 keeps metadata explicit and stable, instead of relying on fragile lambda introspection. +1. Option 2 can ship independently of method-reference support and without changes to annotation processing. +1. Option 2 preserves behavior parity with existing tool execution by delegating to `ToolDefinition` construction and current invocation semantics. + +Option 1 remains valuable and can be added independently as a separate ergonomic layer. It is not blocked by this decision. + +## Design constraints and non-goals + +Constraints for the inline lambda API: + +* Require explicit tool name and description. +* Require explicit parameter metadata (at minimum name and type, with optional description/required/default). +* Support both sync and async handlers (`R` and `CompletableFuture`). +* Keep result semantics aligned with existing behavior (`String` passthrough, `void` maps to `"Success"`, non-string objects serialized to JSON). +* Keep override/permission/defer flags available through options, consistent with existing `ToolDefinition` fields. + +Non-goals for this ADR: + +* Replacing `@CopilotTool`/`fromObject` APIs. +* Defining method-reference registration behavior in detail. +* Introducing compile-time code generation for lambda metadata. + +## Consequences + +If implemented, the SDK gains an explicit inline path for developers who prefer to keep tool declarations at session creation while preserving high-quality schema metadata. + +The annotation-driven API from ADR-005 remains the recommended path for larger tool surfaces where co-locating metadata with method implementations improves maintainability. + +## Related work items + +* #1682 +* #1792 +* #1810 From ad89a0f79637f453c9dd097160b94302ffdcd5dc Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Fri, 26 Jun 2026 18:01:35 -0400 Subject: [PATCH 02/13] Plan --- ...rance-reduction-for-implementation-plan.md | 272 ++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md diff --git a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md new file mode 100644 index 000000000..b7629cbb8 --- /dev/null +++ b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md @@ -0,0 +1,272 @@ +# Implementation plan: tool-as-lambda ergonomics (issue #1810) + +Human DRI: Ed Burns +ADR: `java/docs/adr/adr-006-tool-definition-inline.md` +Related ADR: `java/docs/adr/adr-005-tool-definition.md` +Issue: #1810 + +--- + +## Completed phases + +### Phase 1 ✅ — Define the problem and architectural decision + +* We evaluated method-reference and inline approaches for `ToolDefinition.from(...)`. +* Decision (ADR-006): implement inline tool definition with explicit metadata (name, description, parameter definitions) and typed lambda handlers. +* Key decision driver: metadata quality and schema stability. + +--- + +## Phase 2 — Baseline verification before new API work + +This phase confirms the current runtime behavior we must preserve. + +### 2.1 — Confirm low-level tool behavior contract + +**Question:** What behavior must inline tools match exactly? + +Use existing tests and code as ground truth: + +* `ToolDefinition.create(...)` family in `java/src/main/java/com/github/copilot/rpc/ToolDefinition.java` +* Existing ergonomic behavior in `ToolDefinition.fromObject(...)` tests +* E2E tests under `java/src/test/java/com/github/copilot/e2e/` + +**Contract to preserve:** + +* `String` return is sent as-is. +* `void` return maps to `"Success"`. +* non-`String` return values are JSON-serialized. +* `CompletableFuture` is supported. +* `overridesBuiltInTool`, `skipPermission`, and `defer` are carried through. + +**Resolution target:** Document this contract as explicit acceptance criteria for all new `from(...)` overloads. + +--- + +## Phase 3 — Ignorance reduction: questions to answer before writing code + +Resolve these unknowns before production edits. + +### 3.1 — Public API shape for `ToolDefinition.from(...)` + +**Question:** Which overloads ship in v1? + +Candidate API: + +```java +ToolDefinition.from(String name, String description, Params params, ToolFn1 handler) +ToolDefinition.from(String name, String description, Params params, AsyncToolFn1 handler) +ToolDefinition.from(String name, String description, Params params, ToolFn2 handler) +// ...up to a practical arity cap +``` + +Open decisions: + +1. Arity cap for v1 (`1..3` vs `1..5`). +1. Whether zero-arg tools need a dedicated overload. +1. Whether `ToolInvocation` context injection is included in v1. + +**Recommendation:** start with arity `0..3`, plus context-capable variants only if they remain concise. + +### 3.2 — Functional interface set and type inference + +**Question:** What functional interfaces are needed for clean lambda syntax without casts? + +Unknowns: + +* Naming (`ToolFn1`, `ToolFn2`, `AsyncToolFn1`, etc.). +* Package placement (`com.github.copilot.rpc` vs `com.github.copilot.tool`). +* How to avoid ambiguous overload resolution between sync and async lambdas. + +**Recommendation:** use distinct interfaces for sync and async handlers and keep overload count minimal to reduce ambiguity. + +### 3.3 — Parameter metadata DSL design + +**Question:** What is the smallest expressive parameter-definition API that preserves schema quality? + +Candidate concepts: + +* `ParamDef` builders (type, name, description, required/default). +* `Params.of(...)` container preserving declaration order. +* Optional helpers for common primitives. + +Unknowns: + +1. How defaults are represented and validated by type. +1. How optionality interacts with default values. +1. Whether descriptions are required by policy. + +**Recommendation:** align with `@Param` semantics from ADR-005 wherever possible. + +### 3.4 — Type-to-JSON-schema mapping for inline params + +**Question:** Which Java parameter types are supported in v1 for inline definitions? + +Minimum set: + +* `String` +* numeric primitives/boxed +* `boolean`/`Boolean` +* enums +* `List` for simple `T` +* `Map` (or defer typed map support if not stable) +* record/POJO as parameter type + +Unknowns: + +* Whether nested objects and polymorphic types are in scope for v1. +* Whether schema generation should reuse existing tool schema utilities directly. + +**Recommendation:** implement the subset already validated by existing ergonomic and low-level tests, then extend. + +### 3.5 — Invocation and coercion policy + +**Question:** How are JSON arguments coerced into typed lambda arguments? + +Options: + +* Reuse the same `ObjectMapper` conversion policy used by existing ergonomic tooling. +* Add bespoke coercion logic per primitive and complex type. + +**Recommendation:** reuse existing mapper policy for consistency and reduced risk. + +### 3.6 — Tool options and advanced flags + +**Question:** How do callers set `overridesBuiltInTool`, `skipPermission`, and `defer` on inline tools? + +Candidates: + +* Overloads with an options object. +* Fluent builder wrapping `ToolDefinition.from(...)`. + +**Recommendation:** options object first, to avoid overload explosion. + +### 3.7 — Error model and validation boundaries + +**Question:** Which invalid states should fail early? + +Must-validate cases: + +* duplicate parameter names +* missing required metadata (name/type) +* unsupported type mappings +* incompatible default values + +**Recommendation:** fail fast at tool construction with precise `IllegalArgumentException` messages. + +### 3.8 — Binary compatibility and package placement + +**Question:** Where do new public types live without destabilizing existing API? + +Unknowns: + +* whether to place new functional interfaces and param DSL under `rpc` or `tool` +* impact on `module-info.java` exports + +**Recommendation:** place user-facing ergonomics in the package users already discover for tools, and keep internal helpers package-private. + +### 3.9 — E2E test scenario and snapshot reuse + +**Question:** Do we need a new replay snapshot? + +Because wire format should match existing tool definitions, we should attempt snapshot reuse first. + +**Recommendation:** start with existing tool-definition snapshot; only add a new YAML if wire traffic differs. + +--- + +## Phase 4 — Implementation (build order) + +After Phase 3 is resolved, implement in this order. + +### 4.1 — Add public API types + +**What:** Introduce functional interfaces and parameter metadata classes for inline tools. + +**Likely files:** + +* `java/src/main/java/com/github/copilot/rpc/` (new interfaces and metadata types) + +**Gating criteria:** compile passes; API signatures are stable and unambiguous for common lambda call sites. + +### 4.2 — Implement `ToolDefinition.from(...)` overloads + +**What:** Add typed overloads that build `ToolDefinition` plus invocation adapter. + +**Likely files:** + +* `java/src/main/java/com/github/copilot/rpc/ToolDefinition.java` + +**Gating criteria:** unit tests prove schema output and handler invocation for arities and sync/async paths. + +### 4.3 — Implement schema and coercion internals + +**What:** Build internal mapping from `ParamDef` + handler type info to JSON schema and typed invocation. + +**Likely files:** + +* new internal helper(s) under `java/src/main/java/com/github/copilot/rpc/` or `.../tool/` + +**Gating criteria:** matches baseline behavior contract from Phase 2. + +### 4.4 — Unit tests for API behavior and validation + +**What:** Add focused tests for: + +* successful inline definitions (0..N args) +* sync and async handlers +* option flags propagation +* default/required semantics +* error paths + +**Likely files:** + +* `java/src/test/java/com/github/copilot/rpc/*` + +**Gating criteria:** deterministic tests covering success + failure paths. + +### 4.5 — E2E integration test + +**What:** Add/extend a Java E2E test that uses inline tool definition in a real session. + +**Likely files:** + +* `java/src/test/java/com/github/copilot/e2e/*` +* `test/snapshots/tools/*` (only if new snapshot required) + +**Gating criteria:** E2E passes with expected assistant behavior and tool side effects. + +### 4.6 — Documentation updates + +**What:** Document inline tool definition in Java README and link ADR-006. + +**Likely files:** + +* `java/README.md` +* `java/docs/adr/adr-006-tool-definition-inline.md` (if follow-up clarifications are needed) + +**Gating criteria:** examples compile conceptually and reflect final API names. + +--- + +## Phase 5 — Portability and follow-on work + +### 5.1 — Evaluate method-reference API as separate workstream + +Method-reference registration can be implemented independently after inline tool definition. Track this as separate scope to keep issue #1810 focused. + +### 5.2 — Expand type coverage + +After v1, add deeper schema coverage (nested objects, richer map/list combinations, polymorphic payloads) based on real usage demand. + +--- + +## Acceptance checklist + +Before calling implementation complete: + +1. Inline tool definitions can be authored at call site without annotation processing. +1. Metadata quality (name/description/params/defaults/required) is explicit and stable. +1. Runtime behavior matches existing tool contract (`String`/`void`/JSON/async/options flags). +1. Unit and E2E tests pass for the implemented scope. +1. Java README includes at least one concise inline tool example. From 9ad6c2b2cbe5f61bec54822ec9edc909a4108d08 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Fri, 26 Jun 2026 18:05:59 -0400 Subject: [PATCH 03/13] GUTDODP --- .../20260626-prompts.md | 277 ++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260626-prompts.md diff --git a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260626-prompts.md b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260626-prompts.md new file mode 100644 index 000000000..74a4d412d --- /dev/null +++ b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260626-prompts.md @@ -0,0 +1,277 @@ +## ghcp-sdk-1810-lambda + + +### 16:30 Prompt + +Consider this Copliot SDK C# usage of the C# tools api: + +```C# + 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), + ], + }); +``` + +Note that the `ReportIntent` tool definition uses some kind of inline code, like a lambda in Java. + +We recently merged in support for the an improved tools API in Java. See the files merged in PR #1792. + +Issue #1810 is a stub issue aimed at enabling an analogous, lamba, style to this C# usage: + +``` +CopilotTool.DefineTool(ReportIntent, new() { OverridesBuiltInTool = true }), +``` + +Look at Epic #1682 and its child issues for the standard of verbosity and concreteness for issue descriptions. + +Use `gh` to update the description for #1810 so that we can assign it to a coding agent so it can design and implement the lambda style idiom for defining tools. + +I have added directory `C:\Users\edburns\workareas\copilot-sdk-01`. Look in `copilot-sdk-01/1682-java-tool-ergonomics-prompts-remove-before-merge/simple-weather-demo` for a simple example of the existing Java tools ergonomic API. + +The `WeatherToolDemo` class has this code: + +``` +List toolDefs = ToolDefinition.fromObject(weatherTools); +``` + +I'd like to see a method on `ToolDefinition` called `from` that takes a lambda that is the tool. + +It must support parameters just as the @CopilotTool annotation supports parameters. + + +### 17:01 Prompt + +Consider this Copliot SDK C# usage of the C# tools api: + +```C# + 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), + ], + }); +``` + +Note that the `ReportIntent` tool definition uses some kind of inline code, like a lambda in Java. + +We recently merged in support for the an improved tools API in Java. See the files merged in PR #1792. + +Look in `java/src/test/java/com/github/copilot/e2e/ErgonomicToolDefinitionIT.java` and `java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools.java` for a simple example of the existing Java tools ergonomic API. + +Here is are some excerpts: + +``` +ErgonomicTestTools tools = new ErgonomicTestTools(); +List toolDefs = ToolDefinition.fromObject(tools); +try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client + .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL) + .setAvailableTools(new ToolSet().addCustom("*").addBuiltIn("web_fetch")).setTools(toolDefs)) + .get(30, TimeUnit.SECONDS); + } +} +``` + +``` +class ErgonomicTestTools { + + String currentPhase; + + @CopilotTool("Sets the current phase of the agent") + public String setCurrentPhase(@Param("The phase to transition to") String phase) { + currentPhase = phase; + return "Phase set to " + phase; + } + + @CopilotTool("Search for items by keyword") + public String searchItems(@Param("Search keyword") String keyword) { + return "Found: " + keyword + " -> item_alpha, item_beta"; + } +} +``` + +I'd like to see a method on `ToolDefinition` called `from` that takes a lambda that is the tool. + +It must support parameters just as the `@CopilotTool` annotation supports parameters. + +Something like this, using the preceding example. + +``` +ErgonomicTestTools tools = new ErgonomicTestTools(); +ToolDefinition setPhaseTool = ToolDefinition.from(phase -> currentPhase = phase ); +ToolDefinition searchItemsTool = ToolDefinition.from(keyword -> performSearch(keyword) ); +List tools = new ArrayList(); +tools.add(setPhaseTool); +tools.add(searchItemsTool); +``` + +I want to exit this chat having written a plan to `1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-java-tool-lamba-plan.md`. + +But first I need to perform some ignorance reduction. + +1. Is it possible? + +### 17:26 Response + +Yes, with one important caveat: adding  ToolDefinition.from(...)  for lambda-based tools is feasible in this codebase. + +A bare lambda like  ToolDefinition.from(phase -> ...)  cannot, by itself, reliably provide all  @Param -style metadata (description, required/default, stable parameter name). So the exact syntax you sketched is feasible for behavior, but not enough for full parameter metadata unless you add metadata arguments (or use method references to annotated methods). + +A practical path is: + +1.  ToolDefinition.from("set_current_phase", "Sets the current phase", Params.of("phase", "..."), phase -> ...)  for true inline lambdas. +2.  ToolDefinition.from(tools::setCurrentPhase)  for concise usage when metadata is on a real method (reflectable). + +### 17:27 Prompt + +Look at the lambda-like think in C#. How does that work? + +### 17:29 Response + +I’ll inspect the .NET SDK  CopilotTool.DefineTool(...)  implementation to show exactly how the C# method-group/lambda-like form works. + +Search "class CopilotTool|DefineTool\(|ToolAttribute|DescriptionAttribute|ParameterInfo|MethodInfo" … 4 files found + +Search "**/*CopilotTool*.cs" in dotnet 2 files found +Search "**/*ToolDefinition*.cs" in dotnet No matches found + +Read CopilotTool.cs 169 lines read +Read CopilotToolTests.cs 142 lines read + +Search "DefineTool\(|GetMethodInfo|DescriptionAttribute|DisplayName|JsonPropertyName|ParameterDescr… 48 lines found + +Read README.md L440:560 (121 lines read) + +Search "CopilotTool\.DefineTool\((SetCurrentPhase|ReportIntent|\w+\)|\w+,\s*new\(\)\s*\{\s*OverridesB… 1 line found + +It works because  CopilotTool.DefineTool  accepts a  Delegate , and in C# both method groups ( SetCurrentPhase ) and lambdas ( (string id) => ... ) are convertible to delegates. + +In this SDK,  DefineTool  mostly wraps  AIFunctionFactory.Create(method, factoryOptions)  ( dotnet/src/CopilotTool.cs:36-49 ). The heavy lifting is done by  AIFunctionFactory : it reflects over the delegate’s method signature to build tool schema and invocation binding. That’s why C# can feel “inline/ergonomic”: + +• Method group form: metadata can come from method attributes like  [DisplayName]  and  [Description]  (see  CopilotToolTests.cs:139-141 , inferred in  :26-27 ). +• Lambda form: parameter metadata can be attached inline via attributes like  [Description("Issue identifier")] string id  (README  dotnet/README.md:462-471 ). +• Copilot-specific flags ( OverridesBuiltInTool ,  SkipPermission ,  Defer ) are then layered into  AdditionalProperties  by this wrapper ( CopilotTool.cs:88-117 ). + +So the “lambda-like thing” is standard C# delegate conversion + runtime reflection over the delegate target/signature. + +### 17:29 Prompt + +How would you design an elegant analog to this using lambdas in the Java API? + +### 17:33 Response + +I’d design it as a two-lane API so Java stays elegant without pretending lambdas can carry C#-style parameter metadata. + +Lane 1 (most ergonomic): method reference + annotations + +ToolDefinition setPhase = ToolDefinition.from(tools::setCurrentPhase); +ToolDefinition reportIntent = ToolDefinition.from(tools::reportIntent, ToolOptions.overrideBuiltIn()); + +• Resolve the referenced method ( SerializedLambda ) and reuse existing  @CopilotTool  /  @Param  metadata. +• This is the closest analog to C#  DefineTool(SetCurrentPhase) . + +Lane 2 (true inline lambda): explicit metadata DSL + +ToolDefinition setPhase = ToolDefinition.from( + "set_current_phase", + "Sets the current phase of the agent", + Params.of(ParamDef.string("phase", "The phase to transition to")), + (String phase) -> { + currentPhase = phase; + return "Phase set to " + phase; + }); + +• Required because Java lambdas don’t reliably expose parameter names/descriptions/defaults. +• Keep this concise with typed overloads ( from(name, desc, ParamDef1, Fn1) ,  Fn2 , etc.) and async support ( R  or  CompletableFuture ). + +That gives Java users a clean C#-like path ( from(methodRef) ) plus a practical inline-lambda path when no annotated method exists. + +### 17:33 Prompt + +I like this a lot. But the two lanes seem independently implementable. Are they not? + +### 17:34 Response + +Yes—they’re independently implementable. + +The shared destination is  ToolDefinition  construction, but each lane has separate metadata/introspection mechanics: + +1. Method-reference lane: implement  ToolDefinition.from(methodRef, ...)  by resolving the referenced method and reusing existing  @CopilotTool / @Param  schema+invocation logic. +2. Inline-lambda lane: implement overloads that require explicit metadata ( name ,  description , param defs) plus typed functional interfaces ( Fn1 ,  Fn2 , etc.). +3. You can ship either first without blocking the other; only a small internal helper should be shared (normalize options, schema map assembly, return coercion policy). + +### 17:34 Prompt + +Let us continue with ignorance reduction on the lambda lane for now. + +Look at `copilot-sdk-01/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md`. This is the ignorance reduction plan for the initial ergonomic tools feature. + +We are now in Phase 1 for the "define tools with a lambda" feature. + +We haven't even written out an ADR for this feature yet. Let's start by writing ADR 006 about inline tool definition. As the alternatives we looked at the method-reference and the lambda approach. We elicited that metadata was the most important factor to consider. Write out `copilot-sdk/java/docs/adr/adr-006-tool-definition-inline.md` as the new ADR. + +### 17:45 Response + +I’ll draft ADR 006 in the same style as existing ADRs, grounded in the phase-1 ignorance-reduction notes and the metadata tradeoff between method references and inline lambdas. + +Read docs-style.instructions.md 226 lines read +Read dd-3018003-ignorance-reduction-for-implementation-plan.md Path does not exist + +Search "**/dd-3018003-ignorance-reduction-for-implementation-plan.md" No matches found +Search "**/dd-3018003-ignorance-reduction-for-implementation-plan.md" in ~\workareas 1 file found + +Read dd-3018003-ignorance-reduction-for-implementation-plan.md 1 line read + +Search "^#|^##|^###|Phase 1|metadata|method reference|lambda|alternatives|decision" (~\workareas\co… 32 lines found + +Read dd-3018003-ignorance-reduction-for-implementation-plan.md L1:140 (139 lines read) +Read adr-005-tool-definition.md L1:260 (235 lines read) + +Edit Create java\docs\adr\adr-006-tool-definition-inline.md + +Read adr-006-tool-definition-inline.md 109 lines read + +Created  java/docs/adr/adr-006-tool-definition-inline.md  with ADR 006. + +It frames the two alternatives (method reference vs inline lambda), makes metadata quality the deciding criterion, and records the decision to scope ADR 006 to an inline lambda API with explicit metadata DSL, while keeping method-reference support as an independent follow-on. + +### 17:46 Prompt + +Look at `copilot-sdk-01/1682-java-tool-ergonomics-prompts-remove-before-merge/dd-3018003-ignorance-reduction-for-implementation-plan.md`. This is the ignorance reduction plan for the initial ergonomic tools feature. + +We have now completed the analogous Phase 1. + +Using the `dd-3018003` plan as a guide, map out what we've been calling the "lambda lane" but don't call it that. Call it tool-as-lambda. + +Write it to `1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md`. + +### 18:01 Response + +I’ll mirror the structure and depth of  dd-3018003 , then author the new plan file for the tool-as-lambda feature with phases, open questions, and implementation order. + +Search "**/1810-ignorance-reduction-for-implementation-plan.md" No matches found +Search "**/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/**" No matches found + +Read dd-3018003-ignorance-reduction-for-implementation-plan.md L140:520 (365 lines read) + +Edit Create 1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge\1810-ignorance-reduction-for-implementati + +Read 1810-ignorance-reduction-for-implementation-plan.md L1:260 (260 lines read) + +Done. I mapped out the tool-as-lambda ignorance-reduction plan and wrote it to: + + 1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md  + +It follows the  dd-3018003  structure (completed phases, ignorance-reduction questions, build-order implementation phases, and acceptance checklist) and records Phase 1 as completed via ADR-006. From 5ecb2bbd9ea7ed6ba1f9e5cf8124d045d52766ca Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Mon, 29 Jun 2026 12:50:20 -0400 Subject: [PATCH 04/13] GUTDODP --- .../20260628-prompts.md | 345 ++++++++++++++++++ 1 file changed, 345 insertions(+) create mode 100644 1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260628-prompts.md diff --git a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260628-prompts.md b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260628-prompts.md new file mode 100644 index 000000000..66cfa206e --- /dev/null +++ b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260628-prompts.md @@ -0,0 +1,345 @@ +### 14:35 Prompt + +I am now in Phase 03. + +In this phase we go through each open question and provide a **Resolution** section. + +Starting with 3.1. + +First sub-question: + +> 1. Whether zero-arg tools need a dedicated overload. + +Do the other languages even have zero-arg tools? + +### 14:37 Response + +I’ll check the SDKs for concrete zero-argument tool examples/usages so we can resolve 3.1 with evidence. + +Search "DefineTool\(|fromObject\(|@CopilotTool|define_tool|tool\(" (dotnet, java, go, python, nodejs, … Timed out +Search "ReturnsOk\(\)|void_method\(\)|string_method\(\)|no args|no-arg|without arguments|arguments: \{… Timed out +Search "\(\) =>|func\(\) |def \w+\(\):|\|\|" (dotnet\test, java\src\test, go, python, nodejs, rust) 9 lines found + +Read definetool.go 232 lines read +Read definetool_test.go 559 lines read +Read test_tools.py 429 lines read +Read tool.rs 1 line read + +Search "arguments:\s*\{\}|\(\)\s*->|fn\s+\w+\s*\(\s*\)|NoArgs|empty args|zero" in *.rs (rust) 8 lines found +Search "define_tool\(|DefineTool\(|Tool::|ToolDefinition::|with_tools|tools:\s*\[" in *.rs (rust) 15 files found + +Read manual_tool_resume.rs 152 lines read +Read tool.rs L1:260 (260 lines read) +Read tools.rs L1:220 (220 lines read) +Read tool.rs L260:460 (201 lines read) + +Search "define_tool::<\(\)|struct\s+\w+\s*\{\s*\}|arguments\s*:\s*json!\(\{\}\)|\"type\":\s*\"obj… 35 lines found +Search "z\.object\(\{\}\)|type:\s*'object'\s*,\s*properties:\s*\{\}|arguments:\s*\{\}|tool\(|defi… 73 lines found + +Read tools.py L1:260 (260 lines read) +Read CopilotToolTests.cs L130:170 (13 lines read) +Read CopilotToolTests.cs L14:55 (42 lines read) +Read tools.e2e.test.ts L88:125 (38 lines read) +Read tool_results.e2e.test.ts L165:200 (36 lines read) + +Yes—several SDKs already support zero-arg (or zero-parameter) tools. + +• .NET: true zero-arg delegate/method is used ( CopilotTool.DefineTool(ReturnsOk)  where  ReturnsOk()  has no args). + dotnet/test/Unit/CopilotToolTests.cs:39,141  +• Python: explicit zero-param handler test ( def test_tool() -> str ) with empty invocation arguments. + python/test_tools.py:107-127  +• Node.js/TypeScript: tools with empty parameter schema ( z.object({}) ) and no-arg handler ( handler: () => ... ). + nodejs/test/e2e/tool_results.e2e.test.ts:176-180  (also  nodejs/test/e2e/tools.e2e.test.ts:100-103 ) +• Go: zero-parameter tools are represented with an empty params type ( type Params struct{} ) and  {}  arguments. + go/definetool_test.go:100-114  + +So for 3.1, adding a dedicated zero-arg overload in Java would be consistent with cross-SDK usage patterns. + +### 14:37 Prompt + + +So for this: + +> 1. Whether zero-arg tools need a dedicated overload. + +How would this signature **not** be the dedicated overload: + +```java +ToolDefinition zeroArg = ToolDefinition.from( + "zero_args", + "Tool with zero args", + () -> { + return "OK"; + }); +``` + +I don't recall what a lambda with no args looks like, but such a lambda is the last argument to this overload. + +### 14:42 Response + +It would be the dedicated zero-arg overload. + +Your example is exactly the shape to add: + +ToolDefinition.from(String name, String description, ToolFn0 handler) + +(or  Supplier  if you want JDK functional types). + +For async parity, you’d likely also want: + +ToolDefinition.from(String name, String description, AsyncToolFn0 handler) +// where handler returns CompletableFuture + +So yes: a no-arg lambda as the last parameter is the clean, dedicated zero-arg API. + +### 14:42 Prompt + +Why would I **not** want a Supplier  for JDK functional types? Isn't this the most idiomatic way? + +### 14:44 Response + +You’re right— Supplier  is the most idiomatic Java choice for sync zero-arg handlers. + +You’d avoid it only if you want consistency with a custom tool-function family ( ToolFn0 ,  ToolFn1 , ...), or if you need behavior  Supplier  can’t express cleanly (for example checked exceptions, unless you wrap them, or a separate async SAM instead of  Supplier> ). + +So the practical recommendation is: + +• Use  Supplier  for sync zero-arg overload. +• Use a dedicated async SAM for async zero-arg overload (clearer than  Supplier> ). + +### 14:44 Prompt + +Why is this tools-as-lambda tending to bifurcate into a dual "one for async, another for non-async" pattern? + +### 14:45 Response + +Because Java’s type system doesn’t give a clean single-SAM shape that is both ergonomic and unambiguous for: + +•  R  +•  CompletableFuture  + +If you try one overload, you usually end up with awkward signatures ( Supplier ,  Function<..., ?> ) and runtime branching on return type, or ambiguous lambda inference. + +So APIs split into sync + async mostly for clarity and type inference: + +• sync: simple lambda, returns  R  +• async: simple lambda, returns  CompletableFuture  + +It’s less about architecture and more about making call sites readable and compile-time-safe. + +### 14:45 Prompt + +Pardon my ignorance, but I am unfamiliar with this usage of the acronym SAM. Please make me smart. + +### 14:47 Response + +SAM means Single Abstract Method. + +In Java, a lambda can be assigned to any interface that has exactly one abstract method (a “functional interface”), like  Supplier ,  Function ,  Runnable , etc. + +Example: + +Supplier s = () -> "OK"; // lambda matches get() + + Supplier  is SAM because it has one abstract method:  T get() . + +So when people say “SAM shape,” they mean “the functional-interface method signature the lambda must match.” + +### 14:48 Prompt + +You stated: + +> • Use  Supplier  for sync zero-arg overload. + +So this would be + +``` +ToolDefinition.from(String name, String description, Supplier handler) +``` + +> • Use a dedicated async SAM for async zero-arg overload (clearer than  Supplier> ). + +How about + +``` +ToolDefinition.fromAysnc(String name, String description, Supplier handler) +``` + +Would that meet the requirements in the zero-arg case? + +### 14:53 Response + +Almost—not as written. + +If  fromAsync  takes  Supplier , it’s still a sync signature unless you define  R  to be async. For zero-arg async, it should be one of: + +ToolDefinition.fromAsync(String name, String description, Supplier> handler) + +or (clearer API surface): + +ToolDefinition.fromAsync(String name, String description, AsyncSupplier handler) +// interface AsyncSupplier { CompletableFuture get(); } + +So  fromAsync(..., Supplier)  only works if callers pass  R = CompletableFuture , which is less explicit and easier to misuse. + + +### 14:53 Prompt + + +Where are you getting this `AsyncSupplier`? I see there is https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/function/Supplier.html but no corresponding async version. + +### 14:54 Response + + +You’re correct—there is no JDK  AsyncSupplier . + +I meant a custom interface you would define (if you choose that route), for example: + +@FunctionalInterface +public interface AsyncSupplier { + CompletableFuture get(); +} + +So your realistic choices are: + +1.  fromAsync(..., Supplier> handler)  using only JDK types, or +2.  fromAsync(..., AsyncSupplier handler)  with a tiny custom SAM for readability. + + +### 14:54 Prompt + + + +Who coined this SAM acronym? + +### 14:55 Response + + +There isn’t a single credited inventor. + +SAM (“Single Abstract Method”) was community/OpenJDK terminology that became mainstream during Java 8 lambda design work (JSR 335), especially in materials from the Oracle/OpenJDK lambda expert group led by Brian Goetz. + + +### 14:56 Prompt + +I have added `C:\Users\edburns\workareas\langchain4j-00-main`. This is the up-to-date LangChain4j. As you can see from ADR-005, our `CopilotTool` API is entirely based on their analogous tools API. + +What do they do for tools-as-lambda in the case of zero-arg tools? + +### 15:03 Response + +I’ll inspect LangChain4j’s current tools API to see how they handle zero-arg tool lambdas, then answer with the exact pattern they use. + +Search "ToolProvider|ToolExecutor|tool\(|@Tool|ToolSpecification|Function|Supplier|Runnable|execut… 7 files found +Search "zero|no args|no-arg|without arguments|parameters.*empty|\(\)\s*->" (~\workareas\langchain4… 6 lines found +Search "package dev\.langchain4j\.agent\.tool|class ToolSpecifications|class DefaultToolExecutor|… 13 files found + +Search "**/langchain4j-core/src/main/java/dev/langchain4j/agent/tool/*.java" in ~\workareas\langc… 10 files found +Search "**/langchain4j-core/src/test/java/dev/langchain4j/agent/tool/*.java" in ~\workareas\langch… 3 files found + +Search "toolProvider|toolExecutor|toolExecutors|ToolProvider|AiServices|tools\(|\.tools\(" (~\work… 8 files found +Search "\(\)\s*->\s*"|\(\)\s*->\s*\… rg: the literal "\n" is not allowed in a regex + +Consider enabling multiline mode with the --multiline flag (or -U for short). +When multiline mode is enabled, new line characters can be matched. + +Read tools.md L1:260 (260 lines read) +Read tools.md L260:620 (361 lines read) + +Search "ToolProvider\.builder|ToolProvider\.from|toolExecutor|toolExecutors|Map<.*ToolExecutor|Sup… 8 lines found +Search "Methods without parameters are supported as well" (~\workareas\langchain4j-00-main\docs\doc… 1 line found + +Read tools.md L1040:1165 (125 lines read) +Read ToolExecutor.java Path does not exist +Read DefaultToolExecutor.java 1 line read + +Search "**/ToolExecutor.java" in ~\workareas\langchain4j-00-main 1 file found +Search "**/DefaultToolExecutor.java" in ~\workareas\langchain4j-00-main 1 file found +Search "**/AiServiceTool.java" in ~\workareas\langchain4j-00-main 1 file found + +Read ToolExecutor.java 45 lines read +Read DefaultToolExecutor.java L1:260 (260 lines read) + +Search "parameterCount\(|getParameterCount\(|parameters\(\)\.isEmpty|arguments\(\).+empty|invoke\("… 1 line found +Search "without parameters|no parameters|\(\)|required\(\)|properties\(\)|empty" (~\workareas\lang… 7 lines found + +Read ToolSpecificationsTest.java L1:280 (280 lines read) + +Search (grep) +C:\Users\edburns\workareas\langchain4j-00-main\docs\docs\tutorials\tools.md:1089:ToolExecutor toolExecutor = (toolExecutionRequest, memoryId) -> { +C:\Users\edburns\workareas\langchain4j-00-main\docs\docs\tutorials\tools.md:1115:AiServiceTool tool = AiServiceTool.builder() +C:\Users\edburns\workareas\langchain4j-00-main\docs\docs\tutorials\tools.md:1133:AiServiceTool bookingTool = AiServiceTool.builder() +C:\Users\edburns\workareas\langchain4j-00-main\docs\docs\tutorials\tools.md:1139:AiServiceTool closeTool = AiServiceTool.builder() +C:\Users\edburns\workareas\langchain4j-00-main\docs\docs\tutorials\tools.md:1145:AiServiceTool weatherTool = AiServiceTool.builder() +C:\Users\edburns\workareas\langchain4j-00-main\langchain4j\src\main\java\dev\langchain4j\service\tool\ToolExecutionResult.java:42: private final Supplier resultTextSupplier; +C:\Users\edburns\workareas\langchain4j-00-main\langchain4j\src\main\java\dev\langchain4j\service\tool\ToolExecutionResult.java:194: private Supplier resultTextSupplier; +C:\Users\edburns\workareas\langchain4j-00-main\langchain4j\src\main\java\dev\langchain4j\service\tool\ToolExecutionResult.java:243: public Builder resultTextSupplier(Supplier resultTextSupplier) { +C:\Users\edburns\workareas\langchain4j-00-main\langchain4j\src\main\java\dev\langchain4j\service\tool\ToolProviderResult.java:107: tools.add(AiServiceTool.builder() +C:\Users\edburns\workareas\langchain4j-00-main\langchain4j\src\main\java\dev\langchain4j\service\tool\ToolService.java:248: result.add(AiServiceTool.builder() + +In current LangChain4j, they don’t have a dedicated  Supplier -style “tool-as-lambda” zero-arg API. + +What they do instead: + +• High-level ( @Tool ): zero-arg tools are just methods with no parameters. + docs/docs/tutorials/tools.md:377  (“Methods without parameters are supported as well.”) +• Programmatic/lambda path: the lambda is a  ToolExecutor , whose SAM is always + execute(ToolExecutionRequest request, Object memoryId)  (not zero-arg). + langchain4j/src/main/java/dev/langchain4j/service/tool/ToolExecutor.java:22  + +So for a zero-arg tool, LangChain4j models “zero args” in the tool schema/method signature, but the executor lambda itself still receives request/context and can ignore them. + +### 15:03 Prompt + +Are there any usage examples of tools-as-lambda in the LangChain4j codebase? + +### 15:04 Response + +Yes—there are many examples, but they’re in the programmatic  ToolExecutor  style (lambda takes request/context), not  Supplier -style zero-arg lambdas. + +Examples: + +•  docs/docs/tutorials/tools.md:1089  + ToolExecutor toolExecutor = (toolExecutionRequest, memoryId) -> { ... };  +•  docs/docs/tutorials/skills.md:344  + ToolExecutor validateOrderExecutor = (request, memoryId) -> { ... };  +•  langchain4j/src/test/java/dev/langchain4j/service/tool/ProgrammaticCreatedImmediateReturnToolTest.java:30  +•  langchain4j/src/test/java/dev/langchain4j/service/tool/ToolExecutorTest.java:295  +•  langchain4j-skills/src/test/java/dev/langchain4j/skills/SkillTest.java:271  + + So: yes, tools-as-lambda exists, but via  ToolExecutor  ( (ToolExecutionRequest, memoryId) -> ... ), not a zero-arg lambda API. + +### 15:10 Prompt + +Looking at tools.md, I see some rendering directives like + +``` +:::note +Not all LLMs support tools equally well. +The ability to understand, select, and correctly use tools depends heavily on the specific model and its capabilities. +Some models may not support tools at all, while others might require careful prompt engineering +or additional system instructions. +::: +``` + +This tells me there should be some website that hosts the rendered docs. + +Give me the URL to this `tools.md` in the official such website. + +### 15:11 Response + + +The rendered page is here: + +https://docs.langchain4j.dev/tutorials/tools + + +### 15:14 Prompt + +But `tools.md` states: + +> LangChain4j provides two levels of abstraction for using tools: +> - Low-level, using the `ChatModel` and `ToolSpecification` APIs +> - High-level, using [AI Services](/tutorials/ai-services) and `@Tool`-annotated Java methods + +I don't see a mention of lambda in tools.md. From 5af172edf88df6fed43a98bf19b0bcf2ac656a66 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Mon, 29 Jun 2026 17:37:14 -0400 Subject: [PATCH 05/13] On branch edburns/1810-java-tool-ergonomics-tool-as-lambda GOTDODP Your branch is up to date with 'upstream/edburns/1810-java-tool-ergonomics-tool-as-lambda'. Changes to be committed: (use "git restore --staged ..." to unstage) modified: 1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md modified: 1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260628-prompts.md new file: 1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md Signed-off-by: Ed Burns --- ...rance-reduction-for-implementation-plan.md | 473 +++- .../20260628-prompts.md | 1 + .../20260629-prompts.md | 2217 +++++++++++++++++ 3 files changed, 2689 insertions(+), 2 deletions(-) create mode 100644 1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md diff --git a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md index b7629cbb8..e215272d6 100644 --- a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md +++ b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md @@ -68,6 +68,377 @@ Open decisions: **Recommendation:** start with arity `0..3`, plus context-capable variants only if they remain concise. +**Resolutions:** + +**Arity cap for v1 (`1..3` vs `1..5`).**: + +Assume current annotation class `com.github.copilot.tool.Param` is renamed to `CopilotToolParam`. + +Then we define `Param` as + +```java +package com.github.copilot.tool; + +import java.util.Objects; + +/** + * Runtime parameter metadata for lambda-defined tools. + * Mirrors the fields of @CopilotToolParam. + */ +public final class Param { + + private final Class type; + private final String value; // description + private final String name; // parameter name override + private final boolean required; // required flag + private final String defaultValue; // default value as string + + private Param(Class type, String value, String name, boolean required, String defaultValue) { + this.type = Objects.requireNonNull(type, "type"); + this.value = value == null ? "" : value; + this.name = name == null ? "" : name; + this.required = required; + this.defaultValue = defaultValue == null ? "" : defaultValue; + } + + /** Minimal fluent entrypoint (required=true, defaultValue=""). */ + public static Param of(Class type, String name, String value) { + return new Param<>(type, value, name, true, ""); + } + + /** Full factory for parity with annotation fields. */ + public static Param of(Class type, String name, String value, boolean required, String defaultValue) { + return new Param<>(type, value, name, required, defaultValue); + } + + // Fluent modifiers + public Param name(String name) { + return new Param<>(this.type, this.value, name, this.required, this.defaultValue); + } + + public Param value(String value) { + return new Param<>(this.type, value, this.name, this.required, this.defaultValue); + } + + public Param required(boolean required) { + return new Param<>(this.type, this.value, this.name, required, this.defaultValue); + } + + public Param defaultValue(String defaultValue) { + return new Param<>(this.type, this.value, this.name, this.required, defaultValue); + } + + // Accessors intentionally match annotation element names + public Class type() { return type; } + public String value() { return value; } + public String name() { return name; } + public boolean required() { return required; } + public String defaultValue() { return defaultValue; } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Param other)) return false; + return required == other.required + && Objects.equals(type, other.type) + && Objects.equals(value, other.value) + && Objects.equals(name, other.name) + && Objects.equals(defaultValue, other.defaultValue); + } + + @Override + public int hashCode() { + return Objects.hash(type, value, name, required, defaultValue); + } +} +``` + +The answer to the arity question is shown in this sample. + +```java +package com.github.copilot.tool; + +import java.util.Objects; + +/** + * Runtime parameter metadata for lambda-defined tools. + * Mirrors the fields of @CopilotToolParam. + */ +public final class Param { + + private final Class type; + private final String description; + private final String name; + private final boolean required; + private final String defaultValue; + + private Param(Class type, String description, String name, boolean required, String defaultValue) { + this.type = Objects.requireNonNull(type, "type"); + this.description = requireNonBlank(description, "description"); + this.name = requireNonBlank(name, "name"); + this.defaultValue = defaultValue == null ? "" : defaultValue; + this.required = required; + + if (this.required && !this.defaultValue.isEmpty()) { + throw new IllegalArgumentException("required=true cannot be combined with a non-empty defaultValue"); + } + + validateDefaultValue(type, this.defaultValue); + } + + /** Minimal fluent entrypoint (required=true, no default). */ + public static Param of(Class type, String name, String description) { + return new Param<>(type, description, name, true, ""); + } + + /** Full factory for parity with annotation fields. */ + public static Param of( + Class type, + String name, + String description, + boolean required, + String defaultValue) { + return new Param<>(type, description, name, required, defaultValue); + } + + public Param name(String name) { + return new Param<>(this.type, this.description, name, this.required, this.defaultValue); + } + + public Param description(String description) { + return new Param<>(this.type, description, this.name, this.required, this.defaultValue); + } + + /** + * Alias for annotation parity. + */ + public Param value(String value) { + return description(value); + } + + public Param required(boolean required) { + return new Param<>(this.type, this.description, this.name, required, this.defaultValue); + } + + /** + * Setting a default makes the parameter optional. + */ + public Param defaultValue(String defaultValue) { + return new Param<>(this.type, this.description, this.name, false, defaultValue); + } + + public Class type() { + return type; + } + + /** + * Alias kept for annotation parity. + */ + public String value() { + return description; + } + + public String description() { + return description; + } + + public String name() { + return name; + } + + public boolean required() { + return required; + } + + public String defaultValue() { + return defaultValue; + } + + public boolean hasDefaultValue() { + return !defaultValue.isEmpty(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Param other)) { + return false; + } + return required == other.required + && Objects.equals(type, other.type) + && Objects.equals(description, other.description) + && Objects.equals(name, other.name) + && Objects.equals(defaultValue, other.defaultValue); + } + + @Override + public int hashCode() { + return Objects.hash(type, description, name, required, defaultValue); + } + + private static String requireNonBlank(String value, String fieldName) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException(fieldName + " must not be null or blank"); + } + return value; + } + + private static void validateDefaultValue(Class type, String defaultValue) { + if (defaultValue == null || defaultValue.isEmpty()) { + return; + } + + try { + if (type == String.class) { + return; + } + if (type == Integer.class || type == int.class) { + Integer.parseInt(defaultValue); + return; + } + if (type == Long.class || type == long.class) { + Long.parseLong(defaultValue); + return; + } + if (type == Double.class || type == double.class) { + Double.parseDouble(defaultValue); + return; + } + if (type == Float.class || type == float.class) { + Float.parseFloat(defaultValue); + return; + } + if (type == Short.class || type == short.class) { + Short.parseShort(defaultValue); + return; + } + if (type == Byte.class || type == byte.class) { + Byte.parseByte(defaultValue); + return; + } + if (type == Boolean.class || type == boolean.class) { + if (!"true".equalsIgnoreCase(defaultValue) && !"false".equalsIgnoreCase(defaultValue)) { + throw new IllegalArgumentException("must be 'true' or 'false'"); + } + return; + } + if (type.isEnum()) { + @SuppressWarnings({ "rawtypes", "unchecked" }) + Class enumType = (Class) type; + Enum.valueOf(enumType, defaultValue); + return; + } + } catch (RuntimeException ex) { + throw new IllegalArgumentException( + "defaultValue '" + defaultValue + "' is not valid for type " + type.getSimpleName(), ex); + } + + throw new IllegalArgumentException( + "defaultValue is not supported for type " + type.getName() + " without a custom coercion policy"); + } +} +``` + + +**Whether zero-arg tools need a dedicated overload.**: Yes. And it needs two. See the preceding answer. + +**Whether `ToolInvocation` context injection is included in v1.**: + +Yes, it must be. Here is the shape. + +```java +// ----------------------------- +// With ToolInvocation context +// ----------------------------- + +// 0 visible args + ToolInvocation, sync: +// Function +static ToolDefinition fromWithToolInvocation( + String name, + String description, + Function handler); + +// 0 visible args + ToolInvocation, async: +// Function> +static ToolDefinition fromAsyncWithToolInvocation( + String name, + String description, + Function> handler); + +// 1 visible arg + ToolInvocation, sync: +// BiFunction +static ToolDefinition fromWithToolInvocation( + String name, + String description, + Param p1, + BiFunction handler); + +// 1 visible arg + ToolInvocation, async: +// BiFunction> +static ToolDefinition fromAsyncWithToolInvocation( + String name, + String description, + Param p1, + BiFunction> handler); +``` + +Usage examples. + +```java +import java.util.concurrent.CompletableFuture; +import java.util.function.BiFunction; +import java.util.function.Function; + +// Assume this exists in the lambda-based API. +Param phaseParam = Param.of("phase", "Current phase"); + +// ------------------------------------------- +// fromWithToolInvocation(...) +// ------------------------------------------- + +// 0 visible args + ToolInvocation, sync: +// Function +ToolDefinition sessionInfoSync = ToolDefinition.fromWithToolInvocation( + "session_info", + "Return the current session id", + invocation -> "sessionId=" + invocation.getSessionId() +); + +// 1 visible arg + ToolInvocation, sync: +// BiFunction +ToolDefinition reportPhaseSync = ToolDefinition.fromWithToolInvocation( + "report_phase", + "Report the current phase along with invocation context", + phaseParam, + (phase, invocation) -> + "phase=" + phase + ", toolCallId=" + invocation.getToolCallId() +); + +// ------------------------------------------- +// fromAsyncWithToolInvocation(...) +// ------------------------------------------- + +// 0 visible args + ToolInvocation, async: +// Function> +ToolDefinition sessionInfoAsync = ToolDefinition.fromAsyncWithToolInvocation( + "session_info_async", + "Return the current session id asynchronously", + invocation -> CompletableFuture.completedFuture( + "sessionId=" + invocation.getSessionId() + ) +); + +// 1 visible arg + ToolInvocation, async: +// BiFunction> +ToolDefinition reportPhaseAsync = ToolDefinition.fromAsyncWithToolInvocation( + "report_phase_async", + "Report the current phase with invocation context asynchronously", + phaseParam, + (phase, invocation) -> CompletableFuture.completedFuture( + "phase=" + phase + ", toolCallId=" + invocation.getToolCallId() + ) +); +``` + + ### 3.2 — Functional interface set and type inference **Question:** What functional interfaces are needed for clean lambda syntax without casts? @@ -80,6 +451,10 @@ Unknowns: **Recommendation:** use distinct interfaces for sync and async handlers and keep overload count minimal to reduce ambiguity. +**Resolution:** + +The answers to 3.1 resolve this. But also please confirm that v1 uses only JDK functional interfaces and method-family naming to separate sync, async, and ToolInvocation-aware variants. + ### 3.3 — Parameter metadata DSL design **Question:** What is the smallest expressive parameter-definition API that preserves schema quality? @@ -93,11 +468,22 @@ Candidate concepts: Unknowns: 1. How defaults are represented and validated by type. -1. How optionality interacts with default values. -1. Whether descriptions are required by policy. +2. How optionality interacts with default values. +3. Whether descriptions are required by policy. **Recommendation:** align with `@Param` semantics from ADR-005 wherever possible. +**Resolution:** + +Use the above `Param` class. + +- Lambda API enforcement + - `Param.of(...)` and fluent mutators reject blank `name`/`description`. + - `Param.defaultValue(...)` validates the value against `Class`. + - `required=true` with non-empty `defaultValue` is rejected. + - Every `ToolDefinition.from` / `fromAsync` overload re-validates supplied `Param` objects before building the tool. + + ### 3.4 — Type-to-JSON-schema mapping for inline params **Question:** Which Java parameter types are supported in v1 for inline definitions? @@ -119,6 +505,15 @@ Unknowns: **Recommendation:** implement the subset already validated by existing ergonomic and low-level tests, then extend. +**Resolution:** + +For 3.4, I’d resolve it at this level: + +- tool-as-lambda supports exactly the same parameter-type surface already supported by the existing Java schema/tool pipeline, reused for lambda tools. +- This includes the minimal set you listed. +- No new schema semantics are invented for tool-as-lambda. +- If a type is not already supported by the current Java ergonomic/low-level tool path, it is out of scope for tool-as-lambda. + ### 3.5 — Invocation and coercion policy **Question:** How are JSON arguments coerced into typed lambda arguments? @@ -130,6 +525,8 @@ Options: **Recommendation:** reuse existing mapper policy for consistency and reduced risk. +**Resolution:** Use the existing `ObjectMapper`, eliminating DRY violations if any crop up. + ### 3.6 — Tool options and advanced flags **Question:** How do callers set `overridesBuiltInTool`, `skipPermission`, and `defer` on inline tools? @@ -141,6 +538,52 @@ Candidates: **Recommendation:** options object first, to avoid overload explosion. +**Resolution:** + +Use fluent immutable modifier methods on `ToolDefinition` rather than introducing a separate options object in v1. + +Because `ToolDefinition` is already an immutable record carrying `overridesBuiltInTool`, `skipPermission`, and `defer`, the lambda-based `from*` factories should return a `ToolDefinition` that callers may further customize with copy-style fluent methods. + +Example: + +```java +ToolDefinition tool = ToolDefinition.from( + "report_intent", + "Reports the agent's current intent", + Param.of(String.class, "intent", "The intent to report"), + intent -> "Reported intent: " + intent) + .overridesBuiltInTool(true) + .skipPermission(true) + .defer(ToolDefer.AUTO); +``` + +Equivalent context-aware example: + +``` +ToolDefinition tool = ToolDefinition.fromWithToolInvocation( + "report_phase", + "Reports the current phase with invocation context", + Param.of(String.class, "phase", "The current phase"), + (phase, invocation) -> "phase=" + phase + ", toolCallId=" + invocation.getToolCallId()) + .skipPermission(true) + .defer(ToolDefer.NEVER); +``` + +The modifier surface for v1 is: + +``` +ToolDefinition overridesBuiltInTool(boolean value); +ToolDefinition skipPermission(boolean value); +ToolDefinition defer(ToolDefer value); +``` + +Notes: + +- `defer` should use the existing `ToolDefer` enum, not a boolean. +- This keeps the API aligned with the existing `ToolDefinition` data model. +- This avoids introducing a separate options type solely for inline/lambda-defined tools. +- Existing low-level factories (`createOverride`, `createSkipPermission`, `createWithDefer`) may remain for compatibility, but the new lambda-based API should prefer the fluent style. + ### 3.7 — Error model and validation boundaries **Question:** Which invalid states should fail early? @@ -154,6 +597,21 @@ Must-validate cases: **Recommendation:** fail fast at tool construction with precise `IllegalArgumentException` messages. +**Resolution:** + +- Construction-time validation for lambda tools: + - all `ToolDefinition.from*` factories must validate before returning + - failures use `IllegalArgumentException` + - messages should identify the offending tool name and parameter name when possible +- `Param`-local validation: + - blank name/description + - `required=true` with default + - default incompatible with declared type +- Cross-parameter validation: + - duplicate parameter names + - unsupported schema/type mappings + + ### 3.8 — Binary compatibility and package placement **Question:** Where do new public types live without destabilizing existing API? @@ -165,6 +623,13 @@ Unknowns: **Recommendation:** place user-facing ergonomics in the package users already discover for tools, and keep internal helpers package-private. +**Resolution:** + +- new public helper types like `Param` belong in `com.github.copilot.tool` +- any necessary `module-info.java` export updates should expose only that user-facing package surface +- no extra public internal-helper types should leak just to support lambda tools + + ### 3.9 — E2E test scenario and snapshot reuse **Question:** Do we need a new replay snapshot? @@ -173,6 +638,10 @@ Because wire format should match existing tool definitions, we should attempt sn **Recommendation:** start with existing tool-definition snapshot; only add a new YAML if wire traffic differs. +**Resolution:** + +Yes. start with existing tool-definition snapshot; only add a new YAML if wire traffic differs. + --- ## Phase 4 — Implementation (build order) diff --git a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260628-prompts.md b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260628-prompts.md index 66cfa206e..714c15be1 100644 --- a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260628-prompts.md +++ b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260628-prompts.md @@ -343,3 +343,4 @@ But `tools.md` states: > - High-level, using [AI Services](/tutorials/ai-services) and `@Tool`-annotated Java methods I don't see a mention of lambda in tools.md. + diff --git a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md new file mode 100644 index 000000000..3eae837c5 --- /dev/null +++ b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md @@ -0,0 +1,2217 @@ +### 14:11 Prompt + +I am looking for evidence that Langchain4j has something analogous to the Copilot SDK for C#'s inline define tool, as shown in the `ReportIntent` usage in this sample: + +```C# + 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), + ], + }); +``` + +### 13:52 Response + +I don’t see a true LangChain4j analog to C#’s inline  CopilotTool.DefineTool(...) . + +Evidence: + +•  tools.md  says LangChain4j has only two layers: low-level  ToolSpecification  and high-level  @Tool  methods ( docs/docs/tutorials/tools.md:131 ). +• Zero-arg support is documented, but for  @Tool  methods: “Methods without parameters are supported as well” ( tools.md:377 ). +• Their lambda-style example is  ToolExecutor , whose shape is request-driven, not  Supplier / Function :  (toolExecutionRequest, memoryId) -> ...  ( tools.md:1089 ,  ToolExecutor.java:22 ). + +So: lambda support exists, but not a concise inline define-tool convenience API. + +### 13:54 Prompt + +I have added `micronaut-core-00` as a directory using `/add-dir`. + +Look at `copilot-sdk/java/docs/adr/adr-005-tool-definition.md` and see that it references Micronaut as precedent. + +Look at the Micronaut code and see if it has any lambda usage analogous to the define-tool convenience API. + +### 13:58 Response + +I’ll inspect ADR-005 for the Micronaut precedent it cites, then search  micronaut-core-00  for any analogous lambda-based convenience API. + +Read adr-005-tool-definition.md L1:220 (220 lines read) + +Search "**/*micronaut-core-00*" No matches found +Search rg: C:\Users\edburns\workareas\copilot-sdk\micronaut-core-00: IO error for operation on C:\Users\edburns\wo + +I didn’t find  micronaut-core-00  under the repo, so I’m locating the added directory in your workareas and then I’ll search it for any analogous lambda-based convenience API. + +Search "**/micronaut-core-00" in ~\workareas No matches found + +Search "ToolExecutor|toolSpecificationsFrom\(|@Executable|BeanDefinitionReference|Argument\.|MethodR… 9 files found +Search "annotation processor|compile-time|ExecutableMethod|MethodReference|IntroductionAdvice|Functi… 7 lines found + +I found the Micronaut checkout. Now I’m narrowing to docs and public API examples that would show a user-facing lambda convenience, if one exists. + +Search "lambda|method reference|::|Supplier<|Function<|Consumer<" in **/*.{md,adoc,java,kt,groovy} (… 7 lines found +Search "ExecutableMethod|BeanIntrospection|BeanDefinitionReference|@Executable|annotation processor|… 6 lines found +Search "RouteBuilder|BeanDefinitionRegistry|registerSingleton|BeanContext\.run|ApplicationContext\.b… 7 lines found +It is in C:\Users\edburns\workareas\micronaut-core-00 +Thanks — that matches what I’ve been searching. I’m pulling specific Micronaut evidence now to separate “compile-time annotated metadata” from any user-facing lambda convenience API. + +Search "class BeanDefinitionInjectProcessor|Annotation metadata is computed|compile-time|BeanDefi… No matches found +Search "interface ExecutableMethod|@Executable|ExecutableMethodProcessor|MethodExecutionHandle" in *… 7 lines found +Search "Supplier<|Function<|Consumer<" in **/src/main/java/**/*.java (~\workareas\micronaut-core-00) 7 lines found + +I found one promising area: Micronaut’s  function  module recognizes beans that implement  Supplier ,  Consumer , or  Function . I’m checking whether that is just bean discovery or a true inline lambda convenience. + +Read LocalFunctionRegistry.java L1:180 (115 lines read) + +Search "implements Supplier|implements Function|implements Consumer|class .*Function|@FunctionBea… No matches found +Search "ExecutableMethod get() { return () -> "myvalue"; }  +•  Function round() { return Math::round; }  +•  BiFunction fullname() { return (s, s2) -> ...; }  +—  function-web\src\test\java\io\micronaut\function\web\TestFunctionFactory.java:35-72  + +The metadata lives on  @FunctionBean , not the lambda ( function\src\main\java\io\micronaut\function\FunctionBean.java:38-65 ), and Micronaut discovers them as  ExecutableMethod s ( function\src\main\java\io\micronaut\function\LocalFunctionRegistry.java:78-114 ). + +So: analogous spirit, but not a direct  defineTool(lambda)  call-site convenience. + +### 14:00 Prompt + +Show me a code snippet so I can understand the usage patterns for `FunctionBean` in Micronaut. + + +### 14:03 Response + +Here are the main Micronaut  @FunctionBean  usage patterns: + +// 1) Class-based function bean +@FunctionBean("round") +public class RoundFunction implements Function { + @Override + public Integer apply(Float value) { + return Math.round(value); + } +} + +// 2) Factory method returning a lambda or method reference +@Factory +class MyFunctions { + + @FunctionBean("supplier") + Supplier get() { + return () -> "myvalue"; + } + + @FunctionBean("round") + Function round() { + return Math::round; + } +} + +So the annotation carries metadata; the actual behavior can be a class, lambda, or method reference. + +### 14:03 Prompt + +And how are these tools declared so the LLM can invoke them? + +### 14:04 Response + +They aren’t declared to an LLM directly. + +In Micronaut,  @FunctionBean("name")  declares an application function, and Micronaut discovers it via its function registry/runtime: + +@FunctionBean("round") +public class RoundFunction implements Function { + public Integer apply(Float value) { return Math.round(value); } +} + +or + +@Factory +class MyFunctions { + @FunctionBean("supplier") + Supplier get() { return () -> "myvalue"; } +} + +Micronaut then exposes/discovers these as functions ( LocalFunctionRegistry.findSupplier/findFunction/... ). + +So the LLM-style equivalent is only indirect: + +• Micronaut: register function bean +• Copilot SDK: build  ToolDefinition  and send schema/handler to the model session + +### 14:06 Prompt + +I looked also at MapStruct and Dagger 2 for precedent regarding a lambda approach, but don't see any. Can you check and see if I'm missing something? + +### 14:08 Response + +I’ll check the local MapStruct and Dagger 2 checkouts for any user-facing lambda-based registration/convenience API analogous to  defineTool(...) . + +Search "**/{mapstruct*,*mapstruct*,dagger*}" in ~\workareas No matches found + +Search "Supplier<|Function<|Consumer<|lambda|method reference|::|Mapper\.builder|Dagger|@Mapper|@Compone… Timed out +Search "annotation processor|compile-time|generated implementation|factory|builder" in **/*.{md,adoc,jav… Timed out +I don't have them checked out. +You’re likely not missing anything, but I’ll verify against the upstream repos/docs rather than local checkouts. + +Search code MCP server 'github-mcp-server': failed to search code with query 'repo:mapstruct/mapstruct (Supplier O +Search code MCP server 'github-mcp-server': failed to search code with query 'repo:google/dagger (Supplier OR Func + +Fetching web content https://mapstruct.org… Contents of https://mapstruct.org/documentation/stable/reference/html/: +Fetching web content https://dagger.dev/dev-guide/ · url: "https://dagg… Contents of https://dagger.dev/dev-guide/: + +I’m narrowing the GitHub search queries; the first ones were too fancy for the API. + +Search code (MCP: github-mcp-serv… {"total_count":7,"incomplete_results":false,"items":[{"name":"AbstractBaseBui... +Search code (MCP: github-mcp-serv… {"total_count":5,"incomplete_results":false,"items":[{"name":"Type.java","pat... +Search code (MCP: github-mcp-server) · repo:google/dagger … {"total_count":0,"incomplete_results":false,"items":[]} +Search code (MCP: github-mcp-server) · repo:google/dagger … {"total_count":0,"incomplete_results":false,"items":[]} + +You’re not missing much. + +I don’t see a useful lambda precedent in either: + +• MapStruct: user API is  @Mapper  plus mapping methods/annotations. The  Supplier / Function  hits are internal processor code, not user-facing registration. +• Dagger 2: user API is  @Inject ,  @Module ,  @Provides ,  @Binds , and generated  Dagger...  components. I found no  Supplier / Function -style convenience API in the upstream repo search. + +So as precedents for compile-time metadata generation, both are strong. +As precedents for a user-facing lambda convenience API, they’re weak-to-none. + +Micronaut’s  @FunctionBean  factory-method pattern is the closer analog. + +### 14:09 Prompt + +Coming back to `3.1 — Public API shape` in `copilot-sdk/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md`. + +We need to get to resolution for + +> 1. Whether zero-arg tools need a dedicated overload. + +I believe we are converging on the answer being **yes**, **AND**, what's more, we need two, for sync and async. + +sync + +`ToolDefinition.from(String name, String description, Supplier handler)` + +async + +`ToolDefinition.fromAsync(String name, String description, Supplier> handler)` + +Review this design, just for the `zero-arg-tools` case. Compare it with the C# API. + +### 14:16 Response + +For zero-arg tools only, I think this is a good resolution. + +ToolDefinition.from(String name, String description, Supplier handler) +ToolDefinition.fromAsync(String name, String description, Supplier> handler) + +Why it works: + +•  Supplier  is the idiomatic Java sync zero-arg SAM. +•  Supplier>  is precise and avoids ambiguity. +• The split mirrors Java reality better than C#. + +Compared with C#: + +• C# gets one  DefineTool(...)  because delegates + reflection unify sync/async/arity more naturally. +• Java does not have that same ergonomic unification, so two overloads are cleaner. + +So: yes, dedicated overloads; yes, one sync and one async. + +### 14:18 Prompt + +Now we come to + +> 1. Arity cap for v1 (`1..3` vs `1..5`). + +What does this even mean? + +### 14:20 Response + +It means: + +How many distinct lambda overloads do we ship in v1 before we stop? + +Because Java has no variadic typed function family in the JDK, each arity needs its own SAM/overload family. + +Example: + +• arity 1:  Function  +• arity 2: custom  BiToolFunction  or similar +• arity 3: custom tri-arg SAM +• arity 4+: more custom SAMs + +So  1..3  means support 1-, 2-, and 3-argument tools in v1. + 1..5  means go up through 5 arguments. + +It’s really a scope/control question: where do we stop adding overload families initially? + +### 14:20 Prompt + +Let's go back to C#. How does our C# API handle the arity? + +### 14:21 Response + +C# does not impose an explicit arity cap in our API. + +It takes a single  Delegate : + +public static AIFunction DefineTool( + Delegate method, + CopilotToolOptions? toolOptions = null, + AIFunctionFactoryOptions? factoryOptions = null) + +and then hands that delegate to: + +AIFunctionFactory.Create(method, factoryOptions) + +See  dotnet/src/CopilotTool.cs:36-49 . + +So arity is handled by delegate reflection, not by overload families: + +• zero-arg method:  ReturnsOk  +• one-arg lambda:  (string value) => value  + +C# can therefore accept many arities through one entry point. Java can’t match that cleanly with JDK SAMs, hence the arity-cap question for Java. + +### 14:21 Prompt + +So this is just a core language difference between C# and Java? + +### 14:22 Response + +Yes — mostly a core language/runtime difference. + +C# gives you: + +• a universal  Delegate  abstraction +• rich reflection over delegate signatures +• natural sync/async and multi-arity expression through  Func<> / Action<>  + +Java gives you: + +• individual SAM types, not one universal callable type +• no built-in  Function3 ,  Function4 , etc. +• weaker lambda introspection for metadata/signature recovery + +So C# can unify arities behind one  DefineTool(Delegate ...) . +Java usually has to choose between: + +• explicit overload/SAM families, or +• a lower-level reflective/invocation-based API. + +### 14:23 Prompt + +Ok, so let's go with 1..3. Show me what the `from` overloads would look like for that. And do we need sync and async versions? + + +### 14:25 Response + +Yes, I’d do both sync and async. + +// 0 args +static ToolDefinition from( + String name, String description, Supplier handler); + +static ToolDefinition fromAsync( + String name, String description, Supplier> handler); + +// 1 arg +static ToolDefinition from( + String name, String description, Param p1, Function handler); + +static ToolDefinition fromAsync( + String name, String description, Param p1, AsyncFunction1 handler); + +// 2 args +static ToolDefinition from( + String name, String description, Param p1, Param p2, BiFunction handler); + +static ToolDefinition fromAsync( + String name, String description, Param p1, Param p2, AsyncFunction2 handler); + +// 3 args +static ToolDefinition from( + String name, String description, Param p1, Param p2, Param p3, TriFunction handler); + +static ToolDefinition fromAsync( + String name, String description, Param p1, Param p2, Param p3, AsyncFunction3 handler); + + TriFunction  and  AsyncFunction{1,2,3}  would be custom SAMs. + +### 14:25 Prompt + +This is starting to smell a bit off. I don't like inventing new types for fundamental things like function calling. I want to stick with JDK classes. + +If we limited ourselves to 1..2-arity, and used `Function` and `BiFunction` for the sync case for 1 and 2 arg, then is there something analogous to what we did with the zero-arg async, but for the 1 and 2 arg async case? + +``` +ToolDefinition.fromAsync(String name, String description, Supplier> handler) +``` + +### 15:14 Response + +Yes. If you want JDK-only types and cap at 1..2 arity, the direct async analog is: + +// 0 args +static ToolDefinition fromAsync( + String name, + String description, + Supplier> handler); + +// 1 arg +static ToolDefinition fromAsync( + String name, + String description, + Param p1, + Function> handler); + +// 2 args +static ToolDefinition fromAsync( + String name, + String description, + Param p1, + Param p2, + BiFunction> handler); + +So yes — same pattern as zero-arg async: the handler returns  CompletableFuture , and you don’t need any custom SAM types. + +### 15:14 Prompt + +Please show me usage examples with lambdas for the sync and async 0..2 arg variants of `ToolDefinition.from*`. + +### 15:15 Response + +``` +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + +// Assume Param.of(name, description, type) exists in your API. +Param cityParam = Param.of("city", "City to query", String.class); +Param qParam = Param.of("query", "Search query", String.class); +Param limitParam = Param.of("limit", "Max results", Integer.class); + +// ----------------------------- +// Sync variants (from) +// ----------------------------- + +ToolDefinition pingSync = ToolDefinition.from( + "ping", + "Health check", + () -> "ok" +); + +ToolDefinition weatherSync = ToolDefinition.from( + "weather", + "Get weather by city", + cityParam, + city -> "Sunny in " + city +); + +ToolDefinition searchSync = ToolDefinition.from( + "search", + "Search with limit", + qParam, + limitParam, + (query, limit) -> "Top " + limit + " results for " + query +); + +// ----------------------------- +// Async variants (fromAsync) +// ----------------------------- + +ToolDefinition pingAsync = ToolDefinition.fromAsync( + "ping_async", + "Async health check", + () -> CompletableFuture.completedFuture("ok") +); + +ToolDefinition weatherAsync = ToolDefinition.fromAsync( + "weather_async", + "Async weather by city", + cityParam, + city -> CompletableFuture.supplyAsync(() -> "Sunny in " + city) +); + +ToolDefinition searchAsync = ToolDefinition.fromAsync( + "search_async", + "Async search with limit", + qParam, + limitParam, + (query, limit) -> CompletableFuture.supplyAsync( + () -> "Top " + limit + " results for " + query + ) +); + +List tools = List.of( + pingSync, weatherSync, searchSync, + pingAsync, weatherAsync, searchAsync +); +``` + +### 15:15 Prompt + +Please pepper in comments for the 0 arg, 1 arg, 2 arg, for ease of reading. + +### 15:16 Response + +``` +import java.util.List; +import java.util.concurrent.CompletableFuture; + +// Assume Param.of(name, description, type) exists in your API. +Param cityParam = Param.of("city", "City to query", String.class); +Param qParam = Param.of("query", "Search query", String.class); +Param limitParam = Param.of("limit", "Max results", Integer.class); + +// ----------------------------- +// Sync variants (from) +// ----------------------------- + +// 0-arg sync tool: Supplier +ToolDefinition pingSync = ToolDefinition.from( + "ping", + "Health check", + () -> "ok" +); + +// 1-arg sync tool: Function +ToolDefinition weatherSync = ToolDefinition.from( + "weather", + "Get weather by city", + cityParam, + city -> "Sunny in " + city +); + +// 2-arg sync tool: BiFunction +ToolDefinition searchSync = ToolDefinition.from( + "search", + "Search with limit", + qParam, + limitParam, + (query, limit) -> "Top " + limit + " results for " + query +); + +// ----------------------------- +// Async variants (fromAsync) +// ----------------------------- + +// 0-arg async tool: Supplier> +ToolDefinition pingAsync = ToolDefinition.fromAsync( + "ping_async", + "Async health check", + () -> CompletableFuture.completedFuture("ok") +); + +// 1-arg async tool: Function> +ToolDefinition weatherAsync = ToolDefinition.fromAsync( + "weather_async", + "Async weather by city", + cityParam, + city -> CompletableFuture.supplyAsync(() -> "Sunny in " + city) +); + +// 2-arg async tool: BiFunction> +ToolDefinition searchAsync = ToolDefinition.fromAsync( + "search_async", + "Async search with limit", + qParam, + limitParam, + (query, limit) -> CompletableFuture.supplyAsync( + () -> "Top " + limit + " results for " + query + ) +); + +List tools = List.of( + pingSync, weatherSync, searchSync, + pingAsync, weatherAsync, searchAsync +); +``` + +### 15:18 Prompt + +This is starting to look better. Here is our existing `Param`; `copilot-sdk/java/src/main/java/com/github/copilot/tool/Param.java`. Is this amenable to adding the `of` method? + +### 15:21 Response + +I’ll inspect  Param.java  and tell you whether adding  Param.of(...)  fits cleanly with its current design. + +Read Param.java 49 lines read + +Not directly.  Param  is an annotation type ( public @interface Param ), so it isn’t a good place for a fluent factory like  Param.of(...) . + +For the lambda API, the clean path is to introduce a separate metadata type (for example  ToolParam  /  ParamSpec ) with  of(...) , and leave  @Param  as-is for annotation-based tools. + +### 15:21 Prompt + +What if we pushed the annotation contract down a level and essentially renamed the current `Param` annotation to `CopilotParam` and made that `CopilotParam` extend the non-annotation `Param`. Could we then use `Param` for both lambda and annotation cases? + + +Now, is there any way we can minimize it even further leveraging type inference? + +### 15:25 Response + +No — that specific design won’t work in Java. + + @interface  types cannot extend a class (or declare  extends  at all), so an annotation like  @CopilotParam  cannot inherit from a non-annotation  Param  type. Annotations are a separate language construct with fixed rules. + +The practical pattern is: + +• keep annotation metadata in  @Param  (or  @CopilotParam  if you rename), and +• add a separate runtime metadata type for lambda tools (for example  ParamSpec  with  ParamSpec.of(...) ), +• then normalize both into one internal model. + +### 15:28 Prompt + +Let's assume I rename `Param` to `CopilotToolParam`. + +Show me the `Param` that fits your usage with the fluent api. Make it have parity with the fields in the current `Param`. + +### 15:30 Response + +```java +package com.github.copilot.tool; + +import java.util.Objects; + +/** + * Runtime parameter metadata for lambda-defined tools. + * Mirrors the fields of @CopilotToolParam. + */ +public final class Param { + + private final String value; // description + private final String name; // parameter name override + private final boolean required; // required flag + private final String defaultValue; // default value as string + + private Param(String value, String name, boolean required, String defaultValue) { + this.value = value == null ? "" : value; + this.name = name == null ? "" : name; + this.required = required; + this.defaultValue = defaultValue == null ? "" : defaultValue; + } + + /** Minimal fluent entrypoint (required=true, defaultValue=""). */ + public static Param of(String name, String value) { + return new Param(value, name, true, ""); + } + + /** Full factory for parity with annotation fields. */ + public static Param of(String name, String value, boolean required, String defaultValue) { + return new Param(value, name, required, defaultValue); + } + + // Fluent modifiers + public Param name(String name) { + return new Param(this.value, name, this.required, this.defaultValue); + } + + public Param value(String value) { + return new Param(value, this.name, this.required, this.defaultValue); + } + + public Param required(boolean required) { + return new Param(this.value, this.name, required, this.defaultValue); + } + + public Param defaultValue(String defaultValue) { + return new Param(this.value, this.name, this.required, defaultValue); + } + + // Accessors intentionally match annotation element names + public String value() { return value; } + public String name() { return name; } + public boolean required() { return required; } + public String defaultValue() { return defaultValue; } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Param other)) return false; + return required == other.required + && Objects.equals(value, other.value) + && Objects.equals(name, other.name) + && Objects.equals(defaultValue, other.defaultValue); + } + + @Override + public int hashCode() { + return Objects.hash(value, name, required, defaultValue); + } +} +``` + +This gives you parity with the current annotation fields while supporting fluent construction for lambda-based APIs. + +### 15:47 Prompt + +Now we come to question 3.1.3: + +> 1. Whether `ToolInvocation` context injection is included in v1. + +What do you mean by this question? I know what you mean by `v1`. + + +### 15:48 Response + +It means: + +Should the first release of tool-as-lambda support a handler that receives not just model-declared arguments, but also the SDK’s invocation context object? + +For example, beyond: + +city -> ... + +or + +(query, limit) -> ... + +should v1 also support shapes conceptually like: + +(city, invocation) -> ... + +where  invocation  is a  ToolInvocation  and is not part of the tool schema? + +This mirrors the existing C# idea:  ToolInvocation  can be injected as hidden runtime context, separate from LLM-visible parameters. + +### 15:48 Prompt + +First, let's circle back to the annotation based API. Do we handle `ToolInvocation` in that variant? + +### 15:50 Response + +package com.github.copilot.tool; + +import java.util.Objects; + +/** + * Runtime parameter metadata for lambda-defined tools. + * Mirrors the fields of @CopilotToolParam. + */ +public final class Param { + + private final String value; // description + private final String name; // parameter name override + private final boolean required; // required flag + private final String defaultValue; // default value as string + + private Param(String value, String name, boolean required, String defaultValue) { + this.value = value == null ? "" : value; + this.name = name == null ? "" : name; + this.required = required; + this.defaultValue = defaultValue == null ? "" : defaultValue; + } + + /** Minimal fluent entrypoint (required=true, defaultValue=""). */ + public static Param of(String name, String value) { + return new Param(value, name, true, ""); + } + + /** Full factory for parity with annotation fields. */ + public static Param of(String name, String value, boolean required, String defaultValue) { + return new Param(value, name, required, defaultValue); + } + + // Fluent modifiers + public Param name(String name) { + return new Param(this.value, name, this.required, this.defaultValue); + } + + public Param value(String value) { + return new Param(value, this.name, this.required, this.defaultValue); + } + + public Param required(boolean required) { + return new Param(this.value, this.name, required, this.defaultValue); + } + + public Param defaultValue(String defaultValue) { + return new Param(this.value, this.name, this.required, defaultValue); + } + + // Accessors intentionally match annotation element names + public String value() { return value; } + public String name() { return name; } + public boolean required() { return required; } + public String defaultValue() { return defaultValue; } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Param other)) return false; + return required == other.required + && Objects.equals(value, other.value) + && Objects.equals(name, other.name) + && Objects.equals(defaultValue, other.defaultValue); + } + + @Override + public int hashCode() { + return Objects.hash(value, name, required, defaultValue); + } +} + +This gives you parity with the current annotation fields while supporting fluent construction for lambda-based APIs. + +### 15:50 Prompt + +Is this a gap in the annotation based API? + +### 15:51 Response + +Yes, I would call it a capability gap, but not necessarily a design bug. + +It’s a gap because: + +• the low-level API has  ToolInvocation  +• C# supports hidden invocation injection +• some advanced tools may want session/tool-call context + +It’s not a bug because: + +• the current Java annotation API was clearly designed around “LLM-visible params only” +• many tools don’t need invocation context + +So for planning purposes, I’d phrase 3.1.3 as: + +Should tool-as-lambda v1 stay aligned with the current annotation API and omit  ToolInvocation  injection, or intentionally extend beyond it? + +### 15:52 Prompt + +Because we have identified a gap in the annotation based API, I want to explore this a bit more and file an issue. + +Could we define a JSR-330 compatible way so one could simply + +``` +@Inject +ToolInvocation +``` + +What would that take? + +### 15:54 Response + +Not cleanly with plain JSR-330 alone. + + @Inject  is for DI on managed beans/constructors/fields, not for arbitrary per-call method parameters on a non-container invocation path. + +To make  @Inject ToolInvocation  work, you’d need: + +• tool instances to be container-managed beans +• a custom scope like  @ToolInvocationScoped  +• infrastructure to bind the current  ToolInvocation  before dispatch +• likely  Provider  or a scoped proxy +• generator/dispatcher integration so invocation scope opens/closes per tool call + +So: possible, but it’s really “JSR-330 plus custom runtime/container support,” not a simple annotation-only addition. + +### 15:54 Prompt + +Let's assume the whole system, including the `CopilotSession` is running in a Jakarta EE container. + +What then? + +### 15:56 Response + +Then it becomes feasible, but still not as an arbitrary method-parameter feature. + +What would work in Jakarta EE/CDI: + +• make tool classes CDI beans +• add a custom scope, e.g.  @ToolInvocationScoped  +• add a CDI  Context  +  @Produces ToolInvocation  +• have the dispatcher activate/deactivate that scope around each tool call + +Then a tool bean could do: + +@Inject +ToolInvocation invocation; + +or safer: + +@Inject +Instance invocation; + +Important caveat: + +• CDI does not standardly inject arbitrary business-method parameters +• async  CompletableFuture  paths need context propagation across threads + +### 15:56 Prompt + +So what can we do to provide `ToolInvocation` to tools in the annotation case? + +### 15:57 Response + +Three practical options: + +1. Hidden method parameter injection +Allow: + +@CopilotTool("...") +String doThing(@Param("city") String city, ToolInvocation invocation) + + ToolInvocation  is excluded from schema and injected by generated code. This is the closest analog to C#. + +2. Bean/field context accessor +Expose  ToolInvocation.current()  or injectable provider. Simpler signatures, weaker explicitness. +3. CDI-only integration +If container-managed, inject it from a custom scope. + +For the current Java API, I’d recommend #1: annotation processor special-cases  ToolInvocation  as a hidden non-schema parameter. + +### 15:58 Prompt + +So for option 1, would that mean users would get the `ToolInvocation` whether they wanted it or not? + +### 15:59 Response + +No. It would be opt-in by signature. + +Only methods that declare it would receive it: + +@CopilotTool("...") +String doThing(@Param("city") String city, ToolInvocation invocation) + +Methods that don’t declare it would behave exactly as today: + +@CopilotTool("...") +String doThing(@Param("city") String city) + +So the processor/dispatcher would merely recognize  ToolInvocation  specially: + +• exclude it from schema +• do not read it from LLM args +• pass the current invocation object at call time + +### 15:59 Prompt + +Use `gh` to file a new issue of type **Task** to get an agent to do your "Option 1". Make it a child issue of #1809. + +Look at Epic #1682 and its child issues for the standard of verbosity and concreteness for issue descriptions. + +### 16:05 Response + +I’ll inspect Epic #1682 and its child issues for style, then create the new Task issue under #1809 with a concrete description. + +Shell View epic 1682 details 2 lines… +gh issue view 1682 --repo edburns/copilot-sdk --json number,title,body,url,labels,assignees,state +Shell View parent issue 1809 2 lines… +gh issue view 1809 --repo edburns/copilot-sdk --json number,title,body,url,labels,assignees,state +Shell Find issues related to epic 1682 2 lines… +gh issue list --repo edburns/copilot-sdk --limit 200 --state all --json number,title,body,labels | ConvertFrom-Jso… + + gh issue  is failing because this repository has issues disabled. I’m checking whether #1682/#1809 are actually project items or PRs so I can use the right GitHub API path. + +Shell Check if 1682 is a PR 2 lines… +gh pr view 1682 --repo edburns/copilot-sdk --json number,title,body,url,state +Shell Check if 1809 is a PR 2 lines… +gh pr view 1809 --repo edburns/copilot-sdk --json number,title,body,url,state +Shell Check repo issue settings 2 lines… +gh repo view edburns/copilot-sdk --json nameWithOwner,hasIssuesEnabled,description,url + +I confirmed  edburns/copilot-sdk  has issues disabled, and #1682/#1809 are not PRs here. I’m checking whether your checkout points at an upstream repo where those issues live. + +Shell Show git remotes 5 lines… +git -C C:\Users\edburns\workareas\copilot-sdk remote -v +Shell Check gh repo fork help 34 lines… +gh repo fork --help | Select-Object -First 40 +Shell Check gh auth viewer 2 lines… +gh api graphql -f query='query { viewer { login } }' + +Found it: your fork disables issues, but  upstream  is  github/copilot-sdk . I’m using that repo for #1682/#1809 and for creating the new Task issue. + +Shell View upstream epic 1682 2 lines… +gh issue view 1682 --repo github/copilot-sdk --json number,title,body,url,labels,state +Shell View upstream issue 1809 2 lines… +gh issue view 1809 --repo github/copilot-sdk --json number,title,body,url,labels,state +Shell List child issues of 1682 2 lines… +gh issue list --repo github/copilot-sdk --limit 200 --state all --search "parent:1682" --json number,title,body,la… + +I’ve got the style baseline from #1682 and a concrete child issue (#1794). Next I’m checking the upstream repo’s issue-type and sub-issue GraphQL support so I can create this correctly as a Task under #1809. + +Shell Get repo and parent issue ids 2 lines… +gh api graphql -f query='query { repository(owner:"github", name:"copilot-sdk") { id issue(number:1809) { id numbe… +Shell Check issue type field support 2 lines… +gh api graphql -f query='query { repository(owner:"github", name:"copilot-sdk") { issue(number:1794) { number titl… +Shell Probe GraphQL access baseline 4 lines… +gh api graphql -f query='query { organization(login:"github") { projectV2(number:1) { id title } } }' + +I’ve confirmed the upstream repo uses GitHub issue types and that Task has an issueType ID. I’m introspecting the GraphQL mutation inputs so I can set both the type and the parent link in one pass if possible. + +Shell Inspect CreateIssueInput fields 2 lines… +gh api graphql -f query='query { __type(name:"CreateIssueInput") { inputFields { name type { kind name ofType { ki… +Shell List GraphQL mutations 2 lines… +gh api graphql -f query='query { __schema { mutationType { fields { name } } } }' +Shell Inspect AddSubIssueInput 2 lines… +gh api graphql -f query='query { __type(name:"AddSubIssueInput") { inputFields { name type { kind name ofType { ki… + +I’ve got enough to create it correctly via GraphQL: Task type and parent can both be set at creation time. I’m adding the issue with the same concrete style as the #1682 follow-up tasks. + +Shell Get label ids 2 lines… +gh api graphql -f query='query { repository(owner:"github", name:"copilot-sdk") { enhancement: label(name:"enhance… +Shell Create upstream task issue as child of 1809 213 lines… +$body = @' ## Overview Add support for hidden `ToolInvocation` parameter injection in the annotation-based Java to… +Yes, this should be on the upstream. +Understood. I already targeted  github/copilot-sdk ; the first GraphQL create just failed on argument passing. I’m switching to a more reliable path: create the issue with  gh issue create , then set Task type and parent linkage via GraphQL. + +Shell Inspect issue type update input 2 lines… +gh api graphql -f query='query { __type(name:"UpdateIssueIssueTypeInput") { inputFields { name type { kind name of… +Shell Create upstream issue with body file 187 lines… +$body = @' ## Overview Add support for hidden `ToolInvocation` parameter injection in the annotation-based Java to… + +The issue is created. I’m doing the last two bits now: set its issue type to Task and attach it as a child of #1809. + +Shell Get new issue id and type 2 lines… +gh api graphql -f query='query { repository(owner:"github", name:"copilot-sdk") { issue(number:1831) { id number t… +Shell Set issue type to Task 5 lines… +$mutation = @' mutation($issueId: ID!, $issueTypeId: ID!) { updateIssueIssueType(input: { issueId: $issueId, issue… +Shell Attach sub-issue to 1809 7 lines… +$mutation = @' mutation($issueId: ID!, $subIssueUrl: String!) { addSubIssue(input: { issueId: $issueId, subIssueUr… + +Parent linkage is done. I had a stale issue ID for the type update; retrying once with the actual ID from the created issue. + +Shell Set created issue type to Task 5 lines… +$mutation = @' mutation($issueId: ID!, $issueTypeId: ID!) { updateIssueIssueType(input: { issueId: $issueId, issue… + +Done. + +Created upstream Task issue: + +•  github/copilot-sdk#1831  — child of  #1809  + +It includes: + +• overview/motivation +• concrete implementation spec +• files to modify +• validation rules +• gating tests +• build requirement ( mvn verify ) + +Link: + +• https://github.com/github/copilot-sdk/issues/1831 + +### 16:06 Prompt + +Now we come back to 3.1.3. + +You said: + +------- + +So for planning purposes, I’d phrase 3.1.3 as: + +Should tool-as-lambda v1 stay aligned with the current annotation API and omit  ToolInvocation  injection, or intentionally extend beyond it? + + +------ + +Given that I have just assigned Copilot to your newly created #1831, we will soon have the `ToolInvocation` facility in the annotation API. + +So, yes, let's explore how to get it in the lambda API. + +Given the existing `from` and `fromAsync` overloads, how would that look? + +### 16:10 Response + +Given your JDK-only constraint, I’d model  ToolInvocation  as an extra last parameter. + +// 0 visible args + ToolInvocation +static ToolDefinition from( + String name, String description, + Function handler); + +static ToolDefinition fromAsync( + String name, String description, + Function> handler); + +// 1 visible arg + ToolInvocation +static ToolDefinition from( + String name, String description, + Param p1, + BiFunction handler); + +static ToolDefinition fromAsync( + String name, String description, + Param p1, + BiFunction> handler); + +Usage: + +ToolDefinition.from("who_am_i", "...", + invocation -> invocation.getSessionId()); + +ToolDefinition.from("set_phase", "...", phaseParam, + (phase, invocation) -> "phase=" + phase); + +But 2 visible args + ToolInvocation is the cliff: JDK has no  TriFunction . So with JDK-only, contextual overloads top out at 1 visible arg. + +### 16:10 Prompt + +So we're essentially sacrificing one of our precious arity values to give the `ToolInvocation` as a parameter? + +### 16:16 Response + +Yes — if we insist on JDK-only functional types, that is exactly what happens. + + ToolInvocation  consumes one slot in the available SAM shapes: + +•  Function  = 0 visible args + context +•  BiFunction  = 1 visible arg + context + +But there is no JDK  TriFunction , so you can’t also do: + +• 2 visible args + context + +without inventing a new SAM. + +So the tradeoff is: + +• JDK-only → context support costs an arity slot +• fuller arity + context → requires custom functional interfaces + +### 16:16 Prompt + +But what if we introduced another pair of methods on `ToolDefinition`, analogous to `from` and `fromAsync` but which takes the `ToolInvocation`? Something like `fromWithToolInvocation` and `fromAsyncWithToolInvocation`? + +Then we would be able to keep 0..2 arity for the non-`ToolInvocation` case and 0..1 arity on the `ToolInvocation` case. Right? + +### 16:20 Response + +Yes — that works cleanly. + +You’d have two families: + +Without  ToolInvocation  + +•  from(...)  /  fromAsync(...)  +• supports 0..2 visible args + +With  ToolInvocation  + +•  fromWithToolInvocation(...)  /  fromAsyncWithToolInvocation(...)  +• supports 0..1 visible args using only JDK types + +Example shapes: + +ToolDefinition.from(name, desc, Supplier) +ToolDefinition.from(name, desc, Param p1, Function) +ToolDefinition.from(name, desc, Param p1, Param p2, BiFunction) + +ToolDefinition.fromWithToolInvocation(name, desc, Function) +ToolDefinition.fromWithToolInvocation(name, desc, Param p1, BiFunction) + +and async analogs returning  CompletableFuture . + +So yes: separate families preserve the non-context arities cleanly. + +### 16:20 Prompt + +List out the full 0..1 arity variants of `fromWithToolInvocation` and `fromAsyncWithToolInvocation` for the sync and async cases, similar to what we did with the `from` and `fromAsync`. + + +### 16:22 Response + +```java +// ----------------------------- +// With ToolInvocation context +// ----------------------------- + +// 0 visible args + ToolInvocation, sync: +// Function +static ToolDefinition fromWithToolInvocation( + String name, + String description, + Function handler); + +// 0 visible args + ToolInvocation, async: +// Function> +static ToolDefinition fromAsyncWithToolInvocation( + String name, + String description, + Function> handler); + +// 1 visible arg + ToolInvocation, sync: +// BiFunction +static ToolDefinition fromWithToolInvocation( + String name, + String description, + Param p1, + BiFunction handler); + +// 1 visible arg + ToolInvocation, async: +// BiFunction> +static ToolDefinition fromAsyncWithToolInvocation( + String name, + String description, + Param p1, + BiFunction> handler); +``` + +### 16:22 Prompt + +And give me usage examples for each, with comments. + +### 16:25 Response + +```java +import java.util.concurrent.CompletableFuture; +import java.util.function.BiFunction; +import java.util.function.Function; + +// Assume this exists in the lambda-based API. +Param phaseParam = Param.of("phase", "Current phase"); + +// ------------------------------------------- +// fromWithToolInvocation(...) +// ------------------------------------------- + +// 0 visible args + ToolInvocation, sync: +// Function +ToolDefinition sessionInfoSync = ToolDefinition.fromWithToolInvocation( + "session_info", + "Return the current session id", + invocation -> "sessionId=" + invocation.getSessionId() +); + +// 1 visible arg + ToolInvocation, sync: +// BiFunction +ToolDefinition reportPhaseSync = ToolDefinition.fromWithToolInvocation( + "report_phase", + "Report the current phase along with invocation context", + phaseParam, + (phase, invocation) -> + "phase=" + phase + ", toolCallId=" + invocation.getToolCallId() +); + +// ------------------------------------------- +// fromAsyncWithToolInvocation(...) +// ------------------------------------------- + +// 0 visible args + ToolInvocation, async: +// Function> +ToolDefinition sessionInfoAsync = ToolDefinition.fromAsyncWithToolInvocation( + "session_info_async", + "Return the current session id asynchronously", + invocation -> CompletableFuture.completedFuture( + "sessionId=" + invocation.getSessionId() + ) +); + +// 1 visible arg + ToolInvocation, async: +// BiFunction> +ToolDefinition reportPhaseAsync = ToolDefinition.fromAsyncWithToolInvocation( + "report_phase_async", + "Report the current phase with invocation context asynchronously", + phaseParam, + (phase, invocation) -> CompletableFuture.completedFuture( + "phase=" + phase + ", toolCallId=" + invocation.getToolCallId() + ) +); +``` + +### 16:26 Prompt + +Now we come to 3.2. + +You said: + +----- + +**Question:** What functional interfaces are needed for clean lambda syntax without casts? + +Unknowns: + +* Naming (`ToolFn1`, `ToolFn2`, `AsyncToolFn1`, etc.). +* Package placement (`com.github.copilot.rpc` vs `com.github.copilot.tool`). +* How to avoid ambiguous overload resolution between sync and async lambdas. + +**Recommendation:** use distinct interfaces for sync and async handlers and keep overload count minimal to reduce ambiguity. + +------ + +**Resolutions** + +* Naming: didn't we resolve that already, just above? + +* Package placement `com.github.copilot.tool`. + +* How to avoid ambiguous overload resolution between sync and async lambdas. + + Didn't we also resolve that? + +### 16:29 Response + +Yes — I think 3.2 is now mostly resolved, and the old wording is stale. + +For the design we converged on: + +• Naming: yes, resolved by using JDK functional interfaces plus method-family names: +•  from  +•  fromAsync  +•  fromWithToolInvocation  +•  fromAsyncWithToolInvocation  +• Package placement: yes,  com.github.copilot.tool . +• Ambiguous overload resolution: largely resolved by: +• separate sync vs async method names +• JDK SAMs +• capped arities + +So 3.2 should probably be rewritten from “what custom functional interfaces do we need?” to something like: + +Confirm that v1 uses only JDK functional interfaces and method-family naming to separate sync, async, and ToolInvocation-aware variants. + +### 16:30 Prompt + +Now we come to 3.3: + +------ + +**Question:** What is the smallest expressive parameter-definition API that preserves schema quality? + +Candidate concepts: + +* `ParamDef` builders (type, name, description, required/default). +* `Params.of(...)` container preserving declaration order. +* Optional helpers for common primitives. + +Unknowns: + +1. How defaults are represented and validated by type. +2. How optionality interacts with default values. +3. Whether descriptions are required by policy. + +------- + +I like the `Params.of`. Evaluating the design we currently elicited, how does that answer the three unknowns for 3.3? + +### 16:34 Prompt + + Params.of(...)  helps, but it does not by itself answer all three unknowns. It mainly answers ordering/composition. + +I’d say the current elicited design answers 3.3 like this: + +• 1. Defaults representation and validation by type + Params.of(...)  only works cleanly if each  Param  carries a type token (for example  Param  or  Class ). Then  defaultValue  can mirror the annotation API and be validated against that type. +• 2. Optionality vs. default values +Reuse the current annotation rule: +•  required=true  by default +• non-empty default means effectively optional +• reject  required=true  + defaultValue present +• 3. Whether descriptions are required by policy + Params.of(...)  does not settle that. Policy is separate. My recommendation: keep descriptions allowed-but-not-forced in API, and enforce stricter quality later if desired. + +So:  Params.of(...)  is a good container, but typed  Param  is the real key. + +### 16:34 Prompt + +So what would `Params` look like with typed Param? + +### 16:35 Response + +``` +Something like this: + +package com.github.copilot.tool; + +import java.util.List; + +public final class Params { + + private final List> values; + + private Params(List> values) { + this.values = List.copyOf(values); + } + + public static Params of(Param... values) { + return new Params(List.of(values)); + } + + public List> values() { + return values; + } +} + +And typed  Param : + +package com.github.copilot.tool; + +public final class Param { + + private final Class type; + private final String name; + private final String value; + private final boolean required; + private final String defaultValue; + + private Param(Class type, String name, String value, boolean required, String defaultValue) { + this.type = type; + this.name = name; + this.value = value; + this.required = required; + this.defaultValue = defaultValue; + } + + public static Param of(Class type, String name, String value) { + return new Param<>(type, name, value, true, ""); + } + + public Param required(boolean required) { + return new Param<>(type, name, value, required, defaultValue); + } + + public Param defaultValue(String defaultValue) { + return new Param<>(type, name, value, false, defaultValue); + } + + public Class type() { return type; } + public String name() { return name; } + public String value() { return value; } + public boolean required() { return required; } + public String defaultValue() { return defaultValue; } +} +``` + +Usage: + +``` +Params params = Params.of( + Param.of(String.class, "query", "Search query"), + Param.of(Integer.class, "limit", "Max results").defaultValue("10") +); +``` + +That gives ordered, typed parameter metadata. + + +### 16:38 Prompt + +Here is your complete `Param`. Rewrite it but take `Class type` as the first parameter. + +### 16:40 Response + +``` +package com.github.copilot.tool; + +import java.util.Objects; + +/** + * Runtime parameter metadata for lambda-defined tools. + * Mirrors the fields of @CopilotToolParam. + */ +public final class Param { + + private final Class type; + private final String value; // description + private final String name; // parameter name override + private final boolean required; // required flag + private final String defaultValue; // default value as string + + private Param(Class type, String value, String name, boolean required, String defaultValue) { + this.type = Objects.requireNonNull(type, "type"); + this.value = value == null ? "" : value; + this.name = name == null ? "" : name; + this.required = required; + this.defaultValue = defaultValue == null ? "" : defaultValue; + } + + /** Minimal fluent entrypoint (required=true, defaultValue=""). */ + public static Param of(Class type, String name, String value) { + return new Param<>(type, value, name, true, ""); + } + + /** Full factory for parity with annotation fields. */ + public static Param of(Class type, String name, String value, boolean required, String defaultValue) { + return new Param<>(type, value, name, required, defaultValue); + } + + // Fluent modifiers + public Param name(String name) { + return new Param<>(this.type, this.value, name, this.required, this.defaultValue); + } + + public Param value(String value) { + return new Param<>(this.type, value, this.name, this.required, this.defaultValue); + } + + public Param required(boolean required) { + return new Param<>(this.type, this.value, this.name, required, this.defaultValue); + } + + public Param defaultValue(String defaultValue) { + return new Param<>(this.type, this.value, this.name, this.required, defaultValue); + } + + // Accessors intentionally match annotation element names + public Class type() { return type; } + public String value() { return value; } + public String name() { return name; } + public boolean required() { return required; } + public String defaultValue() { return defaultValue; } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Param other)) return false; + return required == other.required + && Objects.equals(type, other.type) + && Objects.equals(value, other.value) + && Objects.equals(name, other.name) + && Objects.equals(defaultValue, other.defaultValue); + } + + @Override + public int hashCode() { + return Objects.hash(type, value, name, required, defaultValue); + } +} + +``` + +### 16:51 Prompt + + +With this definition of `Param` have we resolved the three unknowns of 3.3? + +### 16:51 Response + +Mostly yes. + +• 1. Defaults representation and validation by type +Yes, mostly resolved.  Param  with  Class type  gives the API enough type information to validate  defaultValue  against the declared type. +• 2. Optionality interacts with default values +Yes, conceptually resolved. Reuse the annotation rule: reject  required=true  with non-empty  defaultValue ;  defaultValue(...)  should imply optional in practice, or at least be validated consistently. +• 3. Whether descriptions are required by policy + +Not fully resolved.  Param  supports descriptions cleanly via  value , but whether they are mandatory is still a policy decision, not an API-shape decision. + +So: 3.3 is nearly resolved; only the description-policy part remains truly open. + +### 16:52 Prompt + +Yes, they need descriptions. That's how the LLM knows how to invoke the right tool for the job. + +What needs to change in the API to set and enforce the description policy? + +### 16:53 Response + +Two things: + +- Set the policy in the API shape + - Keep description as a required positional argument in  Param.of(Class type, String name, String description, ...)  + - Prefer renaming  value  to  description  in the runtime  Param  type for clarity +- Enforce it + -  Param.of(...)  throws if  description  is null/blank + - fluent  value(...) / description(...)  also rejects blank + -  ToolDefinition.from*  validates every supplied  Param  + - for annotation tools, the processor should emit a compile error if  @Param.value()  is empty + +So: mandatory constructor arg + validation at both creation and registration. + +### 16:56 Prompt + +Here, again, is our `Param`. + +Rework it to do these 3.3 things. + +```java +package com.github.copilot.tool; + +import java.util.Objects; + +/** + * Runtime parameter metadata for lambda-defined tools. + * Mirrors the fields of @CopilotToolParam. + */ +public final class Param { + + private final Class type; + private final String value; // description + private final String name; // parameter name override + private final boolean required; // required flag + private final String defaultValue; // default value as string + + private Param(Class type, String value, String name, boolean required, String defaultValue) { + this.type = Objects.requireNonNull(type, "type"); + this.value = value == null ? "" : value; + this.name = name == null ? "" : name; + this.required = required; + this.defaultValue = defaultValue == null ? "" : defaultValue; + } + + /** Minimal fluent entrypoint (required=true, defaultValue=""). */ + public static Param of(Class type, String name, String value) { + return new Param<>(type, value, name, true, ""); + } + + /** Full factory for parity with annotation fields. */ + public static Param of(Class type, String name, String value, boolean required, String defaultValue) { + return new Param<>(type, value, name, required, defaultValue); + } + + // Fluent modifiers + public Param name(String name) { + return new Param<>(this.type, this.value, name, this.required, this.defaultValue); + } + + public Param value(String value) { + return new Param<>(this.type, value, this.name, this.required, this.defaultValue); + } + + public Param required(boolean required) { + return new Param<>(this.type, this.value, this.name, required, this.defaultValue); + } + + public Param defaultValue(String defaultValue) { + return new Param<>(this.type, this.value, this.name, this.required, defaultValue); + } + + // Accessors intentionally match annotation element names + public Class type() { return type; } + public String value() { return value; } + public String name() { return name; } + public boolean required() { return required; } + public String defaultValue() { return defaultValue; } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Param other)) return false; + return required == other.required + && Objects.equals(type, other.type) + && Objects.equals(value, other.value) + && Objects.equals(name, other.name) + && Objects.equals(defaultValue, other.defaultValue); + } + + @Override + public int hashCode() { + return Objects.hash(type, value, name, required, defaultValue); + } +} +``` + +### 16:57 Response + +Yes — I’d rework it like this: + +```java +package com.github.copilot.tool; + +import java.util.Objects; + +/** + * Runtime parameter metadata for lambda-defined tools. + * Mirrors the fields of @CopilotToolParam. + */ +public final class Param { + + private final Class type; + private final String description; + private final String name; + private final boolean required; + private final String defaultValue; + + private Param(Class type, String description, String name, boolean required, String defaultValue) { + this.type = Objects.requireNonNull(type, "type"); + this.description = requireNonBlank(description, "description"); + this.name = requireNonBlank(name, "name"); + this.defaultValue = defaultValue == null ? "" : defaultValue; + this.required = required; + + if (this.required && !this.defaultValue.isEmpty()) { + throw new IllegalArgumentException("required=true cannot be combined with a non-empty defaultValue"); + } + + validateDefaultValue(type, this.defaultValue); + } + + /** Minimal fluent entrypoint (required=true, no default). */ + public static Param of(Class type, String name, String description) { + return new Param<>(type, description, name, true, ""); + } + + /** Full factory for parity with annotation fields. */ + public static Param of( + Class type, + String name, + String description, + boolean required, + String defaultValue) { + return new Param<>(type, description, name, required, defaultValue); + } + + public Param name(String name) { + return new Param<>(this.type, this.description, name, this.required, this.defaultValue); + } + + public Param description(String description) { + return new Param<>(this.type, description, this.name, this.required, this.defaultValue); + } + + /** + * Alias for annotation parity. + */ + public Param value(String value) { + return description(value); + } + + public Param required(boolean required) { + return new Param<>(this.type, this.description, this.name, required, this.defaultValue); + } + + /** + * Setting a default makes the parameter optional. + */ + public Param defaultValue(String defaultValue) { + return new Param<>(this.type, this.description, this.name, false, defaultValue); + } + + public Class type() { + return type; + } + + /** + * Alias kept for annotation parity. + */ + public String value() { + return description; + } + + public String description() { + return description; + } + + public String name() { + return name; + } + + public boolean required() { + return required; + } + + public String defaultValue() { + return defaultValue; + } + + public boolean hasDefaultValue() { + return !defaultValue.isEmpty(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Param other)) { + return false; + } + return required == other.required + && Objects.equals(type, other.type) + && Objects.equals(description, other.description) + && Objects.equals(name, other.name) + && Objects.equals(defaultValue, other.defaultValue); + } + + @Override + public int hashCode() { + return Objects.hash(type, description, name, required, defaultValue); + } + + private static String requireNonBlank(String value, String fieldName) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException(fieldName + " must not be null or blank"); + } + return value; + } + + private static void validateDefaultValue(Class type, String defaultValue) { + if (defaultValue == null || defaultValue.isEmpty()) { + return; + } + + try { + if (type == String.class) { + return; + } + if (type == Integer.class || type == int.class) { + Integer.parseInt(defaultValue); + return; + } + if (type == Long.class || type == long.class) { + Long.parseLong(defaultValue); + return; + } + if (type == Double.class || type == double.class) { + Double.parseDouble(defaultValue); + return; + } + if (type == Float.class || type == float.class) { + Float.parseFloat(defaultValue); + return; + } + if (type == Short.class || type == short.class) { + Short.parseShort(defaultValue); + return; + } + if (type == Byte.class || type == byte.class) { + Byte.parseByte(defaultValue); + return; + } + if (type == Boolean.class || type == boolean.class) { + if (!"true".equalsIgnoreCase(defaultValue) && !"false".equalsIgnoreCase(defaultValue)) { + throw new IllegalArgumentException("must be 'true' or 'false'"); + } + return; + } + if (type.isEnum()) { + @SuppressWarnings({ "rawtypes", "unchecked" }) + Class enumType = (Class) type; + Enum.valueOf(enumType, defaultValue); + return; + } + } catch (RuntimeException ex) { + throw new IllegalArgumentException( + "defaultValue '" + defaultValue + "' is not valid for type " + type.getSimpleName(), ex); + } + + throw new IllegalArgumentException( + "defaultValue is not supported for type " + type.getName() + " without a custom coercion policy"); + } +} +``` + +Key changes: + +• description is mandatory +• default implies optional +• required+default is rejected +• default is type-validated + +### 16:59 Prompt + +You stated: + +------ + +- Enforce it + -  Param.of(...)  throws if  description  is null/blank + - fluent  value(...) / description(...)  also rejects blank + -  ToolDefinition.from*  validates every supplied  Param  + - for annotation tools, the processor should emit a compile error if  @Param.value()  is empty + + +----- + +What do I need to put in the **Resolution** of 3.3 in `copilot-sdk/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md` regarding enforcement, aside from what we already did by updating the `Param` code? + +### 17:01 Response + +Put this in the Resolution: + +- Lambda API enforcement + -  Param.of(...)  and fluent mutators reject blank  name / description . + -  Param.defaultValue(...)  validates the value against  Class . + -  required=true  with non-empty  defaultValue  is rejected. + - Every  ToolDefinition.from*  /  fromAsync*  overload re-validates supplied  Param  objects before building the tool. +- Annotation API alignment + -  @CopilotTool  processing should eventually enforce non-empty  @Param.value()  at compile time for parity. + +So the extra enforcement beyond  Param  itself is mainly: registration-time validation in  ToolDefinition.from* . + +### 17:03 Prompt + + +Still on 3.3, we have + +> * `Params.of(...)` container preserving declaration order. + +We didn't define the `Params` class. What is it? Why do we need it? How is it used? + +### 17:05 Response + + Params  would just be an immutable ordered container around  List> . + +But with the API we’ve now elicited, I don’t think we need it in public v1. + +Why? +Because these already preserve order: + +from(name, desc, p1, handler) +from(name, desc, p1, p2, handler) + + p1 , then  p2 , is the declaration order. + +So: + +• What is it? Ordered wrapper for params. +• Why need it? Mainly if you want one container argument instead of per-arity overloads. +• How used?  from(name, desc, Params.of(p1, p2), handler)  + +My recommendation: mark  Params.of(...)  as not needed for v1. + +### 17:08 Prompt + +Now on to 3.4: + +----- + + +**Question:** Which Java parameter types are supported in v1 for inline definitions? + +Minimum set: + +* `String` +* numeric primitives/boxed +* `boolean`/`Boolean` +* enums +* `List` for simple `T` +* `Map` (or defer typed map support if not stable) +* record/POJO as parameter type + +Unknowns: + +* Whether nested objects and polymorphic types are in scope for v1. +* Whether schema generation should reuse existing tool schema utilities directly. + +**Recommendation:** implement the subset already validated by existing ergonomic and low-level tests, then extend. + + +----- + +Yes, we do need all of those in the minimal set. But this could be a huge rabbit hole. How explicit must we be? + +### 17:09 Response + +Be explicit about policy, not exhaustive implementation detail. + +For 3.4, I’d resolve it at this level: + +• v1 supports exactly the same parameter-type surface already supported by the existing Java schema/tool pipeline, reused for lambda tools. +• This includes the minimal set you listed. +• No new schema semantics are invented for tool-as-lambda. +• If a type is not already supported by the current Java ergonomic/low-level tool path, it is out of scope for v1. + +That keeps it concrete without turning the plan into a second schema-design document. + +### 17:12 Prompt + +Now we come to 3.6. + +----- + + +**Question:** How do callers set `overridesBuiltInTool`, `skipPermission`, and `defer` on inline tools? + +Candidates: + +* Overloads with an options object. +* Fluent builder wrapping `ToolDefinition.from(...)`. + +----- + + +Couldn't we add some fluent modifier methods, like you suggested? Something like this? + +``` +ToolDefinition.from(...).overridesBuiltInTool(true).skipPermission(true).defer(true) +``` + +### 17:15 Response + +Yes — that fits the current shape well, because  ToolDefinition  is an immutable record. + +So fluent modifiers could simply return a new copy: + +ToolDefinition.from(...) + .overridesBuiltInTool(true) + .skipPermission(true) + .defer(ToolDefer.AUTO); + +That’s cleaner than extra options objects for v1. + +One tweak: not  defer(true) . Use the existing enum: + +.defer(ToolDefer.AUTO) // or NEVER + +So I’d resolve 3.6 as: + +• use fluent immutable modifiers on  ToolDefinition  +• reuse existing  Boolean  flags and  ToolDefer  enum +• no separate options object in v1 + +### 17:15 Prompt + + +Ok, give me the text to put in the **Resolution** section for this one. Include the code snippets. + +### 17:23 Response + +**Resolution:** + +Use fluent immutable modifier methods on `ToolDefinition` rather than introducing a separate options object in v1. + +Because `ToolDefinition` is already an immutable record carrying `overridesBuiltInTool`, `skipPermission`, and `defer`, the lambda-based `from*` factories should return a `ToolDefinition` that callers may further customize with copy-style fluent methods. + +Example: + +```java +ToolDefinition tool = ToolDefinition.from( + "report_intent", + "Reports the agent's current intent", + Param.of(String.class, "intent", "The intent to report"), + intent -> "Reported intent: " + intent) + .overridesBuiltInTool(true) + .skipPermission(true) + .defer(ToolDefer.AUTO); +``` + +Equivalent context-aware example: + +``` +ToolDefinition tool = ToolDefinition.fromWithToolInvocation( + "report_phase", + "Reports the current phase with invocation context", + Param.of(String.class, "phase", "The current phase"), + (phase, invocation) -> "phase=" + phase + ", toolCallId=" + invocation.getToolCallId()) + .skipPermission(true) + .defer(ToolDefer.NEVER); +``` + +The modifier surface for v1 is: + +``` +ToolDefinition overridesBuiltInTool(boolean value); +ToolDefinition skipPermission(boolean value); +ToolDefinition defer(ToolDefer value); +``` + +Notes: + +- `defer` should use the existing `ToolDefer` enum, not a boolean. +- This keeps the API aligned with the existing `ToolDefinition` data model. +- This avoids introducing a separate options type solely for inline/lambda-defined tools. +- Existing low-level factories (`createOverride`, `createSkipPermission`, `createWithDefer`) may remain for compatibility, but the new lambda-based API should prefer the fluent style. + +──────────────────── + +### 17:25 Prompt + +Now on to 3.7 + +------ + + +**Question:** Which invalid states should fail early? + +Must-validate cases: + +* duplicate parameter names +* missing required metadata (name/type) +* unsupported type mappings +* incompatible default values + +**Recommendation:** fail fast at tool construction with precise `IllegalArgumentException` messages. + +---- + +I agree with your recommendation. But is there anything else we need to specify in this resolution for 3.7? + +### 17:26 Response + +Yes — I’d make the resolution a bit more explicit about where validation happens and what kind of failure each layer uses. + +Add: + +- Construction-time validation for lambda tools: + - all `ToolDefinition.from*` factories must validate before returning + - failures use `IllegalArgumentException` + - messages should identify the offending tool name and parameter name when possible +- `Param`-local validation: + - blank name/description + - `required=true` with default + - default incompatible with declared type +- Cross-parameter validation: + - duplicate parameter names + - unsupported schema/type mappings + - invalid ToolInvocation-specific combinations, if that lane is included + +So 3.7 should resolve not just what fails early, but which layer owns each validation. + +### 17:28 Prompt + +You stated + +----- + + - invalid ToolInvocation-specific combinations, if that lane is included + + + +---- + +Give me an example of such an invalid combination. + +### 17:29 Response + +A good example is using the wrong factory family for a context-aware lambda. + +For example, this should be invalid: + +ToolDefinition.from( + "report_phase", + "Reports the current phase", + Param.of(String.class, "phase", "The current phase"), + (phase, invocation) -> "phase=" + phase +); + +Why invalid: + +•  from(...)  is the non-ToolInvocation family +• a two-parameter lambda here would mean “2 visible tool params,” not “1 visible param + hidden invocation” + +So if  ToolInvocation  support exists, it should only be legal through: + +ToolDefinition.fromWithToolInvocation(...) +ToolDefinition.fromAsyncWithToolInvocation(...) + +That separation is itself part of the validation story. + +### 17:29 Prompt + +But how would one guard against such an invalid invocation? + +### 17:30 Response + +Mostly by the type system, not runtime checks. + +Example: + +ToolDefinition.from( + "report_phase", + "Reports the current phase", + Param.of(String.class, "phase", "The current phase"), + (phase, invocation) -> ... +); + +If  from(...)  is declared to take: + +Param, Function + +then a two-argument lambda simply does not compile. + +Likewise, only  fromWithToolInvocation(...)  would accept: + +BiFunction + +So the main guard is: + +• distinct method families +• strongly typed JDK SAM parameters +• compile-time mismatch + +Runtime validation is only needed for metadata/state checks, not this misuse. + +### 17:30 Prompt + +So it seems we can remove this from the 3.7 resolution: + +----- + + - invalid ToolInvocation-specific combinations, if that lane is included + + +---- + +No? + +### 17:31 Response + +Yes, I agree. + +For the lambda API shape we’ve converged on, that item does not belong in 3.7, because those misuses are prevented at compile time by overload typing, not by runtime validation. + +So 3.7 should stay focused on true runtime/construction-time invalid states such as: + +• duplicate parameter names +• missing/blank metadata +• unsupported type mappings +• incompatible default values + + +### 17:32 Prompt + + +On to 3.8. + +You said: + +----- + +**Question:** Where do new public types live without destabilizing existing API? + +Unknowns: + +* whether to place new functional interfaces and param DSL under `rpc` or `tool` +* impact on `module-info.java` exports + +**Recommendation:** place user-facing ergonomics in the package users already discover for tools, and keep internal helpers package-private. + + +---- + +I thought we already agreed `package com.github.copilot.tool`? + +As for as the `module-info.java` the common sense thing is what you recommended. + +What are we missing? + +### 17:34 Response + +You’re right: 3.8 is basically resolved now. + +What changed since that earlier wording: + +• we dropped custom functional interfaces +• we settled on  com.github.copilot.tool  for the new public ergonomics types +• only  ToolDefinition  remains in  com.github.copilot.rpc  + +So the only thing left to state in the resolution is: + +• new public helper types like  Param  belong in  com.github.copilot.tool  +• any necessary  module-info.java  export updates should expose only that user-facing package surface +• no extra public internal-helper types should leak just to support lambda tools + +So we’re not really missing a design decision anymore; we just need to record it cleanly. + From dce5e82476ee805fa4d28ed6da3c916a713ba2b4 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Mon, 29 Jun 2026 18:10:21 -0400 Subject: [PATCH 06/13] On branch edburns/1810-java-tool-ergonomics-tool-as-lambda Completed Phase 03. modified: 1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md modified: 1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md Signed-off-by: Ed Burns --- ...rance-reduction-for-implementation-plan.md | 151 +++++------ .../20260629-prompts.md | 247 ++++++++++++++++++ 2 files changed, 311 insertions(+), 87 deletions(-) diff --git a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md index e215272d6..98b294ffa 100644 --- a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md +++ b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md @@ -74,85 +74,7 @@ Open decisions: Assume current annotation class `com.github.copilot.tool.Param` is renamed to `CopilotToolParam`. -Then we define `Param` as - -```java -package com.github.copilot.tool; - -import java.util.Objects; - -/** - * Runtime parameter metadata for lambda-defined tools. - * Mirrors the fields of @CopilotToolParam. - */ -public final class Param { - - private final Class type; - private final String value; // description - private final String name; // parameter name override - private final boolean required; // required flag - private final String defaultValue; // default value as string - - private Param(Class type, String value, String name, boolean required, String defaultValue) { - this.type = Objects.requireNonNull(type, "type"); - this.value = value == null ? "" : value; - this.name = name == null ? "" : name; - this.required = required; - this.defaultValue = defaultValue == null ? "" : defaultValue; - } - - /** Minimal fluent entrypoint (required=true, defaultValue=""). */ - public static Param of(Class type, String name, String value) { - return new Param<>(type, value, name, true, ""); - } - - /** Full factory for parity with annotation fields. */ - public static Param of(Class type, String name, String value, boolean required, String defaultValue) { - return new Param<>(type, value, name, required, defaultValue); - } - - // Fluent modifiers - public Param name(String name) { - return new Param<>(this.type, this.value, name, this.required, this.defaultValue); - } - - public Param value(String value) { - return new Param<>(this.type, value, this.name, this.required, this.defaultValue); - } - - public Param required(boolean required) { - return new Param<>(this.type, this.value, this.name, required, this.defaultValue); - } - - public Param defaultValue(String defaultValue) { - return new Param<>(this.type, this.value, this.name, this.required, defaultValue); - } - - // Accessors intentionally match annotation element names - public Class type() { return type; } - public String value() { return value; } - public String name() { return name; } - public boolean required() { return required; } - public String defaultValue() { return defaultValue; } - - @Override - public boolean equals(Object o) { - if (!(o instanceof Param other)) return false; - return required == other.required - && Objects.equals(type, other.type) - && Objects.equals(value, other.value) - && Objects.equals(name, other.name) - && Objects.equals(defaultValue, other.defaultValue); - } - - @Override - public int hashCode() { - return Objects.hash(type, value, name, required, defaultValue); - } -} -``` - -The answer to the arity question is shown in this sample. +Then, the answer to the arity question is shown here. ```java package com.github.copilot.tool; @@ -337,6 +259,59 @@ public final class Param { } ``` +Then the API: + +```java +// ------------------------------------------------------- +// from(...) — sync, no ToolInvocation, arity 0..2 +// ------------------------------------------------------- + +// 0-arg: Supplier +static ToolDefinition from( + String name, + String description, + Supplier handler); + +// 1-arg: Function +static ToolDefinition from( + String name, + String description, + Param p1, + Function handler); + +// 2-arg: BiFunction +static ToolDefinition from( + String name, + String description, + Param p1, + Param p2, + BiFunction handler); + +// ------------------------------------------------------- +// fromAsync(...) — async, no ToolInvocation, arity 0..2 +// ------------------------------------------------------- + +// 0-arg: Supplier> +static ToolDefinition fromAsync( + String name, + String description, + Supplier> handler); + +// 1-arg: Function> +static ToolDefinition fromAsync( + String name, + String description, + Param p1, + Function> handler); + +// 2-arg: BiFunction> +static ToolDefinition fromAsync( + String name, + String description, + Param p1, + Param p2, + BiFunction> handler); +``` **Whether zero-arg tools need a dedicated overload.**: Yes. And it needs two. See the preceding answer. @@ -387,8 +362,7 @@ import java.util.concurrent.CompletableFuture; import java.util.function.BiFunction; import java.util.function.Function; -// Assume this exists in the lambda-based API. -Param phaseParam = Param.of("phase", "Current phase"); +Param phaseParam = Param.of(String.class, "phase", "Current phase"); // ------------------------------------------- // fromWithToolInvocation(...) @@ -438,7 +412,6 @@ ToolDefinition reportPhaseAsync = ToolDefinition.fromAsyncWithToolInvocation( ); ``` - ### 3.2 — Functional interface set and type inference **Question:** What functional interfaces are needed for clean lambda syntax without casts? @@ -453,7 +426,9 @@ Unknowns: **Resolution:** -The answers to 3.1 resolve this. But also please confirm that v1 uses only JDK functional interfaces and method-family naming to separate sync, async, and ToolInvocation-aware variants. +* Naming (`ToolFn1`, `ToolFn2`, `AsyncToolFn1`, etc.): see 3.1. +* Package placement `com.github.copilot.tool`. +* How to avoid ambiguous: Tools-as-lambda uses only JDK functional interfaces; sync and async are separated by method-family naming (`from`/`fromAsync`/`fromWithToolInvocation`/`fromAsyncWithToolInvocation`); no custom SAMs required. ### 3.3 — Parameter metadata DSL design @@ -475,6 +450,8 @@ Unknowns: **Resolution:** +`Params.of(...)`: not needed. + Use the above `Param` class. - Lambda API enforcement @@ -654,7 +631,7 @@ After Phase 3 is resolved, implement in this order. **Likely files:** -* `java/src/main/java/com/github/copilot/rpc/` (new interfaces and metadata types) +* `java/src/main/java/com/github/copilot/tool/` (new interfaces and metadata types) **Gating criteria:** compile passes; API signatures are stable and unambiguous for common lambda call sites. @@ -670,11 +647,11 @@ After Phase 3 is resolved, implement in this order. ### 4.3 — Implement schema and coercion internals -**What:** Build internal mapping from `ParamDef` + handler type info to JSON schema and typed invocation. +**What:** Build internal mapping from `Param` + handler type info to JSON schema and typed invocation. **Likely files:** -* new internal helper(s) under `java/src/main/java/com/github/copilot/rpc/` or `.../tool/` +* new internal helper(s) under `java/src/main/java/com/github/copilot/tool/`. **Gating criteria:** matches baseline behavior contract from Phase 2. @@ -690,7 +667,7 @@ After Phase 3 is resolved, implement in this order. **Likely files:** -* `java/src/test/java/com/github/copilot/rpc/*` +* `java/src/test/java/com/github/copilot/tool/*` **Gating criteria:** deterministic tests covering success + failure paths. diff --git a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md index 3eae837c5..f84cfdac6 100644 --- a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md +++ b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md @@ -340,6 +340,7 @@ Ok, so let's go with 1..3. Show me what the `from` overloads would look like for Yes, I’d do both sync and async. +``` // 0 args static ToolDefinition from( String name, String description, Supplier handler); @@ -369,6 +370,7 @@ static ToolDefinition fromAsync( String name, String description, Param p1, Param p2, Param p3, AsyncFunction3 handler);  TriFunction  and  AsyncFunction{1,2,3}  would be custom SAMs. +``` ### 14:25 Prompt @@ -384,6 +386,7 @@ ToolDefinition.fromAsync(String name, String description, Supplier ToolDefinition fromAsync( String name, @@ -404,6 +407,7 @@ static ToolDefinition fromAsync( Param p1, Param p2, BiFunction> handler); +``` So yes — same pattern as zero-arg async: the handler returns  CompletableFuture , and you don’t need any custom SAM types. @@ -2215,3 +2219,246 @@ So the only thing left to state in the resolution is: So we’re not really missing a design decision anymore; we just need to record it cleanly. +### 17:41 Prompt + +I have updated `copilot-sdk/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md` with all the Phase 3 **Resolutions:** we elicited. Please review it and see if I missed anything. + +### 17:44 Response + +I see several gaps and issues: + +3.1 has two problems: + +DONE • Contains the old, unvalidated version of  Param  followed by the final version — the first block should be removed, it's stale. +DONE • The complete  from / fromAsync  0..2 arity signatures are never explicitly listed — only usage examples appear. The plan should have the method signatures as the canonical API surface. +DONE • The usage example at line 391 still uses the old untyped form:  Param.of("phase", "Current phase")  instead of  Param.of(String.class, "phase", "Current phase") . + +DONE 3.2 — the resolution reads like an instruction, not a decision. It should say: "v1 uses only JDK functional interfaces; sync and async are separated by method-family naming ( from / fromAsync / fromWithToolInvocation / fromAsyncWithToolInvocation ); no custom SAMs required." + +3.3 — two things missing: + +DONE •  Params.of(...)  decision: not needed for v1 (per our conversation) — should be explicitly recorded. +DONE • Annotation API alignment note:  @CopilotTool  processing should eventually enforce non-empty  @Param.value()  at compile time. Filed issue 1836. + +DONE 4.1 — says  com.github.copilot.rpc/  but we decided new types go in  com.github.copilot.tool . + +DONE 4.3 — says  ParamDef  on line 673, but the design uses  Param . + +### 17:48 Prompt + +You stated + +------ + +• The complete  from / fromAsync  0..2 arity signatures are never explicitly listed — only usage examples appear. The plan should have the method signatures as the canonical API surface. + +----- + +Please state them here and I'll copy them into place + +### 17:49 Response + +```java +// ------------------------------------------------------- +// from(...) — sync, no ToolInvocation, arity 0..2 +// ------------------------------------------------------- + +// 0-arg: Supplier +static ToolDefinition from( + String name, + String description, + Supplier handler); + +// 1-arg: Function +static ToolDefinition from( + String name, + String description, + Param p1, + Function handler); + +// 2-arg: BiFunction +static ToolDefinition from( + String name, + String description, + Param p1, + Param p2, + BiFunction handler); + +// ------------------------------------------------------- +// fromAsync(...) — async, no ToolInvocation, arity 0..2 +// ------------------------------------------------------- + +// 0-arg: Supplier> +static ToolDefinition fromAsync( + String name, + String description, + Supplier> handler); + +// 1-arg: Function> +static ToolDefinition fromAsync( + String name, + String description, + Param p1, + Function> handler); + +// 2-arg: BiFunction> +static ToolDefinition fromAsync( + String name, + String description, + Param p1, + Param p2, + BiFunction> handler); +``` + +### 17:51 Prompt + +You stated: + +> • The usage example at line 391 still uses the old untyped form:  Param.of("phase", "Current phase")  instead of  Param.of(String.class, "phase", "Current phase") . + +Great catch. Here is the full example. Please rewrite it to use the typed form. + +```java +import java.util.concurrent.CompletableFuture; +import java.util.function.BiFunction; +import java.util.function.Function; + +// Assume this exists in the lambda-based API. +Param phaseParam = Param.of("phase", "Current phase"); + +// ------------------------------------------- +// fromWithToolInvocation(...) +// ------------------------------------------- + +// 0 visible args + ToolInvocation, sync: +// Function +ToolDefinition sessionInfoSync = ToolDefinition.fromWithToolInvocation( + "session_info", + "Return the current session id", + invocation -> "sessionId=" + invocation.getSessionId() +); + +// 1 visible arg + ToolInvocation, sync: +// BiFunction +ToolDefinition reportPhaseSync = ToolDefinition.fromWithToolInvocation( + "report_phase", + "Report the current phase along with invocation context", + phaseParam, + (phase, invocation) -> + "phase=" + phase + ", toolCallId=" + invocation.getToolCallId() +); + +// ------------------------------------------- +// fromAsyncWithToolInvocation(...) +// ------------------------------------------- + +// 0 visible args + ToolInvocation, async: +// Function> +ToolDefinition sessionInfoAsync = ToolDefinition.fromAsyncWithToolInvocation( + "session_info_async", + "Return the current session id asynchronously", + invocation -> CompletableFuture.completedFuture( + "sessionId=" + invocation.getSessionId() + ) +); + +// 1 visible arg + ToolInvocation, async: +// BiFunction> +ToolDefinition reportPhaseAsync = ToolDefinition.fromAsyncWithToolInvocation( + "report_phase_async", + "Report the current phase with invocation context asynchronously", + phaseParam, + (phase, invocation) -> CompletableFuture.completedFuture( + "phase=" + phase + ", toolCallId=" + invocation.getToolCallId() + ) +); +``` + +### 17:52 Response + +```java +import java.util.concurrent.CompletableFuture; +import java.util.function.BiFunction; +import java.util.function.Function; + +Param phaseParam = Param.of(String.class, "phase", "Current phase"); + +// ------------------------------------------- +// fromWithToolInvocation(...) +// ------------------------------------------- + +// 0 visible args + ToolInvocation, sync: +// Function +ToolDefinition sessionInfoSync = ToolDefinition.fromWithToolInvocation( + "session_info", + "Return the current session id", + invocation -> "sessionId=" + invocation.getSessionId() +); + +// 1 visible arg + ToolInvocation, sync: +// BiFunction +ToolDefinition reportPhaseSync = ToolDefinition.fromWithToolInvocation( + "report_phase", + "Report the current phase along with invocation context", + phaseParam, + (phase, invocation) -> + "phase=" + phase + ", toolCallId=" + invocation.getToolCallId() +); + +// ------------------------------------------- +// fromAsyncWithToolInvocation(...) +// ------------------------------------------- + +// 0 visible args + ToolInvocation, async: +// Function> +ToolDefinition sessionInfoAsync = ToolDefinition.fromAsyncWithToolInvocation( + "session_info_async", + "Return the current session id asynchronously", + invocation -> CompletableFuture.completedFuture( + "sessionId=" + invocation.getSessionId() + ) +); + +// 1 visible arg + ToolInvocation, async: +// BiFunction> +ToolDefinition reportPhaseAsync = ToolDefinition.fromAsyncWithToolInvocation( + "report_phase_async", + "Report the current phase with invocation context asynchronously", + phaseParam, + (phase, invocation) -> CompletableFuture.completedFuture( + "phase=" + phase + ", toolCallId=" + invocation.getToolCallId() + ) +); +``` + +### 17:55 Prompt + +You stated + +> • Annotation API alignment note:  @CopilotTool  processing should eventually enforce non-empty  @Param.value()  at compile time. + +Using the existing issue-filing practices you just established and used earlier in this session, file a new child issue of https://github.com/github/copilot-sdk/issues/1809 of type **Task** to assign to an agent to do this enforcement work. + +### 18:04 Response + +Thought for 8s + +Shell Create upstream param description enforcement issue 155 lines… +$body = @' ## Overview Enforce a non-empty `@Param.value()` (description) at compile time in the `CopilotToolProce… + +Issue created. Now setting type to Task and attaching as child of #1809. + +Shell Get new issue node id 2 lines… +gh api graphql -f query='query { repository(owner:"github", name:"copilot-sdk") { issue(number:1836) { id } } }' +Shell Set Task type and attach to parent 1809 19 lines… +$typeMutation = @' mutation($issueId: ID!, $issueTypeId: ID!) { updateIssueIssueType(input: { issueId: $issueId, i… + +Done. + +Created  github/copilot-sdk#1836  — Task, child of  #1809 : + +• https://github.com/github/copilot-sdk/issues/1836 + +### 18:04 Prompt + + From 584400ba1a3548cdf9f090373fb3f435c6a66a73 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Mon, 29 Jun 2026 19:29:37 -0400 Subject: [PATCH 07/13] GUTDODP --- .../20260629-prompts.md | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md index f84cfdac6..d0d157326 100644 --- a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md +++ b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md @@ -2461,4 +2461,161 @@ Created  github/copilot-sdk#1836  — Task, child of  #1809 : ### 18:04 Prompt +PR https://github.com/github/copilot-sdk/pull/1832/ implements what you filed as #1831. + +Your involvement in this had this text: + +``` +I’ll inspect Epic #1682 and its child issues for style, then create the new Task issue under #1809 with a concrete description. +``` + +I have checked out this code to the topic branch `edburns/review-copilot-pr-1832`, checked out locally. + +The remote copilot reviewing agent only turned up one comment, a trivial spotless one. + +Review the 4 commits of this PR for correctness and completeness. I am particularly keen to ensure we have good test coverage. + + +### 18:38 Response + +Here's my review: + +──────────────────── + +Overall: solid, correct implementation. The PR cleanly implements `ToolInvocation` injection by: + +1. Filtering it from schema generation +2. Passing `invocation` directly in generated method calls +3. Good compile-time validation (duplicate, `@Param`-annotated) + +Test coverage assessment: + +✅ Well covered: + +- Instance method + ToolInvocation (handler invocation + schema exclusion) +- Static method + ToolInvocation +- Async method + ToolInvocation +- Compile error: duplicate ToolInvocation params +- Compile error: `@Param` on ToolInvocation +- Processor-level tests verify generated code shape + +⚠️ Gaps I'd flag: + +1. Zero visible args + ToolInvocation only. No test covers: + +``` +@CopilotTool("...") +public String onlyInvocation(ToolInvocation invocation) { ... } +``` + +This exercises the `schemaParameters.isEmpty()` path with a non-empty `params` list. + +2. `ToolInvocation` as FIRST parameter. All fixtures put it last. Add a test with: + +``` +@CopilotTool("...") +public String invertedOrder(ToolInvocation invocation, @Param("Phase") String phase) { ... } +``` + +The `generateArgList` iterates over *all* params (not just schema params), so position matters. + +3. Single-record + ToolInvocation. What happens with: + +``` +@CopilotTool("...") +public String withRecord(SearchArgs args, ToolInvocation invocation) { ... } +``` + +The `schemaParameters.size() == 1 && isRecord(...)` path would activate — does the generated code correctly pass both? + +4. README documentation is good but could mention the position-independence (if that's the intent). + +Minor correctness concern: + +In `generateLambdaBody`, when `schemaParameters` is non-empty, the code extracts args from the map. But `generateArgList` is called with the full `params` list (not `schemaParameters`). This is correct! But the variable extraction loop only iterates `schemaParameters`, so ToolInvocation params won't have a local variable declared — they get `invocation` directly in the call. This works because the lambda parameter is named `invocation`. ✅ + +Verdict: Merge-ready with the caveat that adding test cases for gaps #1–3 above would strengthen confidence. Gap #2 (invocation-first position) is the most important to verify works correctly. + +### 18:42 Prompt + +What's this "position-independence" thing? + +### 18:47 Response + +I mean: can  ToolInvocation  appear anywhere in the parameter list — first, middle, or last — and still work? + +// Last (all fixtures use this) +public String report(@Param("Phase") String phase, ToolInvocation invocation) + +// First +public String report(ToolInvocation invocation, @Param("Phase") String phase) + +// Middle (if 2+ visible args) +public String report(@Param("Phase") String phase, ToolInvocation invocation, @Param("Limit") int limit) + +Looking at the implementation,  generateArgList  iterates over all params in declaration order and emits  invocation  for ToolInvocation-typed ones and the extracted variable name for others. So yes, position should not matter — it should be position-independent. + +But there's no test proving it. All fixtures put ToolInvocation last. That's why I flagged gap #2. + +### 18:48 Prompt? + +Does the specification call for position independence? + +### 18:49 Response + +Looking back at the issue spec (#1831), it says: + +If a `@CopilotTool` method declares a parameter of type `com.github.copilot.rpc.ToolInvocation`, the annotation processor must: +• exclude that parameter from generated JSON Schema +• pass the current `invocation` object directly when generating the method call + +It does not specify position requirements or constraints. It doesn't say "must be last" nor does it say "can be anywhere." + +So: the spec is silent on position. The implementation happens to support any position, but there's no explicit requirement either way. + +### 18:50 Prompt + +Given that the implementation supports it, we must document it. We must document it in the javadoc, not just in the README. + +I am working your comments into a review response. Give me the text to require the agent to document the position independence in the normative Javadoc and the java README. Also to create tests proving the correctness of the position independence. + +### 18:52 Prompt + +Position independence of `ToolInvocation` parameter + +The implementation correctly supports `ToolInvocation` at any position in the method signature — first, middle, or last. However, this behavior is undocumented and untested. + +Please: + +1. Add Javadoc to the `ToolInvocation` class (or to `@CopilotTool` annotation Javadoc) stating that `ToolInvocation` may appear at any position in the parameter list and is always excluded from the tool schema regardless of position. + +2. Update the Java README `ToolInvocation` section to mention that position does not matter — it can appear before, between, or after `@Param`-annotated parameters. + +3. Add tests proving position independence: + + - `ToolInvocation` as the first parameter: + + ``` + @CopilotTool("...") + public String report(ToolInvocation invocation, @Param("Phase") String phase) { ... } + ``` + + - `ToolInvocation` as the only parameter (zero visible args): + + ``` + @CopilotTool("...") + public String onlyContext(ToolInvocation invocation) { ... } + ``` + + - `ToolInvocation` in the middle of two visible params: + + ``` + @CopilotTool("...") + public String report(@Param("Phase") String phase, ToolInvocation invocation, @Param("Limit") int limit) { ... } + ``` + +Each test should verify both that the schema excludes `ToolInvocation` and that the handler receives the correct invocation context. + + + From a302632c31785e99b321b92f641c002b5a86e109 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Mon, 29 Jun 2026 19:54:55 -0400 Subject: [PATCH 08/13] GUTDODP --- .../20260629-prompts.md | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md index d0d157326..c8e8e1db9 100644 --- a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md +++ b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md @@ -2528,7 +2528,36 @@ public String withRecord(SearchArgs args, ToolInvocation invocation) { ... } The `schemaParameters.size() == 1 && isRecord(...)` path would activate — does the generated code correctly pass both? -4. README documentation is good but could mention the position-independence (if that's the intent). +4. Test coverage for position independence. + + 1. Add Javadoc to the `ToolInvocation` class (or to `@CopilotTool` annotation Javadoc) stating that `ToolInvocation` may appear at any position in the parameter list and is always excluded from the tool schema regardless of position. + + 2. Update the Java README `ToolInvocation` section to mention that position does not matter — it can appear before, between, or after `@Param`-annotated parameters. + + 3. Add tests proving position independence: + + - `ToolInvocation` as the first parameter: + + ``` + @CopilotTool("...") + public String report(ToolInvocation invocation, @Param("Phase") String phase) { ... } + ``` + + - `ToolInvocation` as the only parameter (zero visible args): + + ``` + @CopilotTool("...") + public String onlyContext(ToolInvocation invocation) { ... } + ``` + + - `ToolInvocation` in the middle of two visible params: + + ``` + @CopilotTool("...") + public String report(@Param("Phase") String phase, ToolInvocation invocation, @Param("Limit") int limit) { ... } + ``` + + Each test should verify both that the schema excludes `ToolInvocation` and that the handler receives the correct invocation context. Minor correctness concern: @@ -2614,8 +2643,18 @@ Please: public String report(@Param("Phase") String phase, ToolInvocation invocation, @Param("Limit") int limit) { ... } ``` -Each test should verify both that the schema excludes `ToolInvocation` and that the handler receives the correct invocation context. + Each test should verify both that the schema excludes `ToolInvocation` and that the handler receives the correct invocation context. + +### 19:24 Prompt + +While working on Feature #1810, we had cause to request the global-search-and-replace renaming of the class `./src/main/java/com/github/copilot/tool/Param.java` to be `CopilotToolParam` instead. + +The reason is that I want to use `Param` for the `tool-as-lambda` variant of the API. See https://github.com/github/copilot-sdk/blob/edburns/1810-java-tool-ergonomics-tool-as-lambda/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md , where you will see the text +--- +Assume current annotation class `com.github.copilot.tool.Param` is renamed to `CopilotToolParam`. +This issue requests the implementation of that rename. +--- From f77da2f291c5fb6d042b7fd9643dbf75df7be672 Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Mon, 29 Jun 2026 20:14:17 -0400 Subject: [PATCH 09/13] On branch edburns/1810-java-tool-ergonomics-tool-as-lambda modified: 1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md - Check off the things already done. modified: 1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md - GUTDODP --- ...rance-reduction-for-implementation-plan.md | 4 +-- .../20260629-prompts.md | 28 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md index 98b294ffa..de896c5f1 100644 --- a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md +++ b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md @@ -17,7 +17,7 @@ Issue: #1810 --- -## Phase 2 — Baseline verification before new API work +## Phase 2 ✅ — Baseline verification before new API work This phase confirms the current runtime behavior we must preserve. @@ -43,7 +43,7 @@ Use existing tests and code as ground truth: --- -## Phase 3 — Ignorance reduction: questions to answer before writing code +## Phase 3 ✅ — Ignorance reduction: questions to answer before writing code Resolve these unknowns before production edits. diff --git a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md index c8e8e1db9..f832c4728 100644 --- a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md +++ b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/20260629-prompts.md @@ -2658,3 +2658,31 @@ Assume current annotation class `com.github.copilot.tool.Param` is renamed to `C This issue requests the implementation of that rename. --- + +## ghcp-sdk-1817 + +Carefully read the plan `copilot-sdk/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md`. + +We have completed Phases 1 - 3. We are now on Phase 4. + +For each sub item in Phase 4, file a child issue of Feature #1810. + +Use `gh` to look at Epic #1682 and its child issues for the standard of verbosity and concreteness for issue descriptions. + +The issue should be assignable to a coding agent and there must be sufficient detail in the description for the agent to proceed to completion. + +Pull out the specific relevant content from Phase 3 as it applies to this current issue in Phase 4. + +Tell the agent to take `edburns/1810-java-tool-ergonomics-tool-as-lambda` as the base branch. When I assign the issue to the coding agent, I will set this as the base branch. + +Tell the agent to read `1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md`, which is present on the branch, for context. + +Tell the agent which sub-item within Phase 4 they are solving. + +Tell the agent that each issue will be assigned, resolved and merged to the base branch in the order in which they appear in the plan. + +Tell the agent to update the check boxes in their phase in `1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md` as they complete their work. + +No work is to start until I assign each issue to the coding agent, in turn. + +Once you have filed all the issues, update `1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md` Phase 4 with progress checkboxes the agents can check off as they work. From fb4049fd3cb7707a5193a07c5da87f3ea636117d Mon Sep 17 00:00:00 2001 From: Ed Burns Date: Mon, 29 Jun 2026 20:19:02 -0400 Subject: [PATCH 10/13] Add Phase 4 checklist linked to child issues Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../1810-ignorance-reduction-for-implementation-plan.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md index de896c5f1..32f0d2e56 100644 --- a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md +++ b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md @@ -625,6 +625,15 @@ Yes. start with existing tool-definition snapshot; only add a new YAML if wire t After Phase 3 is resolved, implement in this order. +### Phase 4 progress checklist + +- [ ] 4.1 — Add public API types ([#1839](https://github.com/github/copilot-sdk/issues/1839)) +- [ ] 4.2 — Implement `ToolDefinition.from(...)` overloads ([#1840](https://github.com/github/copilot-sdk/issues/1840)) +- [ ] 4.3 — Implement schema and coercion internals ([#1841](https://github.com/github/copilot-sdk/issues/1841)) +- [ ] 4.4 — Unit tests for API behavior and validation ([#1842](https://github.com/github/copilot-sdk/issues/1842)) +- [ ] 4.5 — E2E integration test ([#1843](https://github.com/github/copilot-sdk/issues/1843)) +- [ ] 4.6 — Documentation updates ([#1844](https://github.com/github/copilot-sdk/issues/1844)) + ### 4.1 — Add public API types **What:** Introduce functional interfaces and parameter metadata classes for inline tools. From d9d438fc3d589d3f4bd32584a15083b66614523e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Jun 2026 00:44:54 +0000 Subject: [PATCH 11/13] Initial plan From 4d4b0751b6406ce893a5ccfabd938e4e59dd851c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Jun 2026 00:50:35 +0000 Subject: [PATCH 12/13] Add Param public API type for lambda-defined tools (#1839) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce `com.github.copilot.tool.Param` — an immutable, fluent runtime parameter metadata class for inline/lambda tool definitions. Validation behavior: - Rejects blank name/description - Rejects required=true with non-empty defaultValue - Validates default values against declared Class Includes comprehensive unit tests (ParamTest, 24 cases). Updates Phase 4.1 checkbox in the implementation plan. --- ...rance-reduction-for-implementation-plan.md | 2 +- .../java/com/github/copilot/tool/Param.java | 242 +++++++++++++++++ .../com/github/copilot/tool/ParamTest.java | 249 ++++++++++++++++++ 3 files changed, 492 insertions(+), 1 deletion(-) create mode 100644 java/src/main/java/com/github/copilot/tool/Param.java create mode 100644 java/src/test/java/com/github/copilot/tool/ParamTest.java diff --git a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md index 32f0d2e56..d0803f7a7 100644 --- a/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md +++ b/1810-java-tool-ergonomics-tool-as-lambda-remove-before-merge/1810-ignorance-reduction-for-implementation-plan.md @@ -627,7 +627,7 @@ After Phase 3 is resolved, implement in this order. ### Phase 4 progress checklist -- [ ] 4.1 — Add public API types ([#1839](https://github.com/github/copilot-sdk/issues/1839)) +- [x] 4.1 — Add public API types ([#1839](https://github.com/github/copilot-sdk/issues/1839)) - [ ] 4.2 — Implement `ToolDefinition.from(...)` overloads ([#1840](https://github.com/github/copilot-sdk/issues/1840)) - [ ] 4.3 — Implement schema and coercion internals ([#1841](https://github.com/github/copilot-sdk/issues/1841)) - [ ] 4.4 — Unit tests for API behavior and validation ([#1842](https://github.com/github/copilot-sdk/issues/1842)) diff --git a/java/src/main/java/com/github/copilot/tool/Param.java b/java/src/main/java/com/github/copilot/tool/Param.java new file mode 100644 index 000000000..656b92a59 --- /dev/null +++ b/java/src/main/java/com/github/copilot/tool/Param.java @@ -0,0 +1,242 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.tool; + +import java.util.Objects; + +import com.github.copilot.CopilotExperimental; + +/** + * Runtime parameter metadata for lambda-defined tools. + * + *

+ * Each {@code Param} instance describes a single parameter that a tool accepts, + * including its Java type, wire name, description, whether it is required, and + * an optional default value. Instances are immutable; fluent mutators return new + * copies. + * + *

Example Usage

+ * + *
{@code
+ * Param query = Param.of(String.class, "query", "Search query text");
+ *
+ * Param limit = Param.of(Integer.class, "limit", "Max results", false, "10");
+ * }
+ * + * @param the Java type of the parameter value + * @since 1.0.2 + */ +@CopilotExperimental +public final class Param { + + private final Class type; + private final String name; + private final String description; + private final boolean required; + private final String defaultValue; + + private Param(Class type, String name, String description, boolean required, String defaultValue) { + this.type = Objects.requireNonNull(type, "type"); + this.name = requireNonBlank(name, "name"); + this.description = requireNonBlank(description, "description"); + this.defaultValue = defaultValue == null ? "" : defaultValue; + this.required = required; + + if (this.required && !this.defaultValue.isEmpty()) { + throw new IllegalArgumentException("required=true cannot be combined with a non-empty defaultValue"); + } + + validateDefaultValue(type, this.defaultValue); + } + + /** + * Creates a required parameter with no default value. + * + * @param the parameter type + * @param type the Java class of the parameter + * @param name the wire name sent to the model (must not be blank) + * @param description a human-readable description (must not be blank) + * @return a new {@code Param} instance + * @throws NullPointerException if {@code type} is null + * @throws IllegalArgumentException if {@code name} or {@code description} is blank + */ + public static Param of(Class type, String name, String description) { + return new Param<>(type, name, description, true, ""); + } + + /** + * Creates a parameter with explicit required/default settings. + * + * @param the parameter type + * @param type the Java class of the parameter + * @param name the wire name sent to the model (must not be blank) + * @param description a human-readable description (must not be blank) + * @param required whether the parameter is required + * @param defaultValue the default value as a string, or {@code null}/empty for none + * @return a new {@code Param} instance + * @throws NullPointerException if {@code type} is null + * @throws IllegalArgumentException if validation fails + */ + public static Param of(Class type, String name, String description, boolean required, + String defaultValue) { + return new Param<>(type, name, description, required, defaultValue); + } + + /** + * Returns a copy with a different name. + * + * @param name the new parameter name + * @return a new {@code Param} with the updated name + */ + public Param name(String name) { + return new Param<>(this.type, name, this.description, this.required, this.defaultValue); + } + + /** + * Returns a copy with a different description. + * + * @param description the new description + * @return a new {@code Param} with the updated description + */ + public Param description(String description) { + return new Param<>(this.type, this.name, description, this.required, this.defaultValue); + } + + /** + * Returns a copy with a different required flag. + * + * @param required whether the parameter is required + * @return a new {@code Param} with the updated required flag + */ + public Param required(boolean required) { + return new Param<>(this.type, this.name, this.description, required, this.defaultValue); + } + + /** + * Returns an optional copy with the given default value. Setting a default + * implicitly makes the parameter optional ({@code required=false}). + * + * @param defaultValue the default value as a string + * @return a new {@code Param} with the default applied and required set to false + */ + public Param defaultValue(String defaultValue) { + return new Param<>(this.type, this.name, this.description, false, defaultValue); + } + + /** Returns the Java type of this parameter. */ + public Class type() { + return type; + } + + /** Returns the wire name of this parameter. */ + public String name() { + return name; + } + + /** Returns the human-readable description. */ + public String description() { + return description; + } + + /** Returns whether this parameter is required. */ + public boolean required() { + return required; + } + + /** Returns the default value string, or empty if none. */ + public String defaultValue() { + return defaultValue; + } + + /** Returns {@code true} if a non-empty default value is set. */ + public boolean hasDefaultValue() { + return !defaultValue.isEmpty(); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Param other)) { + return false; + } + return required == other.required && Objects.equals(type, other.type) + && Objects.equals(name, other.name) && Objects.equals(description, other.description) + && Objects.equals(defaultValue, other.defaultValue); + } + + @Override + public int hashCode() { + return Objects.hash(type, name, description, required, defaultValue); + } + + @Override + public String toString() { + return "Param[name=" + name + ", type=" + type.getSimpleName() + ", required=" + required + "]"; + } + + // ------------------------------------------------------------------ + // Internal validation helpers + // ------------------------------------------------------------------ + + private static String requireNonBlank(String value, String fieldName) { + if (value == null || value.isBlank()) { + throw new IllegalArgumentException(fieldName + " must not be null or blank"); + } + return value; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private static void validateDefaultValue(Class type, String defaultValue) { + if (defaultValue == null || defaultValue.isEmpty()) { + return; + } + + try { + if (type == String.class) { + return; + } + if (type == Integer.class || type == int.class) { + Integer.parseInt(defaultValue); + return; + } + if (type == Long.class || type == long.class) { + Long.parseLong(defaultValue); + return; + } + if (type == Double.class || type == double.class) { + Double.parseDouble(defaultValue); + return; + } + if (type == Float.class || type == float.class) { + Float.parseFloat(defaultValue); + return; + } + if (type == Short.class || type == short.class) { + Short.parseShort(defaultValue); + return; + } + if (type == Byte.class || type == byte.class) { + Byte.parseByte(defaultValue); + return; + } + if (type == Boolean.class || type == boolean.class) { + if (!"true".equalsIgnoreCase(defaultValue) && !"false".equalsIgnoreCase(defaultValue)) { + throw new IllegalArgumentException("must be 'true' or 'false'"); + } + return; + } + if (type.isEnum()) { + Class enumType = (Class) type; + Enum.valueOf(enumType, defaultValue); + return; + } + } catch (RuntimeException ex) { + throw new IllegalArgumentException( + "defaultValue '" + defaultValue + "' is not valid for type " + type.getSimpleName(), ex); + } + + throw new IllegalArgumentException( + "defaultValue is not supported for type " + type.getName() + " without a custom coercion policy"); + } +} diff --git a/java/src/test/java/com/github/copilot/tool/ParamTest.java b/java/src/test/java/com/github/copilot/tool/ParamTest.java new file mode 100644 index 000000000..a8ebccdc5 --- /dev/null +++ b/java/src/test/java/com/github/copilot/tool/ParamTest.java @@ -0,0 +1,249 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.tool; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link Param} runtime parameter metadata. + */ +public class ParamTest { + + // ------------------------------------------------------------------ + // Factory method: of(type, name, description) + // ------------------------------------------------------------------ + + @Test + void ofCreatesRequiredParamWithNoDefault() { + Param p = Param.of(String.class, "query", "Search query"); + assertEquals(String.class, p.type()); + assertEquals("query", p.name()); + assertEquals("Search query", p.description()); + assertTrue(p.required()); + assertEquals("", p.defaultValue()); + assertFalse(p.hasDefaultValue()); + } + + @Test + void ofFullFactoryCreatesOptionalParamWithDefault() { + Param p = Param.of(Integer.class, "limit", "Max results", false, "10"); + assertEquals(Integer.class, p.type()); + assertEquals("limit", p.name()); + assertEquals("Max results", p.description()); + assertFalse(p.required()); + assertEquals("10", p.defaultValue()); + assertTrue(p.hasDefaultValue()); + } + + // ------------------------------------------------------------------ + // Validation: blank name/description rejected + // ------------------------------------------------------------------ + + @Test + void rejectsNullName() { + var ex = assertThrows(IllegalArgumentException.class, + () -> Param.of(String.class, null, "desc")); + assertTrue(ex.getMessage().contains("name")); + } + + @Test + void rejectsBlankName() { + var ex = assertThrows(IllegalArgumentException.class, + () -> Param.of(String.class, " ", "desc")); + assertTrue(ex.getMessage().contains("name")); + } + + @Test + void rejectsNullDescription() { + var ex = assertThrows(IllegalArgumentException.class, + () -> Param.of(String.class, "n", null)); + assertTrue(ex.getMessage().contains("description")); + } + + @Test + void rejectsBlankDescription() { + var ex = assertThrows(IllegalArgumentException.class, + () -> Param.of(String.class, "n", "")); + assertTrue(ex.getMessage().contains("description")); + } + + // ------------------------------------------------------------------ + // Validation: required=true with non-empty default rejected + // ------------------------------------------------------------------ + + @Test + void rejectsRequiredWithNonEmptyDefault() { + var ex = assertThrows(IllegalArgumentException.class, + () -> Param.of(String.class, "x", "desc", true, "val")); + assertTrue(ex.getMessage().contains("required=true")); + } + + @Test + void allowsRequiredWithEmptyDefault() { + Param p = Param.of(String.class, "x", "desc", true, ""); + assertTrue(p.required()); + assertFalse(p.hasDefaultValue()); + } + + @Test + void allowsRequiredWithNullDefault() { + Param p = Param.of(String.class, "x", "desc", true, null); + assertTrue(p.required()); + assertEquals("", p.defaultValue()); + } + + // ------------------------------------------------------------------ + // Validation: default value type checking + // ------------------------------------------------------------------ + + @Test + void validatesIntegerDefault() { + // valid + Param p = Param.of(Integer.class, "n", "num", false, "42"); + assertEquals("42", p.defaultValue()); + + // invalid + assertThrows(IllegalArgumentException.class, + () -> Param.of(Integer.class, "n", "num", false, "abc")); + } + + @Test + void validatesLongDefault() { + Param p = Param.of(Long.class, "n", "num", false, "999999999999"); + assertEquals("999999999999", p.defaultValue()); + + assertThrows(IllegalArgumentException.class, + () -> Param.of(Long.class, "n", "num", false, "notlong")); + } + + @Test + void validatesDoubleDefault() { + Param p = Param.of(Double.class, "d", "decimal", false, "3.14"); + assertEquals("3.14", p.defaultValue()); + + assertThrows(IllegalArgumentException.class, + () -> Param.of(Double.class, "d", "decimal", false, "xyz")); + } + + @Test + void validatesBooleanDefault() { + Param p1 = Param.of(Boolean.class, "b", "flag", false, "true"); + assertEquals("true", p1.defaultValue()); + + Param p2 = Param.of(Boolean.class, "b", "flag", false, "FALSE"); + assertEquals("FALSE", p2.defaultValue()); + + assertThrows(IllegalArgumentException.class, + () -> Param.of(Boolean.class, "b", "flag", false, "yes")); + } + + @Test + void validatesEnumDefault() { + Param p = Param.of(TestEnum.class, "e", "enum val", false, "ALPHA"); + assertEquals("ALPHA", p.defaultValue()); + + assertThrows(IllegalArgumentException.class, + () -> Param.of(TestEnum.class, "e", "enum val", false, "INVALID")); + } + + @Test + void rejectsUnsupportedTypeWithDefault() { + assertThrows(IllegalArgumentException.class, + () -> Param.of(Object.class, "o", "object", false, "something")); + } + + @Test + void allowsStringDefault() { + Param p = Param.of(String.class, "s", "string", false, "hello"); + assertEquals("hello", p.defaultValue()); + } + + // ------------------------------------------------------------------ + // Fluent mutators return new instances + // ------------------------------------------------------------------ + + @Test + void nameMutatorReturnsNewInstance() { + Param original = Param.of(String.class, "a", "desc"); + Param renamed = original.name("b"); + assertEquals("a", original.name()); + assertEquals("b", renamed.name()); + } + + @Test + void descriptionMutatorReturnsNewInstance() { + Param original = Param.of(String.class, "a", "desc1"); + Param updated = original.description("desc2"); + assertEquals("desc1", original.description()); + assertEquals("desc2", updated.description()); + } + + @Test + void requiredMutatorReturnsNewInstance() { + Param original = Param.of(String.class, "a", "desc"); + Param optional = original.required(false); + assertTrue(original.required()); + assertFalse(optional.required()); + } + + @Test + void defaultValueMutatorSetsOptional() { + Param original = Param.of(String.class, "a", "desc"); + Param withDefault = original.defaultValue("val"); + assertTrue(original.required()); + assertFalse(withDefault.required()); + assertEquals("val", withDefault.defaultValue()); + assertTrue(withDefault.hasDefaultValue()); + } + + // ------------------------------------------------------------------ + // equals / hashCode / toString + // ------------------------------------------------------------------ + + @Test + void equalParamsAreEqual() { + Param a = Param.of(String.class, "x", "desc"); + Param b = Param.of(String.class, "x", "desc"); + assertEquals(a, b); + assertEquals(a.hashCode(), b.hashCode()); + } + + @Test + void differentParamsAreNotEqual() { + Param a = Param.of(String.class, "x", "desc"); + Param b = Param.of(String.class, "y", "desc"); + assertNotEquals(a, b); + } + + @Test + void toStringContainsName() { + Param p = Param.of(String.class, "query", "Search"); + assertTrue(p.toString().contains("query")); + assertTrue(p.toString().contains("String")); + } + + // ------------------------------------------------------------------ + // Null type rejected + // ------------------------------------------------------------------ + + @Test + void rejectsNullType() { + assertThrows(NullPointerException.class, () -> Param.of(null, "n", "desc")); + } + + // ------------------------------------------------------------------ + // Test enum for validation tests + // ------------------------------------------------------------------ + + enum TestEnum { + ALPHA, BETA + } +} From 35b7aad56c0a64af9d37c5cc1aa20dd5911b577a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Jun 2026 00:52:57 +0000 Subject: [PATCH 13/13] Add Float/Short/Byte default validation test coverage for Param --- .../com/github/copilot/tool/ParamTest.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/java/src/test/java/com/github/copilot/tool/ParamTest.java b/java/src/test/java/com/github/copilot/tool/ParamTest.java index a8ebccdc5..a5b1337d0 100644 --- a/java/src/test/java/com/github/copilot/tool/ParamTest.java +++ b/java/src/test/java/com/github/copilot/tool/ParamTest.java @@ -133,6 +133,33 @@ void validatesDoubleDefault() { () -> Param.of(Double.class, "d", "decimal", false, "xyz")); } + @Test + void validatesFloatDefault() { + Param p = Param.of(Float.class, "f", "float val", false, "1.5"); + assertEquals("1.5", p.defaultValue()); + + assertThrows(IllegalArgumentException.class, + () -> Param.of(Float.class, "f", "float val", false, "notfloat")); + } + + @Test + void validatesShortDefault() { + Param p = Param.of(Short.class, "s", "short val", false, "100"); + assertEquals("100", p.defaultValue()); + + assertThrows(IllegalArgumentException.class, + () -> Param.of(Short.class, "s", "short val", false, "99999")); + } + + @Test + void validatesByteDefault() { + Param p = Param.of(Byte.class, "b", "byte val", false, "127"); + assertEquals("127", p.defaultValue()); + + assertThrows(IllegalArgumentException.class, + () -> Param.of(Byte.class, "b", "byte val", false, "999")); + } + @Test void validatesBooleanDefault() { Param p1 = Param.of(Boolean.class, "b", "flag", false, "true");