[Java] Support hidden ToolInvocation injection in @CopilotTool methods#1832
Conversation
Co-authored-by: edburns <75821+edburns@users.noreply.github.com>
Co-authored-by: edburns <75821+edburns@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
Pull request overview
Adds support in the Java annotation-processor-based tools API for injecting com.github.copilot.rpc.ToolInvocation as hidden runtime context in @CopilotTool methods, ensuring it is not exposed as part of the tool’s JSON schema while still being passed to the user method at invocation time.
Changes:
- Updated
CopilotToolProcessorto recognizeToolInvocationparameters as injected context: excluded from schema generation/required list, not read frominvocation.getArguments(), and wired into generated method calls. - Added compile-time validation for invalid
ToolInvocationusage (duplicate parameters, or@Paramapplied toToolInvocation). - Added processor tests and runtime
ToolDefinition.fromObject/fromClassfixture-based tests to verify schema output and invocation behavior; updated Java README docs with usage guidance.
Show a summary per file
| File | Description |
|---|---|
| java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java | Implements ToolInvocation as injected (non-schema) context parameter; adds validation and updates schema/lambda generation accordingly. |
| java/src/test/java/com/github/copilot/tool/CopilotToolProcessorTest.java | Adds processor-level tests covering schema exclusion, method-call wiring (instance/static/async), and validation errors. |
| java/src/test/java/com/github/copilot/rpc/ToolDefinitionFromObjectTest.java | Adds runtime tests ensuring ToolDefinition.fromObject/fromClass correctly injects ToolInvocation and that schema excludes it. |
| java/src/test/java/com/github/copilot/rpc/fixtures/InvocationAwareTools.java | Adds instance/async tool fixtures that consume injected ToolInvocation. |
| java/src/test/java/com/github/copilot/rpc/fixtures/InvocationAwareTools$$CopilotToolMeta.java | Hand-written metadata fixture mirroring expected processor output for injected ToolInvocation (instance + async). |
| java/src/test/java/com/github/copilot/rpc/fixtures/StaticInvocationTools.java | Adds static tool fixture that consumes injected ToolInvocation. |
| java/src/test/java/com/github/copilot/rpc/fixtures/StaticInvocationTools$$CopilotToolMeta.java | Hand-written metadata fixture mirroring expected processor output for injected ToolInvocation (static). |
| java/README.md | Documents annotation-based ToolInvocation injection semantics with an example. |
Review details
- Files reviewed: 8/8 changed files
- Comments generated: 0
- Review effort level: Low
This comment has been minimized.
This comment has been minimized.
edburns
left a comment
There was a problem hiding this comment.
Overall: solid, correct implementation. The PR cleanly implements ToolInvocation injection by:
- Filtering it from schema generation
- Passing
invocationdirectly in generated method calls - 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:
@Paramon ToolInvocation - Processor-level tests verify generated code shape
FIX THESE GAPS
- 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.
ToolInvocationas 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.
- 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?
-
Test coverage for position independence.
-
Add Javadoc to the
ToolInvocationclass (or to@CopilotToolannotation Javadoc) stating thatToolInvocationmay appear at any position in the parameter list and is always excluded from the tool schema regardless of position. -
Update the Java README
ToolInvocationsection to mention that position does not matter — it can appear before, between, or after@Param-annotated parameters. -
Add tests proving position independence:
-
ToolInvocationas the first parameter:@CopilotTool("...") public String report(ToolInvocation invocation, @Param("Phase") String phase) { ... } -
ToolInvocationas the only parameter (zero visible args):@CopilotTool("...") public String onlyContext(ToolInvocation invocation) { ... } -
ToolInvocationin 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
ToolInvocationand that the handler receives the correct invocation context. -
-
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. ✅
Co-authored-by: edburns <75821+edburns@users.noreply.github.com>
Addressed in commit Added coverage for all requested gaps:
This includes both processor-level generated-shape tests and runtime fixture-based |
✅ Cross-SDK Consistency ReviewThis PR closes a genuine consistency gap in the Java annotation-based tool API rather than introducing one. Here's how
The injection semantics in this PR (parameter declared by type, excluded from LLM schema, validated at compile time) closely mirror the patterns already established in .NET (
|
Fixes #1831
This change closes the ergonomics gap in Java’s annotation-based tools API by allowing
@CopilotToolmethods to declareToolInvocationas injected runtime context.ToolInvocationis now treated as non-schema context (not LLM-visible input), while existing parameter binding and return-type behavior remain unchanged.Processor: hidden context parameter handling
CopilotToolProcessornow treatscom.github.copilot.rpc.ToolInvocationas a special injected parameter.ToolInvocationfrom generatedpropertiesandrequired.ToolInvocationfrominvocation.getArguments().ToolInvocationparameters directly to the in-scopeinvocationobject when generating method calls.Processor: compile-time validation
ToolInvocationparameter.ToolInvocationis annotated with@Param.ToolInvocationfrom participating in that shortcut path.Tests and fixtures
ToolInvocationcompile error@Param ToolInvocationcompile errorToolDefinition.fromObject/fromClasscoverage to verify runtime behavior and schema output for injectedToolInvocation.Java docs
ToolInvocationinjection semantics.Example (now supported):