Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
beeaa2a
ADR-006
edburns Jun 26, 2026
37c3bc3
Plan
edburns Jun 26, 2026
f98bd15
GUTDODP
edburns Jun 26, 2026
88ab0b4
GUTDODP
edburns Jun 29, 2026
ae642e4
On branch edburns/1810-java-tool-ergonomics-tool-as-lambda GOTDODP
edburns Jun 29, 2026
6bb0044
On branch edburns/1810-java-tool-ergonomics-tool-as-lambda Completed …
edburns Jun 29, 2026
ddf691e
GUTDODP
edburns Jun 29, 2026
4fdb0c1
GUTDODP
edburns Jun 29, 2026
b13df29
On branch edburns/1810-java-tool-ergonomics-tool-as-lambda
edburns Jun 30, 2026
b9085ef
Add Phase 4 checklist linked to child issues
edburns Jun 30, 2026
bd839df
Initial plan
Copilot Jun 30, 2026
9df0462
Add Param<T> public API type for lambda-defined tools (#1839)
Copilot Jun 30, 2026
822e2af
Add Float/Short/Byte default validation test coverage for Param<T>
Copilot Jun 30, 2026
32301bb
GUTDODP
edburns Jun 30, 2026
6f3d4f3
Add shepherd-task-to-ready skill
edburns Jun 30, 2026
044d471
Iterate the skill
edburns Jun 30, 2026
166b7c0
Iterate skill
edburns Jun 30, 2026
c3c50af
Spotless
edburns Jun 30, 2026
cb9abc8
Mark 4.1 as complete
edburns Jun 30, 2026
8c94478
Update shepherd skill: rerun for approval, ignore expected failure, f…
edburns Jun 30, 2026
57a7403
Skill: prepend base branch instruction before assigning to Copilot
edburns Jun 30, 2026
58e1cca
Iterate the skill
edburns Jun 30, 2026
adb3695
Skill: add guard against editing plan/checklist files
edburns Jun 30, 2026
e1ea5ba
Skill: clarify checklist editing is out of scope, not DRI-specific
edburns Jun 30, 2026
d061214
GUTDODP
edburns Jun 30, 2026
128a8da
Rename skill: shepherd-task-to-ready → shepherd-task-from-assignment-…
edburns Jun 30, 2026
2806a6a
Working to prompt the second skill.
edburns Jun 30, 2026
8662d61
Add shepherd-task-from-ready-to-merged-to-base skill
edburns Jun 30, 2026
5031181
Find the PR
edburns Jun 30, 2026
acdce2a
feat(java): implement ToolDefinition.from* lambda overloads (Phase 4.…
Copilot Jun 30, 2026
083311c
Skill: add GraphQL thread resolution to Step 8
edburns Jun 30, 2026
47fe339
On branch edburns/1810-java-tool-ergonomics-tool-as-lambda
edburns Jun 30, 2026
6d56412
On branch edburns/1810-java-tool-ergonomics-tool-as-lambda
edburns Jun 30, 2026
0015d55
Add shepherd-task skill: end-to-end orchestrator
edburns Jun 30, 2026
0e956fa
On branch edburns/1810-java-tool-ergonomics-tool-as-lambda
edburns Jun 30, 2026
c35bc1a
Invoke skill result
edburns Jul 1, 2026
e990f1b
fix: use --body-file to preserve markdown formatting when prepending …
edburns Jul 1, 2026
0fc4293
fix: add idempotency guard for base branch prepend in shepherd skill
edburns Jul 1, 2026
dc16398
feat: add /compact between Phase 1 and Phase 2 in shepherd-task skill
edburns Jul 1, 2026
775955a
feat(java): implement ParamSchema + ParamCoercion internals for Param…
Copilot Jul 1, 2026
70f3ce5
fix(skills): improve PR discovery with multi-strategy polling
edburns Jul 1, 2026
705610d
feat: add shepherd-task shell scripts (PowerShell + bash)
edburns Jul 1, 2026
70eef6a
[Java] Tool-as-lambda 4.4: Add unit tests for API behavior and valida…
Copilot Jul 1, 2026
3ed4934
fix: strip remote prefix when comparing merged base branch name
edburns Jul 1, 2026
930aa15
On branch edburns/1810-java-tool-ergonomics-tool-as-lambda
edburns Jul 1, 2026
c1b23d5
[Java] Add replay-proxy E2E coverage for inline lambda-defined tools …
Copilot Jul 1, 2026
d4f64f0
Extract workflow approval into reusable sub-skill
edburns Jul 1, 2026
f03bf62
Add workflow approval calls to ready-to-merged skill
edburns Jul 1, 2026
aff9c71
On branch edburns/1810-java-tool-ergonomics-tool-as-lambda
edburns Jul 2, 2026
c431dc2
Use agent_assignment API to guarantee base branch on Copilot assignment
edburns Jul 2, 2026
8ac5e56
Revert "Use agent_assignment API to guarantee base branch on Copilot …
edburns Jul 2, 2026
09924fd
Strengthen base branch enforcement for Copilot assignment
edburns Jul 2, 2026
e7bd4e4
On branch edburns/1810-java-tool-ergonomics-tool-as-lambda
edburns Jul 2, 2026
88281ef
[Java] Align inline tool docs with final lambda API and ADR links (#1…
Copilot Jul 2, 2026
0436a78
Removing these files from this topic branch
edburns Jul 2, 2026
663c34e
Remove prompts before seeking review
edburns Jul 2, 2026
d91372a
java: delegate ToolDefinition schema/coercion to ParamSchema and Para…
edburns Jul 2, 2026
98f8814
java: cache getConfiguredMapper() in local variable per invocation
edburns Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions java/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,73 @@ public String onlyContext(ToolInvocation invocation) { ... }
public String report(@CopilotToolParam("Phase") String phase, ToolInvocation invocation, @CopilotToolParam("Limit") int limit) { ... }
```

## Inline lambda tool definitions (experimental)

For inline tool authoring at the session construction site, use `ToolDefinition.from(...)` with explicit parameter metadata:

```java
import com.github.copilot.rpc.ToolDefinition;
import com.github.copilot.rpc.ToolDefer;
import com.github.copilot.tool.Param;

ToolDefinition search = ToolDefinition
.from(
"search_items",
"Searches indexed items by keyword",
Param.of(String.class, "keyword", "Search keyword"),
keyword -> "Searching for: " + keyword)
.skipPermission(true)
.defer(ToolDefer.AUTO);
```

### Parameter metadata with `Param.of(...)`

`Param.of(type, name, description)` creates a required parameter. For optional parameters with defaults:

```java
Param<Integer> limit = Param.of(Integer.class, "limit", "Max results", false, "10");
```

### Async handlers

Use `fromAsync` for asynchronous tool handlers:

```java
import java.util.concurrent.CompletableFuture;

ToolDefinition fetchData = ToolDefinition.fromAsync(
"fetch_data",
"Fetches data from remote source",
Param.of(String.class, "url", "Data source URL"),
url -> CompletableFuture.supplyAsync(() -> fetchRemote(url))
);
```

### ToolInvocation context injection

Inline tools can access `ToolInvocation` runtime context using `fromWithToolInvocation`:

```java
ToolDefinition reportPhase = 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()
);
```

For async with `ToolInvocation`, use `fromAsyncWithToolInvocation`.

### Fluent option modifiers

Chain fluent modifiers to set tool options:

- `.skipPermission(boolean)` — bypass permission prompts
- `.defer(ToolDefer)` — control deferred execution (`AUTO`, `NEVER`)
- `.overridesBuiltInTool(boolean)` — shadow built-in tools

For design context and decision rationale, see [ADR-006](docs/adr/adr-006-tool-definition-inline.md).

## Memory

Sessions can opt into persistent memory, allowing the agent to read and write memory across turns. Memory is configured per session and applies to both `createSession` and `resumeSession`.
Expand Down
118 changes: 118 additions & 0 deletions java/docs/adr/adr-006-tool-definition-inline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# ADR-006: Inline tool definition with lambdas

## Context and problem statement

[ADR-005](adr-005-tool-definition.md) introduced an ergonomic Java tools API based on `@CopilotTool` method annotations, `@CopilotToolParam` parameter annotations, and `ToolDefinition.fromObject(...)` for reflection-based tool registration. 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",
Param.of(String.class, "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 `Param.of(...)` parameter definitions.

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<R>`).
* 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

The SDK now provides an explicit inline path for developers who prefer to keep tool declarations at session creation while preserving high-quality schema metadata. Implemented API families include:

- `ToolDefinition.from(name, description, [params...], handler)` — sync handlers
- `ToolDefinition.fromAsync(name, description, [params...], asyncHandler)` — async handlers returning `CompletableFuture<R>`
- `ToolDefinition.fromWithToolInvocation(...)` — sync with `ToolInvocation` context injection
- `ToolDefinition.fromAsyncWithToolInvocation(...)` — async with `ToolInvocation` context injection

Parameter metadata is defined using `Param.of(type, name, description)` for required parameters and `Param.of(type, name, description, required, defaultValue)` for optional parameters with defaults.

Fluent option modifiers (`.skipPermission(boolean)`, `.defer(ToolDefer)`, `.overridesBuiltInTool(boolean)`) allow post-construction customization.

The annotation-driven API from [ADR-005](adr-005-tool-definition.md) remains the recommended path for larger tool surfaces where co-locating metadata with method implementations improves maintainability. For usage examples and complete API coverage, see the Java SDK README.

## Related work items

* #1682
* #1792
* #1810
197 changes: 197 additions & 0 deletions java/src/main/java/com/github/copilot/rpc/ParamCoercion.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/

package com.github.copilot.rpc;

import java.util.Map;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.copilot.tool.Param;

/**
* Internal runtime helper: coerces raw invocation arguments to the typed values
* declared by {@link Param} descriptors.
*
* <p>
* Reuses the SDK-configured {@link ObjectMapper} for complex type conversions,
* matching the coercion policy applied by existing ergonomic tooling. No
* bespoke conversion paths are introduced.
*
* <p>
* Package-private: not part of the public API.
*/
class ParamCoercion {

/** Utility class; do not instantiate. */
private ParamCoercion() {
}

/**
* Coerces the named argument from an invocation argument map to the Java type
* declared by {@code param}.
*
* <p>
* Resolution order:
* <ol>
* <li>If the argument is present, convert it to {@code T} via
* {@link ObjectMapper#convertValue}.</li>
* <li>If absent and a default value is set, parse the string default via
* {@link #coerceDefault}.</li>
* <li>If absent and the parameter is optional ({@code required=false}), return
* an empty Optional variant or {@code null}.</li>
* <li>If absent and required, throw {@link IllegalArgumentException} with the
* parameter name.</li>
* </ol>
*
* @param <T>
* the target Java type
* @param args
* the invocation argument map; may be {@code null} for zero-argument
* tools
* @param param
* the parameter descriptor
* @param mapper
* the configured {@link ObjectMapper} for complex type conversion
* @return the coerced argument value
* @throws IllegalArgumentException
* if a required parameter is missing or coercion fails
*/
@SuppressWarnings("unchecked")
static <T> T coerce(Map<String, Object> args, Param<T> param, ObjectMapper mapper) {
Object raw = (args != null) ? args.get(param.name()) : null;

if (raw == null) {
if (param.hasDefaultValue()) {
return coerceDefault(param, mapper);
} else if (!param.required()) {
return (T) emptyOptionalOrNull(param.type());
} else {
throw new IllegalArgumentException(
"Required parameter '" + param.name() + "' is missing from tool invocation");
}
}

Class<T> type = param.type();

// Handle Optional* types explicitly before delegating to ObjectMapper
if (type == java.util.OptionalInt.class) {
try {
return (T) java.util.OptionalInt.of(((Number) raw).intValue());
} catch (ClassCastException ex) {
throw new IllegalArgumentException("Parameter '" + param.name()
+ "' expected a numeric value for OptionalInt, got: " + raw.getClass().getSimpleName(), ex);
}
}
if (type == java.util.OptionalLong.class) {
try {
return (T) java.util.OptionalLong.of(((Number) raw).longValue());
} catch (ClassCastException ex) {
throw new IllegalArgumentException("Parameter '" + param.name()
+ "' expected a numeric value for OptionalLong, got: " + raw.getClass().getSimpleName(), ex);
}
}
if (type == java.util.OptionalDouble.class) {
try {
return (T) java.util.OptionalDouble.of(((Number) raw).doubleValue());
} catch (ClassCastException ex) {
throw new IllegalArgumentException("Parameter '" + param.name()
+ "' expected a numeric value for OptionalDouble, got: " + raw.getClass().getSimpleName(), ex);
}
}

try {
return mapper.convertValue(raw, type);
} catch (IllegalArgumentException ex) {
throw new IllegalArgumentException(
"Failed to coerce parameter '" + param.name() + "' to type " + type.getSimpleName(), ex);
}
}

/**
* Parses a {@link Param}'s string default value into the declared Java type.
*
* <p>
* Handles primitives, boxed types, {@link String}, {@link Boolean}, and enums
* explicitly, mirroring the validation logic in {@link Param}. The
* {@link ObjectMapper#readValue} fallback exists as a safety net but is not
* expected to be reached in practice, since {@link Param} construction rejects
* defaults for non-primitive/boxed/String/Boolean/enum types.
*
* @param <T>
* the target Java type
* @param param
* the parameter descriptor carrying the default value
* @param mapper
* the configured {@link ObjectMapper} used as fallback for complex
* types
* @return the parsed default value
* @throws IllegalArgumentException
* if parsing fails
*/
@SuppressWarnings({"rawtypes", "unchecked"})
static <T> T coerceDefault(Param<T> param, ObjectMapper mapper) {
String defaultValue = param.defaultValue();
Class<T> type = param.type();
try {
if (type == String.class) {
return type.cast(defaultValue);
}
if (type == Integer.class || type == int.class) {
return (T) Integer.valueOf(defaultValue);
}
if (type == Long.class || type == long.class) {
return (T) Long.valueOf(defaultValue);
}
if (type == Double.class || type == double.class) {
return (T) Double.valueOf(defaultValue);
}
if (type == Float.class || type == float.class) {
return (T) Float.valueOf(defaultValue);
}
if (type == Short.class || type == short.class) {
return (T) Short.valueOf(defaultValue);
}
if (type == Byte.class || type == byte.class) {
return (T) Byte.valueOf(defaultValue);
}
if (type == Boolean.class || type == boolean.class) {
return (T) Boolean.valueOf(defaultValue);
}
if (type.isEnum()) {
Class<? extends Enum> enumType = (Class<? extends Enum>) type;
return type.cast(Enum.valueOf(enumType, defaultValue));
}
// Fallback: let ObjectMapper parse the JSON-encoded default string
return mapper.readValue(defaultValue, type);
} catch (IllegalArgumentException ex) {
throw ex;
} catch (Exception ex) {
throw new IllegalArgumentException("Failed to apply default value '" + defaultValue + "' for parameter '"
+ param.name() + "' of type " + type.getSimpleName(), ex);
}
}

/**
* Returns an empty Optional variant for Optional primitive types, or
* {@code null} for all other types.
*
* @param type
* the declared parameter type
* @return {@link java.util.OptionalInt#empty()},
* {@link java.util.OptionalLong#empty()},
* {@link java.util.OptionalDouble#empty()}, or {@code null}
*/
static Object emptyOptionalOrNull(Class<?> type) {
if (type == java.util.OptionalInt.class) {
return java.util.OptionalInt.empty();
}
if (type == java.util.OptionalLong.class) {
return java.util.OptionalLong.empty();
}
if (type == java.util.OptionalDouble.class) {
return java.util.OptionalDouble.empty();
}
return null;
}
}
Loading
Loading