diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..0159bac4d --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,54 @@ +**Please ensure you have read the [contribution guide](./CONTRIBUTING.md) before creating a pull request.** + +### Link to Issue or Description of Change + +**1. Link to an existing issue (if applicable):** + +- Closes: #_issue_number_ +- Related: #_issue_number_ + +**2. Or, if no issue exists, describe the change:** + +_If applicable, please follow the issue templates to provide as much detail as +possible._ + +**Problem:** +_A clear and concise description of what the problem is._ + +**Solution:** +_A clear and concise description of what you want to happen and why you choose +this solution._ + +### Testing Plan + +_Please describe the tests that you ran to verify your changes. This is required +for all PRs that are not small documentation or typo fixes._ + +**Unit Tests:** + +- [ ] I have added or updated unit tests for my change. +- [ ] All unit tests pass locally. + +_Please include a summary of passed java test results._ + +**Manual End-to-End (E2E) Tests:** + +_Please provide instructions on how to manually test your changes, including any +necessary setup or configuration. Please provide logs or screenshots to help +reviewers better understand the fix._ + +### Checklist + +- [ ] I have read the [CONTRIBUTING.md](./CONTRIBUTING.md) document. +- [ ] My pull request contains a single commit. +- [ ] I have performed a self-review of my own code. +- [ ] I have commented my code, particularly in hard-to-understand areas. +- [ ] I have added tests that prove my fix is effective or that my feature works. +- [ ] New and existing unit tests pass locally with my changes. +- [ ] I have manually tested my changes end-to-end. +- [ ] Any dependent changes have been merged and published in downstream modules. + +### Additional context + +_Add any other context or screenshots about the feature request here._ + diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 802e9d13f..1772e6fa2 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.0.0-rc.1" + ".": "1.0.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ef000794..c8ce38d1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,39 @@ # Changelog +## [1.0.0](https://github.com/google/adk-java/compare/v1.0.0-rc.1...v1.0.0) (2026-03-30) + + +### Features + +* add `InMemoryArtifactService` to `AgentExecutor` and update `pom.xml` dependencies ([24f8d5e](https://github.com/google/adk-java/commit/24f8d5e2562e1c0812ce6e248500797d9801fafd)) +* enabling output_schema and tools to coexist ([40ca6a7](https://github.com/google/adk-java/commit/40ca6a7c5163f711e02a54163d6066f7cd86e64d)) + + +### Bug Fixes + +* add media/image support in Spring AI MessageConverter ([8ab7f07](https://github.com/google/adk-java/commit/8ab7f072cdaa363e07b7a786044376c021c4c009)), closes [#705](https://github.com/google/adk-java/issues/705) +* add schema validation to SetModelResponseTool (issue [#587](https://github.com/google/adk-java/issues/587) already implemented, but adding tests from PR [#603](https://github.com/google/adk-java/issues/603)) ([cdc5199](https://github.com/google/adk-java/commit/cdc5199eb0f92cb95db2ee7ff139d67317968457)) +* Ensure callbackContextData is preserved across session update ([d1e05ca](https://github.com/google/adk-java/commit/d1e05caf524b7cafb3f321550659296ea70d9286)) +* **firestore:** Remove hardcoded dependency version ([6a5a55e](https://github.com/google/adk-java/commit/6a5a55eb3e531c6f8a7083712308c4800f680ca5)) +* Fixing tracing for function calls ([84dff10](https://github.com/google/adk-java/commit/84dff10a3ee7f47e30a40409e56b5e9365c69815)) +* handle null `AiMessage.text()` to prevent NPE and add unit test (PR [#1035](https://github.com/google/adk-java/issues/1035)) ([3e21e7a](https://github.com/google/adk-java/commit/3e21e7ac46b634341819b3543388a38caef85516)) +* parallel agent execution ([677b6d7](https://github.com/google/adk-java/commit/677b6d7452aa28fab42d554d18c150d59ca88eec)) +* Removing deprecated methods from Runner ([3633a7d](https://github.com/google/adk-java/commit/3633a7dd071265087ea2ff148d419969b0c888ef)) +* resolve MCP tool parsing errors in Claude integration ([5a2abbf](https://github.com/google/adk-java/commit/5a2abbfe6f9e4e1ebdd5b918e34fcdb144603b5a)) +* revert changes to AbstractMcpTool, maintaining backwards compatible text_output field in the response ([5f34d59](https://github.com/google/adk-java/commit/5f34d598435a2a8d875a5dbb14344c201db0e75f)) +* Using App conformant agent names ([f3eb936](https://github.com/google/adk-java/commit/f3eb936772740b7dc7a803a40d0d39fdbccc4af4)) + + +### Documentation + +* add pull request template ([6bb721b](https://github.com/google/adk-java/commit/6bb721b9a6000dac9dfda498fb6dd2c45862e25c)) + + +### Miscellaneous Chores + +* set release version to 1.0.0 ([dd1c941](https://github.com/google/adk-java/commit/dd1c94184835838fa47de024cf458c2ea0786aff)) +* set version to 1.0.0-rc.2 ([678b496](https://github.com/google/adk-java/commit/678b49653fa93606e2e57926213f0facaf9d6666)) + ## [1.0.0-rc.1](https://github.com/google/adk-java/compare/v0.9.0...v1.0.0-rc.1) (2026-03-20) diff --git a/README.md b/README.md index 169c78564..58307be2b 100644 --- a/README.md +++ b/README.md @@ -50,13 +50,13 @@ If you're using Maven, add the following to your dependencies: com.google.adk google-adk - 1.0.0-rc.1 + 1.0.0 com.google.adk google-adk-dev - 1.0.0-rc.1 + 1.0.0 ``` diff --git a/a2a/pom.xml b/a2a/pom.xml index 97c186606..38d1edafa 100644 --- a/a2a/pom.xml +++ b/a2a/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 1.0.1-rc.1-SNAPSHOT + 1.0.1-SNAPSHOT google-adk-a2a diff --git a/contrib/firestore-session-service/pom.xml b/contrib/firestore-session-service/pom.xml index 34b577984..917e23a0c 100644 --- a/contrib/firestore-session-service/pom.xml +++ b/contrib/firestore-session-service/pom.xml @@ -14,15 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. --> - + 4.0.0 com.google.adk google-adk-parent - 1.0.1-rc.1-SNAPSHOT + 1.0.1-SNAPSHOT ../../pom.xml diff --git a/contrib/langchain4j/pom.xml b/contrib/langchain4j/pom.xml index e88174849..51815af03 100644 --- a/contrib/langchain4j/pom.xml +++ b/contrib/langchain4j/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 1.0.1-rc.1-SNAPSHOT + 1.0.1-SNAPSHOT ../../pom.xml diff --git a/contrib/samples/a2a_basic/pom.xml b/contrib/samples/a2a_basic/pom.xml index 80881842b..6ed3d6475 100644 --- a/contrib/samples/a2a_basic/pom.xml +++ b/contrib/samples/a2a_basic/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-samples - 1.0.1-rc.1-SNAPSHOT + 1.0.1-SNAPSHOT .. diff --git a/contrib/samples/a2a_server/pom.xml b/contrib/samples/a2a_server/pom.xml index 61c44bc97..496106315 100644 --- a/contrib/samples/a2a_server/pom.xml +++ b/contrib/samples/a2a_server/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-samples - 1.0.1-rc.1-SNAPSHOT + 1.0.1-SNAPSHOT .. @@ -23,12 +23,13 @@ 0.3.0.Beta1 3.30.6 0.8 + graalvm - io.quarkus + io.quarkus.platform quarkus-bom ${quarkus.platform.version} pom diff --git a/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentExecutorProducer.java b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentExecutorProducer.java index 4ecd2517d..a367f87b6 100644 --- a/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentExecutorProducer.java +++ b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentExecutorProducer.java @@ -1,6 +1,7 @@ package com.google.adk.samples.a2aagent; import com.google.adk.a2a.executor.AgentExecutorConfig; +import com.google.adk.artifacts.InMemoryArtifactService; import com.google.adk.samples.a2aagent.agent.Agent; import com.google.adk.sessions.InMemorySessionService; import io.a2a.server.agentexecution.AgentExecutor; @@ -18,10 +19,12 @@ public class AgentExecutorProducer { @Produces public AgentExecutor agentExecutor() { InMemorySessionService sessionService = new InMemorySessionService(); + InMemoryArtifactService artifactService = new InMemoryArtifactService(); return new com.google.adk.a2a.executor.AgentExecutor.Builder() .agent(Agent.ROOT_AGENT) .appName(appName) .sessionService(sessionService) + .artifactService(artifactService) .agentExecutorConfig(AgentExecutorConfig.builder().build()) .build(); } diff --git a/contrib/samples/configagent/pom.xml b/contrib/samples/configagent/pom.xml index 8f57b7f9e..c83cc9e2b 100644 --- a/contrib/samples/configagent/pom.xml +++ b/contrib/samples/configagent/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-samples - 1.0.1-rc.1-SNAPSHOT + 1.0.1-SNAPSHOT .. diff --git a/contrib/samples/helloworld/pom.xml b/contrib/samples/helloworld/pom.xml index 676a2bc96..39ec6f736 100644 --- a/contrib/samples/helloworld/pom.xml +++ b/contrib/samples/helloworld/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-samples - 1.0.1-rc.1-SNAPSHOT + 1.0.1-SNAPSHOT .. diff --git a/contrib/samples/mcpfilesystem/pom.xml b/contrib/samples/mcpfilesystem/pom.xml index 7275313ab..15c05eb7e 100644 --- a/contrib/samples/mcpfilesystem/pom.xml +++ b/contrib/samples/mcpfilesystem/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 1.0.1-rc.1-SNAPSHOT + 1.0.1-SNAPSHOT ../../.. diff --git a/contrib/samples/pom.xml b/contrib/samples/pom.xml index ff48d6bd3..ab84e33ee 100644 --- a/contrib/samples/pom.xml +++ b/contrib/samples/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 1.0.1-rc.1-SNAPSHOT + 1.0.1-SNAPSHOT ../.. diff --git a/contrib/spring-ai/pom.xml b/contrib/spring-ai/pom.xml index 5f7300896..eb7abde49 100644 --- a/contrib/spring-ai/pom.xml +++ b/contrib/spring-ai/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 1.0.1-rc.1-SNAPSHOT + 1.0.1-SNAPSHOT ../../pom.xml diff --git a/core/pom.xml b/core/pom.xml index 8559f396d..5670d425f 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 1.0.1-rc.1-SNAPSHOT + 1.0.1-SNAPSHOT google-adk diff --git a/core/src/main/java/com/google/adk/SchemaUtils.java b/core/src/main/java/com/google/adk/SchemaUtils.java index 644aa6ded..200865d33 100644 --- a/core/src/main/java/com/google/adk/SchemaUtils.java +++ b/core/src/main/java/com/google/adk/SchemaUtils.java @@ -17,6 +17,7 @@ package com.google.adk; import com.fasterxml.jackson.core.JsonProcessingException; +import com.google.common.base.Preconditions; import com.google.genai.types.Schema; import com.google.genai.types.Type; import java.util.HashMap; @@ -87,6 +88,7 @@ private static Boolean matchType(Object value, Schema schema, Boolean isInput) { * @throws IllegalArgumentException If the map does not match the schema. */ public static void validateMapOnSchema(Map args, Schema schema, Boolean isInput) { + Preconditions.checkNotNull(isInput, "IsInput cannot be null"); Map properties = schema.properties().get(); for (Entry arg : args.entrySet()) { // Check if the argument is in the schema. diff --git a/core/src/main/java/com/google/adk/Version.java b/core/src/main/java/com/google/adk/Version.java index d6e18c5ad..b32afce6f 100644 --- a/core/src/main/java/com/google/adk/Version.java +++ b/core/src/main/java/com/google/adk/Version.java @@ -22,7 +22,7 @@ */ public final class Version { // Don't touch this, release-please should keep it up to date. - public static final String JAVA_ADK_VERSION = "1.0.0-rc.1"; // x-release-please-released-version + public static final String JAVA_ADK_VERSION = "1.0.0"; // x-release-please-released-version private Version() {} } diff --git a/core/src/main/java/com/google/adk/agents/ActiveStreamingTool.java b/core/src/main/java/com/google/adk/agents/ActiveStreamingTool.java index 78e7a471e..b0dff0c10 100644 --- a/core/src/main/java/com/google/adk/agents/ActiveStreamingTool.java +++ b/core/src/main/java/com/google/adk/agents/ActiveStreamingTool.java @@ -18,12 +18,12 @@ package com.google.adk.agents; import io.reactivex.rxjava3.disposables.Disposable; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** Manages streaming tool related resources during invocation. */ public class ActiveStreamingTool { - @Nullable private Disposable task; - @Nullable private LiveRequestQueue stream; + private @Nullable Disposable task; + private @Nullable LiveRequestQueue stream; public ActiveStreamingTool(Disposable task) { this(task, null); @@ -41,8 +41,7 @@ public ActiveStreamingTool(Disposable task, LiveRequestQueue stream) { public ActiveStreamingTool() {} /** Returns the active task of this streaming tool. */ - @Nullable - public Disposable task() { + public @Nullable Disposable task() { return task; } @@ -52,8 +51,7 @@ public void task(@Nullable Disposable task) { } /** Returns the active stream of this streaming tool. */ - @Nullable - public LiveRequestQueue stream() { + public @Nullable LiveRequestQueue stream() { return stream; } diff --git a/core/src/main/java/com/google/adk/agents/ConfigAgentUtils.java b/core/src/main/java/com/google/adk/agents/ConfigAgentUtils.java index c489c9b0d..893353f27 100644 --- a/core/src/main/java/com/google/adk/agents/ConfigAgentUtils.java +++ b/core/src/main/java/com/google/adk/agents/ConfigAgentUtils.java @@ -35,7 +35,7 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/core/src/main/java/com/google/adk/agents/InvocationContext.java b/core/src/main/java/com/google/adk/agents/InvocationContext.java index 365f4f8c1..f3a3cf3b0 100644 --- a/core/src/main/java/com/google/adk/agents/InvocationContext.java +++ b/core/src/main/java/com/google/adk/agents/InvocationContext.java @@ -33,7 +33,7 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** The context for an agent invocation. */ public class InvocationContext { diff --git a/core/src/main/java/com/google/adk/agents/LiveRequest.java b/core/src/main/java/com/google/adk/agents/LiveRequest.java index df1ada6a5..416ffa794 100644 --- a/core/src/main/java/com/google/adk/agents/LiveRequest.java +++ b/core/src/main/java/com/google/adk/agents/LiveRequest.java @@ -26,7 +26,7 @@ import com.google.genai.types.Blob; import com.google.genai.types.Content; import java.util.Optional; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** Represents a request to be sent to a live connection to the LLM model. */ @AutoValue diff --git a/core/src/main/java/com/google/adk/agents/LlmAgent.java b/core/src/main/java/com/google/adk/agents/LlmAgent.java index 077068283..98bba4606 100644 --- a/core/src/main/java/com/google/adk/agents/LlmAgent.java +++ b/core/src/main/java/com/google/adk/agents/LlmAgent.java @@ -71,7 +71,7 @@ import java.util.Optional; import java.util.concurrent.Executor; import java.util.function.Function; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -129,10 +129,10 @@ protected LlmAgent(Builder builder) { requireNonNullElse(builder.globalInstruction, new Instruction.Static("")); this.generateContentConfig = Optional.ofNullable(builder.generateContentConfig); this.includeContents = requireNonNullElse(builder.includeContents, IncludeContents.DEFAULT); - this.planning = builder.planning != null && builder.planning; + this.planning = requireNonNullElse(builder.planning, false); this.maxSteps = Optional.ofNullable(builder.maxSteps); - this.disallowTransferToParent = builder.disallowTransferToParent; - this.disallowTransferToPeers = builder.disallowTransferToPeers; + this.disallowTransferToParent = requireNonNullElse(builder.disallowTransferToParent, false); + this.disallowTransferToPeers = requireNonNullElse(builder.disallowTransferToPeers, false); this.beforeModelCallback = requireNonNullElse(builder.beforeModelCallback, ImmutableList.of()); this.afterModelCallback = requireNonNullElse(builder.afterModelCallback, ImmutableList.of()); this.onModelErrorCallback = @@ -566,8 +566,7 @@ public Builder codeExecutor(BaseCodeExecutor codeExecutor) { return this; } - @Nullable - private static ImmutableList convertCallbacks( + private static @Nullable ImmutableList convertCallbacks( @Nullable List callbacks, Function converter, String callbackType) { return Optional.ofNullable(callbacks) .map( diff --git a/core/src/main/java/com/google/adk/agents/ParallelAgent.java b/core/src/main/java/com/google/adk/agents/ParallelAgent.java index 2593ec13a..1e98dbf50 100644 --- a/core/src/main/java/com/google/adk/agents/ParallelAgent.java +++ b/core/src/main/java/com/google/adk/agents/ParallelAgent.java @@ -148,7 +148,8 @@ protected Flowable runAsyncImpl(InvocationContext invocationContext) { for (BaseAgent subAgent : currentSubAgents) { agentFlowables.add(subAgent.runAsync(updatedInvocationContext).subscribeOn(scheduler)); } - return Flowable.merge(agentFlowables); + return Flowable.merge(agentFlowables) + .takeUntil((Event event) -> event.actions().escalate().orElse(false)); } /** diff --git a/core/src/main/java/com/google/adk/agents/RunConfig.java b/core/src/main/java/com/google/adk/agents/RunConfig.java index 1ca203eaf..3ec02e1da 100644 --- a/core/src/main/java/com/google/adk/agents/RunConfig.java +++ b/core/src/main/java/com/google/adk/agents/RunConfig.java @@ -22,7 +22,7 @@ import com.google.genai.types.AudioTranscriptionConfig; import com.google.genai.types.Modality; import com.google.genai.types.SpeechConfig; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -76,59 +76,115 @@ public enum ToolExecutionMode { public static Builder builder() { return new AutoValue_RunConfig.Builder() - .setSaveInputBlobsAsArtifacts(false) - .setResponseModalities(ImmutableList.of()) - .setStreamingMode(StreamingMode.NONE) - .setToolExecutionMode(ToolExecutionMode.NONE) - .setMaxLlmCalls(500) - .setAutoCreateSession(false); + .saveInputBlobsAsArtifacts(false) + .responseModalities(ImmutableList.of()) + .streamingMode(StreamingMode.NONE) + .toolExecutionMode(ToolExecutionMode.NONE) + .maxLlmCalls(500) + .autoCreateSession(false); } public static Builder builder(RunConfig runConfig) { return new AutoValue_RunConfig.Builder() - .setSaveInputBlobsAsArtifacts(runConfig.saveInputBlobsAsArtifacts()) - .setStreamingMode(runConfig.streamingMode()) - .setToolExecutionMode(runConfig.toolExecutionMode()) - .setMaxLlmCalls(runConfig.maxLlmCalls()) - .setResponseModalities(runConfig.responseModalities()) - .setSpeechConfig(runConfig.speechConfig()) - .setOutputAudioTranscription(runConfig.outputAudioTranscription()) - .setInputAudioTranscription(runConfig.inputAudioTranscription()) - .setAutoCreateSession(runConfig.autoCreateSession()); + .saveInputBlobsAsArtifacts(runConfig.saveInputBlobsAsArtifacts()) + .streamingMode(runConfig.streamingMode()) + .toolExecutionMode(runConfig.toolExecutionMode()) + .maxLlmCalls(runConfig.maxLlmCalls()) + .responseModalities(runConfig.responseModalities()) + .speechConfig(runConfig.speechConfig()) + .outputAudioTranscription(runConfig.outputAudioTranscription()) + .inputAudioTranscription(runConfig.inputAudioTranscription()) + .autoCreateSession(runConfig.autoCreateSession()); } /** Builder for {@link RunConfig}. */ @AutoValue.Builder public abstract static class Builder { + @Deprecated @CanIgnoreReturnValue - public abstract Builder setSpeechConfig(@Nullable SpeechConfig speechConfig); + public final Builder setSpeechConfig(@Nullable SpeechConfig speechConfig) { + return speechConfig(speechConfig); + } + + @CanIgnoreReturnValue + public abstract Builder speechConfig(@Nullable SpeechConfig speechConfig); + + @Deprecated + @CanIgnoreReturnValue + public final Builder setResponseModalities(Iterable responseModalities) { + return responseModalities(responseModalities); + } + + @CanIgnoreReturnValue + public abstract Builder responseModalities(Iterable responseModalities); + @Deprecated @CanIgnoreReturnValue - public abstract Builder setResponseModalities(Iterable responseModalities); + public final Builder setSaveInputBlobsAsArtifacts(boolean saveInputBlobsAsArtifacts) { + return saveInputBlobsAsArtifacts(saveInputBlobsAsArtifacts); + } @CanIgnoreReturnValue - public abstract Builder setSaveInputBlobsAsArtifacts(boolean saveInputBlobsAsArtifacts); + public abstract Builder saveInputBlobsAsArtifacts(boolean saveInputBlobsAsArtifacts); + @Deprecated @CanIgnoreReturnValue - public abstract Builder setStreamingMode(StreamingMode streamingMode); + public final Builder setStreamingMode(StreamingMode streamingMode) { + return streamingMode(streamingMode); + } @CanIgnoreReturnValue - public abstract Builder setToolExecutionMode(ToolExecutionMode toolExecutionMode); + public abstract Builder streamingMode(StreamingMode streamingMode); + @Deprecated @CanIgnoreReturnValue - public abstract Builder setOutputAudioTranscription( + public final Builder setToolExecutionMode(ToolExecutionMode toolExecutionMode) { + return toolExecutionMode(toolExecutionMode); + } + + @CanIgnoreReturnValue + public abstract Builder toolExecutionMode(ToolExecutionMode toolExecutionMode); + + @Deprecated + @CanIgnoreReturnValue + public final Builder setOutputAudioTranscription( + @Nullable AudioTranscriptionConfig outputAudioTranscription) { + return outputAudioTranscription(outputAudioTranscription); + } + + @CanIgnoreReturnValue + public abstract Builder outputAudioTranscription( @Nullable AudioTranscriptionConfig outputAudioTranscription); + @Deprecated + @CanIgnoreReturnValue + public final Builder setInputAudioTranscription( + @Nullable AudioTranscriptionConfig inputAudioTranscription) { + return inputAudioTranscription(inputAudioTranscription); + } + @CanIgnoreReturnValue - public abstract Builder setInputAudioTranscription( + public abstract Builder inputAudioTranscription( @Nullable AudioTranscriptionConfig inputAudioTranscription); + @Deprecated @CanIgnoreReturnValue - public abstract Builder setMaxLlmCalls(int maxLlmCalls); + public final Builder setMaxLlmCalls(int maxLlmCalls) { + return maxLlmCalls(maxLlmCalls); + } + + @CanIgnoreReturnValue + public abstract Builder maxLlmCalls(int maxLlmCalls); + + @Deprecated + @CanIgnoreReturnValue + public final Builder setAutoCreateSession(boolean autoCreateSession) { + return autoCreateSession(autoCreateSession); + } @CanIgnoreReturnValue - public abstract Builder setAutoCreateSession(boolean autoCreateSession); + public abstract Builder autoCreateSession(boolean autoCreateSession); abstract RunConfig autoBuild(); diff --git a/core/src/main/java/com/google/adk/agents/ToolResolver.java b/core/src/main/java/com/google/adk/agents/ToolResolver.java index b48b25310..09a3d79c1 100644 --- a/core/src/main/java/com/google/adk/agents/ToolResolver.java +++ b/core/src/main/java/com/google/adk/agents/ToolResolver.java @@ -32,7 +32,7 @@ import java.lang.reflect.Modifier; import java.util.List; import java.util.Optional; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/core/src/main/java/com/google/adk/apps/App.java b/core/src/main/java/com/google/adk/apps/App.java index a087b738c..500c29253 100644 --- a/core/src/main/java/com/google/adk/apps/App.java +++ b/core/src/main/java/com/google/adk/apps/App.java @@ -24,7 +24,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.List; import java.util.regex.Pattern; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** * Represents an LLM-backed agentic application. @@ -40,8 +40,8 @@ public class App { private final String name; private final BaseAgent rootAgent; private final ImmutableList plugins; - @Nullable private final EventsCompactionConfig eventsCompactionConfig; - @Nullable private final ContextCacheConfig contextCacheConfig; + private final @Nullable EventsCompactionConfig eventsCompactionConfig; + private final @Nullable ContextCacheConfig contextCacheConfig; private App( String name, diff --git a/core/src/main/java/com/google/adk/events/Event.java b/core/src/main/java/com/google/adk/events/Event.java index 2677b635d..cde4e4452 100644 --- a/core/src/main/java/com/google/adk/events/Event.java +++ b/core/src/main/java/com/google/adk/events/Event.java @@ -39,7 +39,7 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; // TODO - b/413761119 update Agent.java when resolved. /** Represents an event in a session. */ @@ -348,7 +348,7 @@ public static class Builder { private String invocationId; private String author; private @Nullable Content content; - private EventActions actions; + private @Nullable EventActions actions; private @Nullable Set longRunningToolIds; private @Nullable Boolean partial; private @Nullable Boolean turnComplete; @@ -399,7 +399,7 @@ public Builder content(@Nullable Content value) { @CanIgnoreReturnValue @JsonProperty("actions") - public Builder actions(EventActions value) { + public Builder actions(@Nullable EventActions value) { this.actions = value; return this; } diff --git a/core/src/main/java/com/google/adk/events/EventActions.java b/core/src/main/java/com/google/adk/events/EventActions.java index 105300aa0..9c977240c 100644 --- a/core/src/main/java/com/google/adk/events/EventActions.java +++ b/core/src/main/java/com/google/adk/events/EventActions.java @@ -281,7 +281,7 @@ private Builder(EventActions eventActions) { @CanIgnoreReturnValue @JsonProperty("skipSummarization") - public Builder skipSummarization(boolean skipSummarization) { + public Builder skipSummarization(@Nullable Boolean skipSummarization) { this.skipSummarization = skipSummarization; return this; } @@ -324,7 +324,7 @@ public Builder transferToAgent(@Nullable String agentId) { @CanIgnoreReturnValue @JsonProperty("escalate") - public Builder escalate(boolean escalate) { + public Builder escalate(@Nullable Boolean escalate) { this.escalate = escalate; return this; } diff --git a/core/src/main/java/com/google/adk/events/ToolConfirmation.java b/core/src/main/java/com/google/adk/events/ToolConfirmation.java index 167286094..ae0104aa1 100644 --- a/core/src/main/java/com/google/adk/events/ToolConfirmation.java +++ b/core/src/main/java/com/google/adk/events/ToolConfirmation.java @@ -21,7 +21,7 @@ import com.google.adk.JsonBaseModel; import com.google.auto.value.AutoValue; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** Represents a tool confirmation configuration. */ @AutoValue diff --git a/core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java b/core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java index aa62b9f31..48d141819 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java @@ -415,6 +415,11 @@ private Flowable runOneStep(Context spanContext, InvocationContext contex }) .concatMap( event -> { + // Update event ID for the new resulting events + String oldId = event.id(); + String newId = Event.generateEventId(); + logger.debug("Resetting event ID from {} to {}", oldId, newId); + event = event.toBuilder().id(newId).build(); Flowable postProcessedEvents = Flowable.just(event); if (event.actions().transferToAgent().isPresent()) { String agentToTransfer = diff --git a/core/src/main/java/com/google/adk/flows/llmflows/Contents.java b/core/src/main/java/com/google/adk/flows/llmflows/Contents.java index 840a370c6..876f3a206 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/Contents.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/Contents.java @@ -42,7 +42,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** {@link RequestProcessor} that populates content in request for LLM flows. */ public final class Contents implements RequestProcessor { diff --git a/core/src/main/java/com/google/adk/memory/InMemoryMemoryService.java b/core/src/main/java/com/google/adk/memory/InMemoryMemoryService.java index 200c6df55..ff2995c74 100644 --- a/core/src/main/java/com/google/adk/memory/InMemoryMemoryService.java +++ b/core/src/main/java/com/google/adk/memory/InMemoryMemoryService.java @@ -130,7 +130,7 @@ public Single searchMemory(String appName, String userId, } return SearchMemoryResponse.builder() - .setMemories(ImmutableList.copyOf(matchingMemories)) + .memories(ImmutableList.copyOf(matchingMemories)) .build(); }); } diff --git a/core/src/main/java/com/google/adk/memory/MemoryEntry.java b/core/src/main/java/com/google/adk/memory/MemoryEntry.java index ef310f627..65f1dadc3 100644 --- a/core/src/main/java/com/google/adk/memory/MemoryEntry.java +++ b/core/src/main/java/com/google/adk/memory/MemoryEntry.java @@ -22,7 +22,7 @@ import com.google.auto.value.AutoValue; import com.google.genai.types.Content; import java.time.Instant; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** Represents one memory entry. */ @AutoValue diff --git a/core/src/main/java/com/google/adk/memory/SearchMemoryResponse.java b/core/src/main/java/com/google/adk/memory/SearchMemoryResponse.java index b282bdfc8..b14e1d156 100644 --- a/core/src/main/java/com/google/adk/memory/SearchMemoryResponse.java +++ b/core/src/main/java/com/google/adk/memory/SearchMemoryResponse.java @@ -18,6 +18,7 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.List; /** Represents the response from a memory search. */ @@ -29,18 +30,32 @@ public abstract class SearchMemoryResponse { /** Creates a new builder for {@link SearchMemoryResponse}. */ public static Builder builder() { - return new AutoValue_SearchMemoryResponse.Builder().setMemories(ImmutableList.of()); + return new AutoValue_SearchMemoryResponse.Builder().memories(ImmutableList.of()); } /** Builder for {@link SearchMemoryResponse}. */ @AutoValue.Builder public abstract static class Builder { - abstract Builder setMemories(ImmutableList memories); + @Deprecated + @CanIgnoreReturnValue + public final Builder setMemories(ImmutableList memories) { + return memories(memories); + } /** Sets the list of memory entries using a list. */ - public Builder setMemories(List memories) { - return setMemories(ImmutableList.copyOf(memories)); + @Deprecated + @CanIgnoreReturnValue + public final Builder setMemories(List memories) { + return memories(ImmutableList.copyOf(memories)); + } + + @CanIgnoreReturnValue + public abstract Builder memories(ImmutableList memories); + + @CanIgnoreReturnValue + public Builder memories(List memories) { + return memories(ImmutableList.copyOf(memories)); } /** Builds the immutable {@link SearchMemoryResponse} object. */ diff --git a/core/src/main/java/com/google/adk/models/ChatCompletionsResponse.java b/core/src/main/java/com/google/adk/models/ChatCompletionsResponse.java new file mode 100644 index 000000000..fe5cdd116 --- /dev/null +++ b/core/src/main/java/com/google/adk/models/ChatCompletionsResponse.java @@ -0,0 +1,224 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.adk.models; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; + +/** + * Data Transfer Objects for Chat Completion and Chat Completion Chunk API responses. + * + *

These classes are used for deserializing JSON responses from the `/chat/completions` endpoint. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +final class ChatCompletionsResponse { + + private ChatCompletionsResponse() {} + + @JsonIgnoreProperties(ignoreUnknown = true) + static class ChatCompletion { + public String id; + public List choices; + public Long created; + public String model; + public String object; + + @JsonProperty("service_tier") + public String serviceTier; + + @JsonProperty("system_fingerprint") + public String systemFingerprint; + + public Usage usage; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static class Choice { + @JsonProperty("finish_reason") + public String finishReason; + + public Integer index; + public Logprobs logprobs; + public Message message; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static class ChatCompletionChunk { + public String id; + public List choices; + public Long created; + public String model; + public String object; + + @JsonProperty("service_tier") + public String serviceTier; + + @JsonProperty("system_fingerprint") + public String systemFingerprint; + + public Usage usage; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static class ChunkChoice { + @JsonProperty("finish_reason") + public String finishReason; + + public Integer index; + public Logprobs logprobs; + public Message delta; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static class Message { + public String content; + public String refusal; + public String role; + + @JsonProperty("tool_calls") + public List toolCalls; + + // function_call is not supported in ChatCompletionChunk and ChatCompletion support is + // deprecated. + @JsonProperty("function_call") + public Function functionCall; // Fallback for deprecated top-level function calls + + public List annotations; + public Audio audio; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static class ToolCall { + // Index is only used in ChatCompletionChunk. + public Integer index; + public String id; + public String type; + public Function function; + public Custom custom; + + @JsonProperty("extra_content") + public Map extraContent; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static class Function { + public String name; + public String arguments; // JSON string + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static class Custom { + public String input; + public String name; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static class Logprobs { + public List content; + public List refusal; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + static class TokenLogprob { + public String token; + public List bytes; + public Double logprob; + + @JsonProperty("top_logprobs") + public List topLogprobs; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static class Usage { + @JsonProperty("completion_tokens") + public Integer completionTokens; + + @JsonProperty("prompt_tokens") + public Integer promptTokens; + + @JsonProperty("total_tokens") + public Integer totalTokens; + + @JsonProperty("thoughts_token_count") + public Integer thoughtsTokenCount; // Gemini-specific extension + + @JsonProperty("completion_tokens_details") + public CompletionTokensDetails completionTokensDetails; + + @JsonProperty("prompt_tokens_details") + public PromptTokensDetails promptTokensDetails; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static class CompletionTokensDetails { + @JsonProperty("accepted_prediction_tokens") + public Integer acceptedPredictionTokens; + + @JsonProperty("audio_tokens") + public Integer audioTokens; + + @JsonProperty("reasoning_tokens") + public Integer reasoningTokens; + + @JsonProperty("rejected_prediction_tokens") + public Integer rejectedPredictionTokens; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static class PromptTokensDetails { + @JsonProperty("audio_tokens") + public Integer audioTokens; + + @JsonProperty("cached_tokens") + public Integer cachedTokens; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static class Annotation { + public String type; + + @JsonProperty("url_citation") + public UrlCitation urlCitation; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static class UrlCitation { + @JsonProperty("end_index") + public Integer endIndex; + + @JsonProperty("start_index") + public Integer startIndex; + + public String title; + public String url; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static class Audio { + public String id; + public String data; + + @JsonProperty("expires_at") + public Long expiresAt; + + public String transcript; + } +} diff --git a/core/src/main/java/com/google/adk/models/Claude.java b/core/src/main/java/com/google/adk/models/Claude.java index 01feda1d4..79201c261 100644 --- a/core/src/main/java/com/google/adk/models/Claude.java +++ b/core/src/main/java/com/google/adk/models/Claude.java @@ -172,17 +172,11 @@ private ContentBlockParam partToAnthropicMessageBlock(Part part) { if (part.functionResponse().get().response().isPresent()) { Map responseData = part.functionResponse().get().response().get(); - Object contentObj = responseData.get("content"); Object resultObj = responseData.get("result"); - - if (contentObj instanceof List list && !list.isEmpty()) { - // Native MCP format: list of content blocks - content = extractMcpContentBlocks(list); - } else if (resultObj != null) { - // ADK tool result object - content = resultObj instanceof String s ? s : serializeToJson(resultObj); - } else if (!responseData.isEmpty()) { - // Fallback: arbitrary JSON structure + if (resultObj != null) { + content = resultObj.toString(); + } else { + // Fallback to json serialization of the function response. content = serializeToJson(responseData); } } @@ -196,21 +190,6 @@ private ContentBlockParam partToAnthropicMessageBlock(Part part) { throw new UnsupportedOperationException("Not supported yet."); } - private String extractMcpContentBlocks(List list) { - List textBlocks = new ArrayList<>(); - for (Object item : list) { - if (item instanceof Map m && "text".equals(m.get("type"))) { - Object textObj = m.get("text"); - textBlocks.add(textObj != null ? String.valueOf(textObj) : ""); - } else if (item instanceof String s) { - textBlocks.add(s); - } else { - textBlocks.add(serializeToJson(item)); - } - } - return String.join("\n", textBlocks); - } - private String serializeToJson(Object obj) { try { return JsonBaseModel.getMapper().writeValueAsString(obj); diff --git a/core/src/main/java/com/google/adk/models/LlmResponse.java b/core/src/main/java/com/google/adk/models/LlmResponse.java index 1ca381e23..560e2abcc 100644 --- a/core/src/main/java/com/google/adk/models/LlmResponse.java +++ b/core/src/main/java/com/google/adk/models/LlmResponse.java @@ -33,7 +33,7 @@ import com.google.genai.types.GroundingMetadata; import java.util.List; import java.util.Optional; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** Represents a response received from the LLM. */ @AutoValue diff --git a/core/src/main/java/com/google/adk/models/VertexCredentials.java b/core/src/main/java/com/google/adk/models/VertexCredentials.java index 93dc05ec1..2c069b0dd 100644 --- a/core/src/main/java/com/google/adk/models/VertexCredentials.java +++ b/core/src/main/java/com/google/adk/models/VertexCredentials.java @@ -18,8 +18,9 @@ import com.google.auth.oauth2.GoogleCredentials; import com.google.auto.value.AutoValue; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.util.Optional; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** Credentials for accessing Gemini models through Vertex. */ @AutoValue @@ -39,11 +40,32 @@ public static Builder builder() { @AutoValue.Builder public abstract static class Builder { - public abstract Builder setProject(@Nullable String value); + @Deprecated + @CanIgnoreReturnValue + public final Builder setProject(@Nullable String value) { + return project(value); + } - public abstract Builder setLocation(@Nullable String value); + @CanIgnoreReturnValue + public abstract Builder project(@Nullable String value); - public abstract Builder setCredentials(@Nullable GoogleCredentials value); + @Deprecated + @CanIgnoreReturnValue + public final Builder setLocation(@Nullable String value) { + return location(value); + } + + @CanIgnoreReturnValue + public abstract Builder location(@Nullable String value); + + @Deprecated + @CanIgnoreReturnValue + public final Builder setCredentials(@Nullable GoogleCredentials value) { + return credentials(value); + } + + @CanIgnoreReturnValue + public abstract Builder credentials(@Nullable GoogleCredentials value); public abstract VertexCredentials build(); } diff --git a/core/src/main/java/com/google/adk/plugins/agentanalytics/BigQueryLoggerConfig.java b/core/src/main/java/com/google/adk/plugins/agentanalytics/BigQueryLoggerConfig.java index aa5bf37de..22ced137e 100644 --- a/core/src/main/java/com/google/adk/plugins/agentanalytics/BigQueryLoggerConfig.java +++ b/core/src/main/java/com/google/adk/plugins/agentanalytics/BigQueryLoggerConfig.java @@ -20,12 +20,13 @@ import com.google.auto.value.AutoValue; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import java.time.Duration; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.BiFunction; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** Configuration for the BigQueryAgentAnalyticsPlugin. */ @AutoValue @@ -104,66 +105,208 @@ public abstract class BigQueryLoggerConfig { public static Builder builder() { return new AutoValue_BigQueryLoggerConfig.Builder() - .setEnabled(true) - .setMaxContentLength(500 * 1024) - .setDatasetId("agent_analytics") - .setTableName("events") - .setClusteringFields(ImmutableList.of("event_type", "agent", "user_id")) - .setLogMultiModalContent(true) - .setRetryConfig(RetryConfig.builder().build()) - .setBatchSize(1) - .setBatchFlushInterval(Duration.ofSeconds(1)) - .setShutdownTimeout(Duration.ofSeconds(10)) - .setQueueMaxSize(10000) - .setLogSessionMetadata(true) - .setCustomTags(ImmutableMap.of()) + .enabled(true) + .maxContentLength(500 * 1024) + .datasetId("agent_analytics") + .tableName("events") + .clusteringFields(ImmutableList.of("event_type", "agent", "user_id")) + .logMultiModalContent(true) + .retryConfig(RetryConfig.builder().build()) + .batchSize(1) + .batchFlushInterval(Duration.ofSeconds(1)) + .shutdownTimeout(Duration.ofSeconds(10)) + .queueMaxSize(10000) + .logSessionMetadata(true) + .customTags(ImmutableMap.of()) // TODO(b/491851868): Enable auto-schema upgrade once implemented. - .setAutoSchemaUpgrade(false); + .autoSchemaUpgrade(false); } /** Builder for {@link BigQueryLoggerConfig}. */ @AutoValue.Builder public abstract static class Builder { - public abstract Builder setEnabled(boolean enabled); - public abstract Builder setEventAllowlist(@Nullable List eventAllowlist); + @Deprecated + @CanIgnoreReturnValue + public final Builder setEnabled(boolean enabled) { + return enabled(enabled); + } + + @CanIgnoreReturnValue + public abstract Builder enabled(boolean enabled); + + @Deprecated + @CanIgnoreReturnValue + public final Builder setEventAllowlist(@Nullable List eventAllowlist) { + return eventAllowlist(eventAllowlist); + } + + @CanIgnoreReturnValue + public abstract Builder eventAllowlist(@Nullable List eventAllowlist); + + @Deprecated + @CanIgnoreReturnValue + public final Builder setEventDenylist(@Nullable List eventDenylist) { + return eventDenylist(eventDenylist); + } + + @CanIgnoreReturnValue + public abstract Builder eventDenylist(@Nullable List eventDenylist); + + @Deprecated + @CanIgnoreReturnValue + public final Builder setMaxContentLength(int maxContentLength) { + return maxContentLength(maxContentLength); + } + + @CanIgnoreReturnValue + public abstract Builder maxContentLength(int maxContentLength); - public abstract Builder setEventDenylist(@Nullable List eventDenylist); + @Deprecated + @CanIgnoreReturnValue + public final Builder setProjectId(String projectId) { + return projectId(projectId); + } - public abstract Builder setMaxContentLength(int maxContentLength); + @CanIgnoreReturnValue + public abstract Builder projectId(String projectId); - public abstract Builder setProjectId(String projectId); + @Deprecated + @CanIgnoreReturnValue + public final Builder setDatasetId(String datasetId) { + return datasetId(datasetId); + } - public abstract Builder setDatasetId(String datasetId); + @CanIgnoreReturnValue + public abstract Builder datasetId(String datasetId); - public abstract Builder setTableName(String tableName); + @Deprecated + @CanIgnoreReturnValue + public final Builder setTableName(String tableName) { + return tableName(tableName); + } - public abstract Builder setClusteringFields(List clusteringFields); + @CanIgnoreReturnValue + public abstract Builder tableName(String tableName); - public abstract Builder setLogMultiModalContent(boolean logMultiModalContent); + @Deprecated + @CanIgnoreReturnValue + public final Builder setClusteringFields(List clusteringFields) { + return clusteringFields(clusteringFields); + } - public abstract Builder setRetryConfig(RetryConfig retryConfig); + @CanIgnoreReturnValue + public abstract Builder clusteringFields(List clusteringFields); - public abstract Builder setBatchSize(int batchSize); + @Deprecated + @CanIgnoreReturnValue + public final Builder setLogMultiModalContent(boolean logMultiModalContent) { + return logMultiModalContent(logMultiModalContent); + } - public abstract Builder setBatchFlushInterval(Duration batchFlushInterval); + @CanIgnoreReturnValue + public abstract Builder logMultiModalContent(boolean logMultiModalContent); - public abstract Builder setShutdownTimeout(Duration shutdownTimeout); + @Deprecated + @CanIgnoreReturnValue + public final Builder setRetryConfig(RetryConfig retryConfig) { + return retryConfig(retryConfig); + } - public abstract Builder setQueueMaxSize(int queueMaxSize); + @CanIgnoreReturnValue + public abstract Builder retryConfig(RetryConfig retryConfig); + + @Deprecated + @CanIgnoreReturnValue + public final Builder setBatchSize(int batchSize) { + return batchSize(batchSize); + } + + @CanIgnoreReturnValue + public abstract Builder batchSize(int batchSize); + + @Deprecated + @CanIgnoreReturnValue + public final Builder setBatchFlushInterval(Duration batchFlushInterval) { + return batchFlushInterval(batchFlushInterval); + } - public abstract Builder setContentFormatter( + @CanIgnoreReturnValue + public abstract Builder batchFlushInterval(Duration batchFlushInterval); + + @Deprecated + @CanIgnoreReturnValue + public final Builder setShutdownTimeout(Duration shutdownTimeout) { + return shutdownTimeout(shutdownTimeout); + } + + @CanIgnoreReturnValue + public abstract Builder shutdownTimeout(Duration shutdownTimeout); + + @Deprecated + @CanIgnoreReturnValue + public final Builder setQueueMaxSize(int queueMaxSize) { + return queueMaxSize(queueMaxSize); + } + + @CanIgnoreReturnValue + public abstract Builder queueMaxSize(int queueMaxSize); + + @Deprecated + @CanIgnoreReturnValue + public final Builder setContentFormatter( + @Nullable BiFunction contentFormatter) { + return contentFormatter(contentFormatter); + } + + @CanIgnoreReturnValue + public abstract Builder contentFormatter( @Nullable BiFunction contentFormatter); - public abstract Builder setConnectionId(String connectionId); + @Deprecated + @CanIgnoreReturnValue + public final Builder setConnectionId(String connectionId) { + return connectionId(connectionId); + } - public abstract Builder setLogSessionMetadata(boolean logSessionMetadata); + @CanIgnoreReturnValue + public abstract Builder connectionId(String connectionId); - public abstract Builder setCustomTags(Map customTags); + @Deprecated + @CanIgnoreReturnValue + public final Builder setLogSessionMetadata(boolean logSessionMetadata) { + return logSessionMetadata(logSessionMetadata); + } - public abstract Builder setAutoSchemaUpgrade(boolean autoSchemaUpgrade); + @CanIgnoreReturnValue + public abstract Builder logSessionMetadata(boolean logSessionMetadata); - public abstract Builder setCredentials(Credentials credentials); + @Deprecated + @CanIgnoreReturnValue + public final Builder setCustomTags(Map customTags) { + return customTags(customTags); + } + + @CanIgnoreReturnValue + public abstract Builder customTags(Map customTags); + + @Deprecated + @CanIgnoreReturnValue + public final Builder setAutoSchemaUpgrade(boolean autoSchemaUpgrade) { + return autoSchemaUpgrade(autoSchemaUpgrade); + } + + @CanIgnoreReturnValue + public abstract Builder autoSchemaUpgrade(boolean autoSchemaUpgrade); + + @Deprecated + @CanIgnoreReturnValue + public final Builder setCredentials(Credentials credentials) { + return credentials(credentials); + } + + @CanIgnoreReturnValue + public abstract Builder credentials(Credentials credentials); public abstract BigQueryLoggerConfig build(); } @@ -181,22 +324,51 @@ public abstract static class RetryConfig { public static Builder builder() { return new AutoValue_BigQueryLoggerConfig_RetryConfig.Builder() - .setMaxRetries(3) - .setInitialDelay(Duration.ofSeconds(1)) - .setMultiplier(2.0) - .setMaxDelay(Duration.ofSeconds(10)); + .maxRetries(3) + .initialDelay(Duration.ofSeconds(1)) + .multiplier(2.0) + .maxDelay(Duration.ofSeconds(10)); } /** Builder for {@link RetryConfig}. */ @AutoValue.Builder public abstract static class Builder { - public abstract Builder setMaxRetries(int maxRetries); - - public abstract Builder setInitialDelay(Duration initialDelay); - - public abstract Builder setMultiplier(double multiplier); - public abstract Builder setMaxDelay(Duration maxDelay); + @Deprecated + @CanIgnoreReturnValue + public final Builder setMaxRetries(int maxRetries) { + return maxRetries(maxRetries); + } + + @CanIgnoreReturnValue + public abstract Builder maxRetries(int maxRetries); + + @Deprecated + @CanIgnoreReturnValue + public final Builder setInitialDelay(Duration initialDelay) { + return initialDelay(initialDelay); + } + + @CanIgnoreReturnValue + public abstract Builder initialDelay(Duration initialDelay); + + @Deprecated + @CanIgnoreReturnValue + public final Builder setMultiplier(double multiplier) { + return multiplier(multiplier); + } + + @CanIgnoreReturnValue + public abstract Builder multiplier(double multiplier); + + @Deprecated + @CanIgnoreReturnValue + public final Builder setMaxDelay(Duration maxDelay) { + return maxDelay(maxDelay); + } + + @CanIgnoreReturnValue + public abstract Builder maxDelay(Duration maxDelay); public abstract RetryConfig build(); } diff --git a/core/src/main/java/com/google/adk/runner/Runner.java b/core/src/main/java/com/google/adk/runner/Runner.java index 2bfbca881..d6ad38561 100644 --- a/core/src/main/java/com/google/adk/runner/Runner.java +++ b/core/src/main/java/com/google/adk/runner/Runner.java @@ -64,7 +64,7 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** The main class for the GenAI Agents runner. */ public class Runner { @@ -477,13 +477,8 @@ protected Flowable runAsyncImpl( session.appName(), session.userId(), session.id(), Optional.empty()) .flatMapPublisher( updatedSession -> - runAgentWithFreshSession( - session, - updatedSession, - event, - invocationId, - runConfig, - rootAgent)) + runAgentWithUpdatedSession( + initialContext, updatedSession, event, rootAgent)) .compose(Tracing.withContext(capturedContext)); }); }) @@ -495,19 +490,27 @@ protected Flowable runAsyncImpl( }); } - private Flowable runAgentWithFreshSession( - Session session, - Session updatedSession, - Event event, - String invocationId, - RunConfig runConfig, - BaseAgent rootAgent) { + /** + * Runs the agent with the updated session state. + * + *

This method is called after the user message has been persistent in the session. It creates + * a final {@link InvocationContext} that inherits state from the {@code initialContext} but uses + * the {@code updatedSession} to ensure the agent can access the latest conversation history. + * + * @param initialContext the context from the start of the invocation, used to preserve metadata + * and callback data. + * @param updatedSession the session object containing the latest message. + * @param event the event representing the user message that was just appended. + * @param rootAgent the agent to be executed. + * @return a stream of events from the agent execution and subsequent plugin callbacks. + */ + private Flowable runAgentWithUpdatedSession( + InvocationContext initialContext, Session updatedSession, Event event, BaseAgent rootAgent) { // Create context with updated session for beforeRunCallback InvocationContext contextWithUpdatedSession = - newInvocationContextBuilder(updatedSession) - .invocationId(invocationId) + initialContext.toBuilder() + .session(updatedSession) .agent(this.findAgentToRun(updatedSession, rootAgent)) - .runConfig(runConfig) .userContent(event.content().orElseGet(Content::fromParts)) .build(); @@ -536,7 +539,7 @@ private Flowable runAgentWithFreshSession( .flatMap( registeredEvent -> { // TODO: remove this hack after deprecating runAsync with Session. - copySessionStates(updatedSession, session); + copySessionStates(updatedSession, initialContext.session()); return contextWithUpdatedSession .pluginManager() .onEventCallback(contextWithUpdatedSession, registeredEvent) @@ -579,19 +582,18 @@ private InvocationContext newInvocationContextForLive( if (liveRequestQueue != null) { // Default to AUDIO modality if not specified. if (CollectionUtils.isNullOrEmpty(runConfig.responseModalities())) { - runConfigBuilder.setResponseModalities( - ImmutableList.of(new Modality(Modality.Known.AUDIO))); + runConfigBuilder.responseModalities(ImmutableList.of(new Modality(Modality.Known.AUDIO))); if (runConfig.outputAudioTranscription() == null) { - runConfigBuilder.setOutputAudioTranscription(AudioTranscriptionConfig.builder().build()); + runConfigBuilder.outputAudioTranscription(AudioTranscriptionConfig.builder().build()); } } else if (!runConfig.responseModalities().contains(new Modality(Modality.Known.TEXT))) { if (runConfig.outputAudioTranscription() == null) { - runConfigBuilder.setOutputAudioTranscription(AudioTranscriptionConfig.builder().build()); + runConfigBuilder.outputAudioTranscription(AudioTranscriptionConfig.builder().build()); } } // Need input transcription for agent transferring in live mode. if (runConfig.inputAudioTranscription() == null) { - runConfigBuilder.setInputAudioTranscription(AudioTranscriptionConfig.builder().build()); + runConfigBuilder.inputAudioTranscription(AudioTranscriptionConfig.builder().build()); } } InvocationContext.Builder builder = diff --git a/core/src/main/java/com/google/adk/sessions/BaseSessionService.java b/core/src/main/java/com/google/adk/sessions/BaseSessionService.java index 7a0885544..8596f8eb6 100644 --- a/core/src/main/java/com/google/adk/sessions/BaseSessionService.java +++ b/core/src/main/java/com/google/adk/sessions/BaseSessionService.java @@ -28,7 +28,7 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** * Defines the contract for managing {@link Session}s and their associated {@link Event}s. Provides diff --git a/core/src/main/java/com/google/adk/sessions/InMemorySessionService.java b/core/src/main/java/com/google/adk/sessions/InMemorySessionService.java index d9bb047a3..72a14cc4d 100644 --- a/core/src/main/java/com/google/adk/sessions/InMemorySessionService.java +++ b/core/src/main/java/com/google/adk/sessions/InMemorySessionService.java @@ -186,13 +186,20 @@ public Completable deleteSession(String appName, String userId, String sessionId Objects.requireNonNull(userId, "userId cannot be null"); Objects.requireNonNull(sessionId, "sessionId cannot be null"); - ConcurrentMap> appSessionsMap = sessions.get(appName); - if (appSessionsMap != null) { - ConcurrentMap userSessionsMap = appSessionsMap.get(userId); - if (userSessionsMap != null) { - userSessionsMap.remove(sessionId); - } - } + sessions.computeIfPresent( + appName, + (app, appSessionsMap) -> { + appSessionsMap.computeIfPresent( + userId, + (user, userSessionsMap) -> { + userSessionsMap.remove(sessionId); + // If userSessionsMap is now empty, return null to automatically remove the userId + // key + return userSessionsMap.isEmpty() ? null : userSessionsMap; + }); + // If appSessionsMap is now empty, return null to automatically remove the appName key + return appSessionsMap.isEmpty() ? null : appSessionsMap; + }); return Completable.complete(); } diff --git a/core/src/main/java/com/google/adk/sessions/SessionJsonConverter.java b/core/src/main/java/com/google/adk/sessions/SessionJsonConverter.java index 0c2b33704..adc84fbb6 100644 --- a/core/src/main/java/com/google/adk/sessions/SessionJsonConverter.java +++ b/core/src/main/java/com/google/adk/sessions/SessionJsonConverter.java @@ -39,7 +39,7 @@ import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/core/src/main/java/com/google/adk/sessions/State.java b/core/src/main/java/com/google/adk/sessions/State.java index 70d2dfbf2..577559f85 100644 --- a/core/src/main/java/com/google/adk/sessions/State.java +++ b/core/src/main/java/com/google/adk/sessions/State.java @@ -24,7 +24,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** A {@link State} object that also keeps track of the changes to the state. */ @SuppressWarnings("ShouldNotSubclass") diff --git a/core/src/main/java/com/google/adk/sessions/VertexAiClient.java b/core/src/main/java/com/google/adk/sessions/VertexAiClient.java index 1168d1166..2d8a13f2f 100644 --- a/core/src/main/java/com/google/adk/sessions/VertexAiClient.java +++ b/core/src/main/java/com/google/adk/sessions/VertexAiClient.java @@ -18,8 +18,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.TimeoutException; -import javax.annotation.Nullable; import okhttp3.ResponseBody; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/core/src/main/java/com/google/adk/summarizer/EventsCompactionConfig.java b/core/src/main/java/com/google/adk/summarizer/EventsCompactionConfig.java index 39698c3db..db462466c 100644 --- a/core/src/main/java/com/google/adk/summarizer/EventsCompactionConfig.java +++ b/core/src/main/java/com/google/adk/summarizer/EventsCompactionConfig.java @@ -18,7 +18,7 @@ import com.google.auto.value.AutoBuilder; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** * Configuration for event compaction. diff --git a/core/src/main/java/com/google/adk/tools/BaseTool.java b/core/src/main/java/com/google/adk/tools/BaseTool.java index 01a399920..e7c15af20 100644 --- a/core/src/main/java/com/google/adk/tools/BaseTool.java +++ b/core/src/main/java/com/google/adk/tools/BaseTool.java @@ -40,7 +40,6 @@ import java.util.Map; import java.util.Optional; import java.util.function.Function; -import javax.annotation.Nonnull; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,11 +51,11 @@ public abstract class BaseTool { private final boolean isLongRunning; private final HashMap customMetadata; - protected BaseTool(@Nonnull String name, @Nonnull String description) { + protected BaseTool(String name, String description) { this(name, description, /* isLongRunning= */ false); } - protected BaseTool(@Nonnull String name, @Nonnull String description, boolean isLongRunning) { + protected BaseTool(String name, String description, boolean isLongRunning) { this.name = name; this.description = description; this.isLongRunning = isLongRunning; diff --git a/core/src/main/java/com/google/adk/tools/BaseToolset.java b/core/src/main/java/com/google/adk/tools/BaseToolset.java index 40167aa75..76369e5b9 100644 --- a/core/src/main/java/com/google/adk/tools/BaseToolset.java +++ b/core/src/main/java/com/google/adk/tools/BaseToolset.java @@ -19,7 +19,7 @@ import com.google.adk.agents.ReadonlyContext; import io.reactivex.rxjava3.core.Flowable; import java.util.List; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** Base interface for toolsets. */ public interface BaseToolset extends AutoCloseable { diff --git a/core/src/main/java/com/google/adk/tools/ExampleTool.java b/core/src/main/java/com/google/adk/tools/ExampleTool.java index d03c2e4f1..c11259c52 100644 --- a/core/src/main/java/com/google/adk/tools/ExampleTool.java +++ b/core/src/main/java/com/google/adk/tools/ExampleTool.java @@ -102,7 +102,7 @@ public static ExampleTool fromConfig(ToolArgsConfig args, String configAbsPath) var maybeExamplesProvider = args.getOrEmpty("examples", new TypeReference() {}); if (maybeExamplesProvider.isPresent()) { BaseExampleProvider provider = resolveExampleProvider(maybeExamplesProvider.get()); - return ExampleTool.builder().setExampleProvider(provider).build(); + return ExampleTool.builder().exampleProvider(provider).build(); } var maybeListOfExamples = args.getOrEmpty("examples", new TypeReference>() {}); if (maybeListOfExamples.isPresent()) { @@ -166,14 +166,26 @@ public static final class Builder { private String description = "Adds few-shot examples to the request"; private Optional provider = Optional.empty(); + @Deprecated @CanIgnoreReturnValue - public Builder setName(String name) { + public final Builder setName(String name) { + return name(name); + } + + @CanIgnoreReturnValue + public Builder name(String name) { this.name = name; return this; } + @Deprecated @CanIgnoreReturnValue - public Builder setDescription(String description) { + public final Builder setDescription(String description) { + return description(description); + } + + @CanIgnoreReturnValue + public Builder description(String description) { this.description = description; return this; } @@ -184,8 +196,14 @@ public Builder addExample(Example ex) { return this; } + @Deprecated + @CanIgnoreReturnValue + public final Builder setExampleProvider(BaseExampleProvider provider) { + return exampleProvider(provider); + } + @CanIgnoreReturnValue - public Builder setExampleProvider(BaseExampleProvider provider) { + public Builder exampleProvider(BaseExampleProvider provider) { this.provider = Optional.ofNullable(provider); return this; } diff --git a/core/src/main/java/com/google/adk/tools/FunctionTool.java b/core/src/main/java/com/google/adk/tools/FunctionTool.java index 4323b4569..ea67d40d4 100644 --- a/core/src/main/java/com/google/adk/tools/FunctionTool.java +++ b/core/src/main/java/com/google/adk/tools/FunctionTool.java @@ -36,7 +36,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -222,8 +222,7 @@ public Method func() { } /** Returns the underlying function's {@link Object} instance if present. */ - @Nullable - Object instance() { + @Nullable Object instance() { return instance; } diff --git a/core/src/main/java/com/google/adk/tools/LongRunningFunctionTool.java b/core/src/main/java/com/google/adk/tools/LongRunningFunctionTool.java index 68b7c242b..23733c4eb 100644 --- a/core/src/main/java/com/google/adk/tools/LongRunningFunctionTool.java +++ b/core/src/main/java/com/google/adk/tools/LongRunningFunctionTool.java @@ -19,7 +19,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.google.adk.utils.ComponentRegistry; import java.lang.reflect.Method; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** A function tool that returns the result asynchronously. */ public class LongRunningFunctionTool extends FunctionTool { diff --git a/core/src/main/java/com/google/adk/tools/SetModelResponseTool.java b/core/src/main/java/com/google/adk/tools/SetModelResponseTool.java index 3b0e411b4..94569dd7d 100644 --- a/core/src/main/java/com/google/adk/tools/SetModelResponseTool.java +++ b/core/src/main/java/com/google/adk/tools/SetModelResponseTool.java @@ -22,7 +22,6 @@ import io.reactivex.rxjava3.core.Single; import java.util.Map; import java.util.Optional; -import javax.annotation.Nonnull; /** * Internal tool used for output schema workaround. @@ -36,7 +35,7 @@ public class SetModelResponseTool extends BaseTool { private final Schema outputSchema; - public SetModelResponseTool(@Nonnull Schema outputSchema) { + public SetModelResponseTool(Schema outputSchema) { super( NAME, "Set your final response using the required output schema. " diff --git a/core/src/main/java/com/google/adk/tools/ToolContext.java b/core/src/main/java/com/google/adk/tools/ToolContext.java index 5192d19ff..974d1f017 100644 --- a/core/src/main/java/com/google/adk/tools/ToolContext.java +++ b/core/src/main/java/com/google/adk/tools/ToolContext.java @@ -24,7 +24,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import io.reactivex.rxjava3.core.Single; import java.util.Optional; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** ToolContext object provides a structured context for executing tools or functions. */ public class ToolContext extends CallbackContext { diff --git a/core/src/main/java/com/google/adk/tools/ToolPredicate.java b/core/src/main/java/com/google/adk/tools/ToolPredicate.java index 6adf53c18..5c8dd9893 100644 --- a/core/src/main/java/com/google/adk/tools/ToolPredicate.java +++ b/core/src/main/java/com/google/adk/tools/ToolPredicate.java @@ -18,7 +18,7 @@ import com.google.adk.agents.ReadonlyContext; import java.util.Optional; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** * Functional interface to decide whether a tool should be exposed to the LLM based on the current diff --git a/core/src/main/java/com/google/adk/tools/applicationintegrationtoolset/CredentialsHelper.java b/core/src/main/java/com/google/adk/tools/applicationintegrationtoolset/CredentialsHelper.java index 60bf6fc8d..c6c26d0af 100644 --- a/core/src/main/java/com/google/adk/tools/applicationintegrationtoolset/CredentialsHelper.java +++ b/core/src/main/java/com/google/adk/tools/applicationintegrationtoolset/CredentialsHelper.java @@ -5,7 +5,7 @@ import java.net.http.HttpRequest; import java.util.List; import java.util.Map; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** * This interface provides a method to convert a service account JSON string to a Google Credentials diff --git a/core/src/main/java/com/google/adk/tools/applicationintegrationtoolset/GoogleCredentialsHelper.java b/core/src/main/java/com/google/adk/tools/applicationintegrationtoolset/GoogleCredentialsHelper.java index ca036d21b..c78d91159 100644 --- a/core/src/main/java/com/google/adk/tools/applicationintegrationtoolset/GoogleCredentialsHelper.java +++ b/core/src/main/java/com/google/adk/tools/applicationintegrationtoolset/GoogleCredentialsHelper.java @@ -7,7 +7,7 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; public final class GoogleCredentialsHelper implements CredentialsHelper { diff --git a/core/src/main/java/com/google/adk/tools/mcp/AbstractMcpTool.java b/core/src/main/java/com/google/adk/tools/mcp/AbstractMcpTool.java index 3b0c3d70a..d01b7c54e 100644 --- a/core/src/main/java/com/google/adk/tools/mcp/AbstractMcpTool.java +++ b/core/src/main/java/com/google/adk/tools/mcp/AbstractMcpTool.java @@ -16,6 +16,7 @@ package com.google.adk.tools.mcp; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.google.adk.tools.BaseTool; @@ -23,9 +24,13 @@ import com.google.common.collect.ImmutableMap; import com.google.genai.types.FunctionDeclaration; import io.modelcontextprotocol.spec.McpSchema.CallToolResult; +import io.modelcontextprotocol.spec.McpSchema.Content; import io.modelcontextprotocol.spec.McpSchema.JsonSchema; +import io.modelcontextprotocol.spec.McpSchema.TextContent; import io.modelcontextprotocol.spec.McpSchema.Tool; import io.modelcontextprotocol.spec.McpSchema.ToolAnnotations; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -110,7 +115,51 @@ protected static Map wrapCallResult( if (callResult == null) { return ImmutableMap.of("error", "MCP framework error: CallToolResult was null"); } + List contents = callResult.content(); + Boolean isToolError = callResult.isError(); - return objectMapper.convertValue(callResult, new TypeReference>() {}); + if (isToolError != null && isToolError) { + String errorMessage = "Tool execution failed."; + if (contents != null + && !contents.isEmpty() + && contents.get(0) instanceof TextContent textContent) { + if (textContent.text() != null && !textContent.text().isEmpty()) { + errorMessage += " Details: " + textContent.text(); + } + } + return ImmutableMap.of("error", errorMessage); + } + + if (contents == null || contents.isEmpty()) { + return ImmutableMap.of(); + } + + List textOutputs = new ArrayList<>(); + for (Content content : contents) { + if (content instanceof TextContent textContent) { + if (textContent.text() != null) { + textOutputs.add(textContent.text()); + } + } + } + + if (textOutputs.isEmpty()) { + return ImmutableMap.of( + "error", + "Tool '" + mcpToolName + "' returned content that is not TextContent.", + "content_details", + contents.toString()); + } + + List> resultMaps = new ArrayList<>(); + for (String textOutput : textOutputs) { + try { + resultMaps.add( + objectMapper.readValue(textOutput, new TypeReference>() {})); + } catch (JsonProcessingException e) { + resultMaps.add(ImmutableMap.of("text", textOutput)); + } + } + return ImmutableMap.of("text_output", resultMaps); } } diff --git a/core/src/main/java/com/google/adk/tools/mcp/McpAsyncToolset.java b/core/src/main/java/com/google/adk/tools/mcp/McpAsyncToolset.java index bcc786d69..5f4c2164b 100644 --- a/core/src/main/java/com/google/adk/tools/mcp/McpAsyncToolset.java +++ b/core/src/main/java/com/google/adk/tools/mcp/McpAsyncToolset.java @@ -35,7 +35,7 @@ import java.util.List; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Mono; diff --git a/core/src/main/java/com/google/adk/tools/mcp/SseServerParameters.java b/core/src/main/java/com/google/adk/tools/mcp/SseServerParameters.java index 67ea7ef4b..3b12f7064 100644 --- a/core/src/main/java/com/google/adk/tools/mcp/SseServerParameters.java +++ b/core/src/main/java/com/google/adk/tools/mcp/SseServerParameters.java @@ -23,7 +23,7 @@ import com.google.common.collect.ImmutableMap; import java.time.Duration; import java.util.Map; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** Parameters for establishing a MCP Server-Sent Events (SSE) connection. */ @AutoValue diff --git a/core/src/main/java/com/google/adk/tools/mcp/StdioServerParameters.java b/core/src/main/java/com/google/adk/tools/mcp/StdioServerParameters.java index b5632594f..50c07c840 100644 --- a/core/src/main/java/com/google/adk/tools/mcp/StdioServerParameters.java +++ b/core/src/main/java/com/google/adk/tools/mcp/StdioServerParameters.java @@ -25,7 +25,7 @@ import io.modelcontextprotocol.client.transport.ServerParameters; import java.util.List; import java.util.Map; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** Parameters for establishing a MCP stdio connection. */ @AutoValue diff --git a/core/src/main/java/com/google/adk/tools/mcp/StreamableHttpServerParameters.java b/core/src/main/java/com/google/adk/tools/mcp/StreamableHttpServerParameters.java index f09c8c5e7..e9f8a3ac8 100644 --- a/core/src/main/java/com/google/adk/tools/mcp/StreamableHttpServerParameters.java +++ b/core/src/main/java/com/google/adk/tools/mcp/StreamableHttpServerParameters.java @@ -21,7 +21,7 @@ import java.time.Duration; import java.util.Collections; import java.util.Map; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** Server parameters for Streamable HTTP client transport. */ public class StreamableHttpServerParameters { diff --git a/core/src/main/java/com/google/adk/tools/retrieval/VertexAiRagRetrieval.java b/core/src/main/java/com/google/adk/tools/retrieval/VertexAiRagRetrieval.java index 16f11a1f8..a2720aae5 100644 --- a/core/src/main/java/com/google/adk/tools/retrieval/VertexAiRagRetrieval.java +++ b/core/src/main/java/com/google/adk/tools/retrieval/VertexAiRagRetrieval.java @@ -39,8 +39,7 @@ import io.reactivex.rxjava3.core.Single; import java.util.List; import java.util.Map; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -60,10 +59,10 @@ public class VertexAiRagRetrieval extends BaseRetrievalTool { private final RetrieveContextsRequest.VertexRagStore apiVertexRagStore; public VertexAiRagRetrieval( - @Nonnull String name, - @Nonnull String description, - @Nonnull VertexRagServiceClient vertexRagServiceClient, - @Nonnull String parent, + String name, + String description, + VertexRagServiceClient vertexRagServiceClient, + String parent, @Nullable List ragResources, @Nullable Double vectorDistanceThreshold) { super(name, description); diff --git a/core/src/test/java/com/google/adk/agents/ParallelAgentEscalationTest.java b/core/src/test/java/com/google/adk/agents/ParallelAgentEscalationTest.java new file mode 100644 index 000000000..42db353c8 --- /dev/null +++ b/core/src/test/java/com/google/adk/agents/ParallelAgentEscalationTest.java @@ -0,0 +1,137 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.adk.agents; + +import static com.google.adk.testing.TestUtils.createInvocationContext; +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import com.google.adk.events.Event; +import com.google.adk.events.EventActions; +import com.google.common.collect.ImmutableList; +import com.google.genai.types.Content; +import com.google.genai.types.Part; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Scheduler; +import io.reactivex.rxjava3.schedulers.TestScheduler; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class ParallelAgentEscalationTest { + + static class TestAgent extends BaseAgent { + private final long delayMillis; + private final Scheduler scheduler; + private final String content; + private final EventActions actions; + + private TestAgent(String name, long delayMillis, Scheduler scheduler, String content) { + this(name, delayMillis, scheduler, content, null); + } + + private TestAgent( + String name, long delayMillis, Scheduler scheduler, String content, EventActions actions) { + super(name, "Test Agent", ImmutableList.of(), null, null); + this.delayMillis = delayMillis; + this.scheduler = scheduler; + this.content = content; + this.actions = actions; + } + + @Override + protected Flowable runAsyncImpl(InvocationContext invocationContext) { + Flowable event = + Flowable.fromCallable( + () -> { + Event.Builder builder = + Event.builder() + .author(name()) + .branch(invocationContext.branch().orElse(null)) + .invocationId(invocationContext.invocationId()) + .content(Content.fromParts(Part.fromText(content))); + + if (actions != null) { + builder.actions(actions); + } + return builder.build(); + }); + + if (delayMillis > 0) { + return event.delay(delayMillis, MILLISECONDS, scheduler); + } + return event; + } + + @Override + protected Flowable runLiveImpl(InvocationContext invocationContext) { + throw new UnsupportedOperationException("Not implemented"); + } + } + + @Test + public void runAsync_escalationEvent_shortCircuitsOtherAgents() { + TestScheduler testScheduler = new TestScheduler(); + + TestAgent escalatingAgent = + new TestAgent( + "escalating_agent", + 100, + testScheduler, + "Escalating!", + EventActions.builder().escalate(true).build()); + TestAgent slowAgent = new TestAgent("slow_agent", 500, testScheduler, "Finished"); + TestAgent fastAgent = new TestAgent("fast_agent", 50, testScheduler, "Finished"); + + ParallelAgent parallelAgent = + ParallelAgent.builder() + .name("parallel_agent") + .subAgents(fastAgent, escalatingAgent, slowAgent) + .scheduler(testScheduler) + .build(); + + InvocationContext invocationContext = createInvocationContext(parallelAgent); + + var subscriber = parallelAgent.runAsync(invocationContext).test(); + + // Fast agent completes at 50ms (before the escalation) + testScheduler.advanceTimeBy(50, MILLISECONDS); + subscriber.assertValueCount(1); + assertThat(subscriber.values().get(0).author()).isEqualTo("fast_agent"); + + // Escalating agent completes at 100ms + testScheduler.advanceTimeBy(50, MILLISECONDS); + subscriber.assertValueCount(2); + + Event event1 = subscriber.values().get(0); + assertThat(event1.author()).isEqualTo("fast_agent"); + + Event event2 = subscriber.values().get(1); + assertThat(event2.author()).isEqualTo("escalating_agent"); + assertThat(event2.actions().escalate()).hasValue(true); + + subscriber.assertComplete(); + + // Slow agent would complete at 500ms, but test scheduler advances time to prove + // sequence was forcibly terminated! + testScheduler.advanceTimeBy(400, MILLISECONDS); + + // Test RxJava Disposal behavior: SlowAgent won't emit anything + subscriber.assertValueCount(2); + } +} diff --git a/core/src/test/java/com/google/adk/flows/llmflows/ToolRequestConfirmationActionTest.java b/core/src/test/java/com/google/adk/flows/llmflows/ToolRequestConfirmationActionTest.java new file mode 100644 index 000000000..6e34095c2 --- /dev/null +++ b/core/src/test/java/com/google/adk/flows/llmflows/ToolRequestConfirmationActionTest.java @@ -0,0 +1,132 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.adk.flows.llmflows; + +import static com.google.adk.testing.TestUtils.createInvocationContext; +import static com.google.adk.testing.TestUtils.createLlmResponse; +import static com.google.adk.testing.TestUtils.createTestAgentBuilder; +import static com.google.adk.testing.TestUtils.createTestLlm; +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.truth.Truth.assertThat; + +import com.google.adk.agents.InvocationContext; +import com.google.adk.agents.LlmAgent; +import com.google.adk.events.Event; +import com.google.adk.runner.InMemoryRunner; +import com.google.adk.runner.Runner; +import com.google.adk.sessions.Session; +import com.google.adk.tools.BaseTool; +import com.google.adk.tools.ToolContext; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.genai.types.Content; +import com.google.genai.types.FunctionDeclaration; +import com.google.genai.types.Part; +import com.google.genai.types.Schema; +import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.core.Single; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class ToolRequestConfirmationActionTest { + + private static class ToolRequestConfirmationTool extends BaseTool { + ToolRequestConfirmationTool() { + super("request_confirmation_tool", "Requests confirmation."); + } + + @Override + public Optional declaration() { + return Optional.of( + FunctionDeclaration.builder() + .name(name()) + .description(description()) + .parameters(Schema.builder().type("OBJECT").build()) // No parameters needed + .build()); + } + + @Override + public Single> runAsync(Map args, ToolContext toolContext) { + toolContext.requestConfirmation("Please confirm this action"); + return Single.just(ImmutableMap.of()); + } + } + + @Test + public void toolRequestConfirmation_eventHasNonNullId() { + Content requestConfirmationCallContent = + Content.fromParts(Part.fromFunctionCall("request_confirmation_tool", ImmutableMap.of())); + Content response1 = Content.fromParts(Part.fromText("response1")); + Content response2 = Content.fromParts(Part.fromText("response2")); + + var testLlm = + createTestLlm( + Flowable.just(createLlmResponse(requestConfirmationCallContent)), + Flowable.just(createLlmResponse(response1)), + Flowable.just(createLlmResponse(response2))); + + LlmAgent rootAgent = + createTestAgentBuilder(testLlm) + .name("root_agent") + .tools(ImmutableList.of(new ToolRequestConfirmationTool())) + .build(); + InvocationContext invocationContext = createInvocationContext(rootAgent); + + Runner runner = getRunnerAndCreateSession(rootAgent, invocationContext.session()); + + ImmutableList confirmationEvents = + runRunner(runner, invocationContext).stream() + .filter( + e -> + e.functionCalls().stream() + .anyMatch( + f -> + Objects.equals( + f.name().orElse(""), + Functions.REQUEST_CONFIRMATION_FUNCTION_CALL_NAME))) + .collect(toImmutableList()); + + assertThat(confirmationEvents).isNotEmpty(); + assertThat(confirmationEvents.get(0).id()).isNotNull(); + } + + private Runner getRunnerAndCreateSession(LlmAgent agent, Session session) { + Runner runner = new InMemoryRunner(agent, session.appName()); + + var unused = + runner + .sessionService() + .createSession(session.appName(), session.userId(), session.state(), session.id()) + .blockingGet(); + + return runner; + } + + private List runRunner(Runner runner, InvocationContext invocationContext) { + Session session = invocationContext.session(); + return runner + .runAsync(session.userId(), session.id(), invocationContext.userContent().orElse(null)) + .toList() + .blockingGet(); + } +} diff --git a/core/src/test/java/com/google/adk/models/ChatCompletionsResponseTest.java b/core/src/test/java/com/google/adk/models/ChatCompletionsResponseTest.java new file mode 100644 index 000000000..53fcdfbdf --- /dev/null +++ b/core/src/test/java/com/google/adk/models/ChatCompletionsResponseTest.java @@ -0,0 +1,328 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.adk.models; + +import static com.google.common.truth.Truth.assertThat; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.adk.models.ChatCompletionsResponse.ChatCompletion; +import com.google.adk.models.ChatCompletionsResponse.ChatCompletionChunk; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class ChatCompletionsResponseTest { + + private ObjectMapper objectMapper; + + @Before + public void setUp() { + objectMapper = new ObjectMapper(); + } + + @Test + public void testDeserializeChatCompletion_standardResponse() throws Exception { + String json = + """ + { + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1677652288, + "model": "gpt-4o-mini", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": "Hello!" + }, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 9, + "completion_tokens": 12, + "total_tokens": 21 + } + } + """; + + ChatCompletion completion = objectMapper.readValue(json, ChatCompletion.class); + + assertThat(completion.id).isEqualTo("chatcmpl-123"); + assertThat(completion.object).isEqualTo("chat.completion"); + assertThat(completion.created).isEqualTo(1677652288L); + assertThat(completion.model).isEqualTo("gpt-4o-mini"); + assertThat(completion.choices).hasSize(1); + assertThat(completion.choices.get(0).index).isEqualTo(0); + assertThat(completion.choices.get(0).message.role).isEqualTo("assistant"); + assertThat(completion.choices.get(0).message.content).isEqualTo("Hello!"); + assertThat(completion.choices.get(0).finishReason).isEqualTo("stop"); + assertThat(completion.usage.promptTokens).isEqualTo(9); + assertThat(completion.usage.completionTokens).isEqualTo(12); + assertThat(completion.usage.totalTokens).isEqualTo(21); + } + + @Test + public void testDeserializeChatCompletion_withFunctionCallFallback() throws Exception { + String json = + """ + { + "id": "chatcmpl-123", + "choices": [{ + "message": { + "role": "assistant", + "function_call": { + "name": "get_current_weather", + "arguments": "{\\"location\\": \\"Boston\\"}" + } + } + }] + } + """; + + ChatCompletion completion = objectMapper.readValue(json, ChatCompletion.class); + + assertThat(completion.choices.get(0).message.functionCall).isNotNull(); + assertThat(completion.choices.get(0).message.functionCall.name) + .isEqualTo("get_current_weather"); + assertThat(completion.choices.get(0).message.functionCall.arguments) + .isEqualTo("{\"location\": \"Boston\"}"); + } + + @Test + public void testDeserializeChatCompletion_withThoughtSignatureAndGeminiTokens() throws Exception { + String json = + """ + { + "choices": [{ + "message": { + "role": "assistant", + "tool_calls": [{ + "id": "call_abc", + "type": "function", + "extra_content": { + "google": { + "thought_signature": "c2lnbmF0dXJl" + } + } + }] + } + }], + "usage": { + "thoughts_token_count": 50 + } + } + """; + + ChatCompletion completion = objectMapper.readValue(json, ChatCompletion.class); + + assertThat(completion.choices.get(0).message.toolCalls).hasSize(1); + assertThat(completion.choices.get(0).message.toolCalls.get(0).extraContent).isNotNull(); + Map extraContentMap = + (Map) completion.choices.get(0).message.toolCalls.get(0).extraContent; + @SuppressWarnings("unchecked") // This code won't run in production and it's is a JSON object. + Map googleMap = (Map) extraContentMap.get("google"); + assertThat(googleMap.get("thought_signature")).isEqualTo("c2lnbmF0dXJl"); + assertThat(completion.usage.thoughtsTokenCount).isEqualTo(50); + } + + @Test + public void testDeserializeChatCompletion_withArbitraryExtraContent() throws Exception { + String json = + """ + { + "choices": [{ + "message": { + "role": "assistant", + "tool_calls": [{ + "id": "call_abc", + "type": "function", + "extra_content": { + "custom_key": "custom_value", + "nested": { + "key": 123 + } + } + }] + } + }] + } + """; + + ChatCompletion got = objectMapper.readValue(json, ChatCompletion.class); + + assertThat(got.choices.get(0).message.toolCalls).hasSize(1); + Map extraContent = + (Map) got.choices.get(0).message.toolCalls.get(0).extraContent; + assertThat(extraContent.get("custom_key")).isEqualTo("custom_value"); + @SuppressWarnings("unchecked") // This code won't run in production and it's is a JSON object. + Map nested = (Map) extraContent.get("nested"); + assertThat(nested.get("key")).isEqualTo(123); + } + + @Test + public void testDeserializeChatCompletion_withAudio() throws Exception { + String json = + """ + { + "choices": [{ + "message": { + "role": "assistant", + "content": "Hello", + "annotations": [{ + "type": "url_citation", + "url_citation": { + "end_index": 5, + "start_index": 0, + "title": "Example Title", + "url": "https://example.com" + } + }], + "audio": { + "id": "audio_123", + "data": "base64data", + "expires_at": 1234567890, + "transcript": "Hello" + } + } + }] + } + """; + + ChatCompletionsResponse.ChatCompletion completion = + objectMapper.readValue(json, ChatCompletionsResponse.ChatCompletion.class); + + assertThat(completion.choices.get(0).message.annotations).hasSize(1); + ChatCompletionsResponse.Annotation annotation = + completion.choices.get(0).message.annotations.get(0); + assertThat(annotation.type).isEqualTo("url_citation"); + assertThat(annotation.urlCitation.title).isEqualTo("Example Title"); + assertThat(annotation.urlCitation.url).isEqualTo("https://example.com"); + + assertThat(completion.choices.get(0).message.audio).isNotNull(); + assertThat(completion.choices.get(0).message.audio.id).isEqualTo("audio_123"); + assertThat(completion.choices.get(0).message.audio.data).isEqualTo("base64data"); + } + + @Test + public void testDeserializeChatCompletion_withCustomToolCall() throws Exception { + String json = + """ + { + "choices": [{ + "message": { + "role": "assistant", + "tool_calls": [{ + "id": "call_custom", + "type": "custom", + "custom": { + "input": "{\\\"arg\\\":\\\"val\\\"}", + "name": "custom_tool" + } + }] + } + }] + } + """; + + ChatCompletionsResponse.ChatCompletion completion = + objectMapper.readValue(json, ChatCompletionsResponse.ChatCompletion.class); + + assertThat(completion.choices.get(0).message.toolCalls).hasSize(1); + ChatCompletionsResponse.ToolCall toolCall = completion.choices.get(0).message.toolCalls.get(0); + assertThat(toolCall.type).isEqualTo("custom"); + assertThat(toolCall.custom.name).isEqualTo("custom_tool"); + assertThat(toolCall.custom.input).isEqualTo("{\"arg\":\"val\"}"); + } + + @Test + public void testDeserializeChatCompletionChunk_streamingResponse() throws Exception { + String json = + """ + { + "id": "chatcmpl-123", + "object": "chat.completion.chunk", + "created": 1694268190, + "choices": [{ + "index": 0, + "delta": { + "content": "Hello" + } + }] + } + """; + + ChatCompletionChunk chunk = objectMapper.readValue(json, ChatCompletionChunk.class); + + assertThat(chunk.id).isEqualTo("chatcmpl-123"); + assertThat(chunk.object).isEqualTo("chat.completion.chunk"); + assertThat(chunk.choices).hasSize(1); + assertThat(chunk.choices.get(0).delta.content).isEqualTo("Hello"); + } + + @Test + public void testDeserializeChatCompletionChunk_withToolCallDelta() throws Exception { + String json = + """ + { + "choices": [{ + "delta": { + "tool_calls": [{ + "index": 1, + "id": "call_abc", + "type": "function", + "function": { + "name": "get_weather", + "arguments": "{\\\"location\\\":\\\"Boston\\\"}" + }, + "extra_content": { + "google": { + "thought_signature": "sig" + } + } + }] + } + }], + "usage": { + "completion_tokens": 10, + "prompt_tokens": 5, + "total_tokens": 15 + } + } + """; + + ChatCompletionChunk chunk = objectMapper.readValue(json, ChatCompletionChunk.class); + + assertThat(chunk.choices.get(0).delta.toolCalls).hasSize(1); + ChatCompletionsResponse.ToolCall toolCall = chunk.choices.get(0).delta.toolCalls.get(0); + assertThat(toolCall.index).isEqualTo(1); + assertThat(toolCall.id).isEqualTo("call_abc"); + assertThat(toolCall.type).isEqualTo("function"); + assertThat(toolCall.function.name).isEqualTo("get_weather"); + assertThat(toolCall.function.arguments).isEqualTo("{\"location\":\"Boston\"}"); + @SuppressWarnings("unchecked") // This code won't run in production and it's is a JSON object. + Map google = (Map) toolCall.extraContent.get("google"); + assertThat(google).containsEntry("thought_signature", "sig"); + + assertThat(chunk.usage).isNotNull(); + assertThat(chunk.usage.completionTokens).isEqualTo(10); + assertThat(chunk.usage.promptTokens).isEqualTo(5); + assertThat(chunk.usage.totalTokens).isEqualTo(15); + } +} diff --git a/core/src/test/java/com/google/adk/models/ClaudeTest.java b/core/src/test/java/com/google/adk/models/ClaudeTest.java index 677d40627..febcaf4be 100644 --- a/core/src/test/java/com/google/adk/models/ClaudeTest.java +++ b/core/src/test/java/com/google/adk/models/ClaudeTest.java @@ -21,7 +21,6 @@ import com.anthropic.client.AnthropicClient; import com.anthropic.models.messages.ContentBlockParam; import com.anthropic.models.messages.ToolResultBlockParam; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.genai.types.FunctionResponse; import com.google.genai.types.Part; @@ -51,11 +50,9 @@ public void setUp() throws Exception { } @Test - public void testPartToAnthropicMessageBlock_mcpNativeFormat() throws Exception { + public void testPartToAnthropicMessageBlock_mcpTool_legacyTextOutputKey() throws Exception { Map responseData = - ImmutableMap.of( - "content", - ImmutableList.of(ImmutableMap.of("type", "text", "text", "Extracted native MCP text"))); + ImmutableMap.of("text_output", ImmutableMap.of("text", "Legacy result text")); FunctionResponse funcParam = FunctionResponse.builder().name("test_tool").response(responseData).id("call_123").build(); Part part = Part.builder().functionResponse(funcParam).build(); @@ -64,21 +61,8 @@ public void testPartToAnthropicMessageBlock_mcpNativeFormat() throws Exception { (ContentBlockParam) partToAnthropicMessageBlockMethod.invoke(claude, part); ToolResultBlockParam toolResult = result.asToolResult(); - assertThat(toolResult.content().get().asString()).isEqualTo("Extracted native MCP text"); - } - - @Test - public void testPartToAnthropicMessageBlock_legacyResultKey() throws Exception { - Map responseData = ImmutableMap.of("result", "Legacy result text"); - FunctionResponse funcParam = - FunctionResponse.builder().name("test_tool").response(responseData).id("call_123").build(); - Part part = Part.builder().functionResponse(funcParam).build(); - - ContentBlockParam result = - (ContentBlockParam) partToAnthropicMessageBlockMethod.invoke(claude, part); - - ToolResultBlockParam toolResult = result.asToolResult(); - assertThat(toolResult.content().get().asString()).isEqualTo("Legacy result text"); + assertThat(toolResult.content().get().asString()) + .isEqualTo("{\"text_output\":{\"text\":\"Legacy result text\"}}"); } @Test diff --git a/core/src/test/java/com/google/adk/plugins/agentanalytics/BigQueryAgentAnalyticsPluginTest.java b/core/src/test/java/com/google/adk/plugins/agentanalytics/BigQueryAgentAnalyticsPluginTest.java index 8147c5cc6..0822c2cae 100644 --- a/core/src/test/java/com/google/adk/plugins/agentanalytics/BigQueryAgentAnalyticsPluginTest.java +++ b/core/src/test/java/com/google/adk/plugins/agentanalytics/BigQueryAgentAnalyticsPluginTest.java @@ -96,15 +96,15 @@ public void setUp() throws Exception { fakeAgent = new FakeAgent("agent_name"); config = BigQueryLoggerConfig.builder() - .setEnabled(true) - .setProjectId("project") - .setDatasetId("dataset") - .setTableName("table") - .setBatchSize(10) - .setBatchFlushInterval(Duration.ofSeconds(10)) - .setAutoSchemaUpgrade(false) - .setCredentials(mock(Credentials.class)) - .setCustomTags(ImmutableMap.of("global_tag", "global_value")) + .enabled(true) + .projectId("project") + .datasetId("dataset") + .tableName("table") + .batchSize(10) + .batchFlushInterval(Duration.ofSeconds(10)) + .autoSchemaUpgrade(false) + .credentials(mock(Credentials.class)) + .customTags(ImmutableMap.of("global_tag", "global_value")) .build(); when(mockBigQuery.getOptions()) @@ -167,9 +167,9 @@ public void afterRunCallback_flushesAndAppends() throws Exception { public void getStreamName_returnsCorrectFormat() { BigQueryLoggerConfig config = BigQueryLoggerConfig.builder() - .setProjectId("test-project") - .setDatasetId("test-dataset") - .setTableName("test-table") + .projectId("test-project") + .datasetId("test-dataset") + .tableName("test-table") .build(); String streamName = plugin.getStreamName(config); diff --git a/core/src/test/java/com/google/adk/runner/RunnerTest.java b/core/src/test/java/com/google/adk/runner/RunnerTest.java index b68b6ff5f..36530faf2 100644 --- a/core/src/test/java/com/google/adk/runner/RunnerTest.java +++ b/core/src/test/java/com/google/adk/runner/RunnerTest.java @@ -591,6 +591,29 @@ public void onEventCallback_success() { verify(plugin).onEventCallback(any(), any()); } + @Test + public void callbackContextData_preservedAcrossInvocation() { + String testKey = "testKey"; + String testValue = "testValue"; + + when(plugin.onUserMessageCallback(any(), any())) + .thenAnswer( + invocation -> { + InvocationContext context = invocation.getArgument(0); + context.callbackContextData().put(testKey, testValue); + return Maybe.empty(); + }); + + ArgumentCaptor contextCaptor = + ArgumentCaptor.forClass(InvocationContext.class); + when(plugin.afterRunCallback(contextCaptor.capture())).thenReturn(Completable.complete()); + + var unused = + runner.runAsync("user", session.id(), createContent("test")).toList().blockingGet(); + + assertThat(contextCaptor.getValue().callbackContextData()).containsEntry(testKey, testValue); + } + @Test public void runAsync_withSessionKey_success() { var events = diff --git a/core/src/test/java/com/google/adk/sessions/InMemorySessionServiceTest.java b/core/src/test/java/com/google/adk/sessions/InMemorySessionServiceTest.java index 0d9235b1b..6a271efac 100644 --- a/core/src/test/java/com/google/adk/sessions/InMemorySessionServiceTest.java +++ b/core/src/test/java/com/google/adk/sessions/InMemorySessionServiceTest.java @@ -20,6 +20,7 @@ import com.google.adk.events.Event; import com.google.adk.events.EventActions; import io.reactivex.rxjava3.core.Single; +import java.lang.reflect.Field; import java.time.Instant; import java.util.HashMap; import java.util.Optional; @@ -266,4 +267,54 @@ public void sequentialAgents_shareTempState() { assertThat(retrievedSession.state()).doesNotContainKey("temp:agent1_output"); assertThat(retrievedSession.state()).containsEntry("temp:agent2_output", "processed_data"); } + + @Test + public void deleteSession_cleansUpEmptyParentMaps() throws Exception { + InMemorySessionService sessionService = new InMemorySessionService(); + + Session session = sessionService.createSession("app-name", "user-id").blockingGet(); + + sessionService.deleteSession(session.appName(), session.userId(), session.id()).blockingAwait(); + + // Use reflection to access the private 'sessions' field + Field field = InMemorySessionService.class.getDeclaredField("sessions"); + field.setAccessible(true); + ConcurrentMap sessions = (ConcurrentMap) field.get(sessionService); + + // After deleting the only session for "user-id" under "app-name", + // both the userId map and the appName map should have been removed + assertThat(sessions).isEmpty(); + } + + @Test + public void deleteSession_doesNotRemoveUserMapWhenOtherSessionsExist() throws Exception { + InMemorySessionService sessionService = new InMemorySessionService(); + + Session session1 = sessionService.createSession("app-name", "user-id").blockingGet(); + Session session2 = sessionService.createSession("app-name", "user-id").blockingGet(); + + // Delete only one of the two sessions + sessionService + .deleteSession(session1.appName(), session1.userId(), session1.id()) + .blockingAwait(); + + // session2 should still be retrievable + assertThat( + sessionService + .getSession(session2.appName(), session2.userId(), session2.id(), Optional.empty()) + .blockingGet()) + .isNotNull(); + + // The userId entry should still exist (not pruned) because session2 remains + Field field = InMemorySessionService.class.getDeclaredField("sessions"); + field.setAccessible(true); + @SuppressWarnings("unchecked") + ConcurrentMap>> sessions = + (ConcurrentMap>>) + field.get(sessionService); + + assertThat(sessions.get("app-name")).isNotNull(); + assertThat(sessions.get("app-name").get("user-id")).isNotNull(); + assertThat(sessions.get("app-name").get("user-id")).hasSize(1); + } } diff --git a/core/src/test/java/com/google/adk/tools/ExampleToolTest.java b/core/src/test/java/com/google/adk/tools/ExampleToolTest.java index 55e5d8f93..e56afe60b 100644 --- a/core/src/test/java/com/google/adk/tools/ExampleToolTest.java +++ b/core/src/test/java/com/google/adk/tools/ExampleToolTest.java @@ -73,7 +73,7 @@ public void processLlmRequest_withInlineExamples_appendsFewShot() { @Test public void processLlmRequest_withProvider_appendsFewShot() { - ExampleTool tool = ExampleTool.builder().setExampleProvider(ProviderHolder.EXAMPLES).build(); + ExampleTool tool = ExampleTool.builder().exampleProvider(ProviderHolder.EXAMPLES).build(); InvocationContext ctx = buildInvocationContext(); LlmRequest.Builder builder = LlmRequest.builder().model("gemini-2.0-flash"); diff --git a/core/src/test/java/com/google/adk/tools/mcp/AbstractMcpToolTest.java b/core/src/test/java/com/google/adk/tools/mcp/AbstractMcpToolTest.java index e8d9ea631..7a8b97bda 100644 --- a/core/src/test/java/com/google/adk/tools/mcp/AbstractMcpToolTest.java +++ b/core/src/test/java/com/google/adk/tools/mcp/AbstractMcpToolTest.java @@ -49,14 +49,11 @@ public void testWrapCallResult_success() { Map map = AbstractMcpTool.wrapCallResult(objectMapper, "my_tool", result); - assertThat(map).containsKey("content"); - List content = (List) map.get("content"); + assertThat(map).containsKey("text_output"); + List content = (List) map.get("text_output"); assertThat(content).hasSize(1); Map contentItem = (Map) content.get(0); - assertThat(contentItem).containsEntry("type", "text"); assertThat(contentItem).containsEntry("text", "success"); - - assertThat(map).containsEntry("isError", false); } } diff --git a/dev/pom.xml b/dev/pom.xml index 5468a1187..7f246d2f6 100644 --- a/dev/pom.xml +++ b/dev/pom.xml @@ -18,7 +18,7 @@ com.google.adk google-adk-parent - 1.0.1-rc.1-SNAPSHOT + 1.0.1-SNAPSHOT google-adk-dev diff --git a/maven_plugin/examples/custom_tools/pom.xml b/maven_plugin/examples/custom_tools/pom.xml index aa273d732..9421f9bee 100644 --- a/maven_plugin/examples/custom_tools/pom.xml +++ b/maven_plugin/examples/custom_tools/pom.xml @@ -4,7 +4,7 @@ com.example custom-tools-example - 1.0.1-rc.1-SNAPSHOT + 1.0.1-SNAPSHOT jar ADK Custom Tools Example diff --git a/maven_plugin/examples/simple-agent/pom.xml b/maven_plugin/examples/simple-agent/pom.xml index 34aeb8c1c..514ce45a3 100644 --- a/maven_plugin/examples/simple-agent/pom.xml +++ b/maven_plugin/examples/simple-agent/pom.xml @@ -4,7 +4,7 @@ com.example simple-adk-agent - 1.0.1-rc.1-SNAPSHOT + 1.0.1-SNAPSHOT jar Simple ADK Agent Example diff --git a/maven_plugin/pom.xml b/maven_plugin/pom.xml index d0feb41e3..8cc365dfd 100644 --- a/maven_plugin/pom.xml +++ b/maven_plugin/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 1.0.1-rc.1-SNAPSHOT + 1.0.1-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index bdd2f33b1..b80734b2c 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ com.google.adk google-adk-parent - 1.0.1-rc.1-SNAPSHOT + 1.0.1-SNAPSHOT pom Google Agent Development Kit Maven Parent POM diff --git a/tutorials/city-time-weather/pom.xml b/tutorials/city-time-weather/pom.xml index 19ef08a2d..8d1cb7976 100644 --- a/tutorials/city-time-weather/pom.xml +++ b/tutorials/city-time-weather/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 1.0.1-rc.1-SNAPSHOT + 1.0.1-SNAPSHOT ../../pom.xml diff --git a/tutorials/live-audio-single-agent/pom.xml b/tutorials/live-audio-single-agent/pom.xml index 99243893b..0c3dc454b 100644 --- a/tutorials/live-audio-single-agent/pom.xml +++ b/tutorials/live-audio-single-agent/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 1.0.1-rc.1-SNAPSHOT + 1.0.1-SNAPSHOT ../../pom.xml