From 5ea48cd6f69cb5221317e6660f15c88053565de7 Mon Sep 17 00:00:00 2001 From: OwenDavisBC Date: Mon, 9 Feb 2026 13:43:42 -0700 Subject: [PATCH 01/10] ISSUE-777: Ensure token usage metadata included with streaming responses --- .../java/com/google/adk/models/Gemini.java | 25 ++- .../com/google/adk/models/GeminiTest.java | 191 ++++++++++++++++++ 2 files changed, 211 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/com/google/adk/models/Gemini.java b/core/src/main/java/com/google/adk/models/Gemini.java index 74cf78b98..6f145e1de 100644 --- a/core/src/main/java/com/google/adk/models/Gemini.java +++ b/core/src/main/java/com/google/adk/models/Gemini.java @@ -239,7 +239,7 @@ public Flowable generateContent(LlmRequest llmRequest, boolean stre p -> p.functionCall().isPresent() || p.functionResponse().isPresent() - || p.text().map(t -> !t.isBlank()).orElse(false))) + || p.text().isPresent())) .orElse(false)); } else { logger.debug("Sending generateContent request to model {}", effectiveModelName); @@ -272,11 +272,17 @@ static Flowable processRawResponses(Flowable 0 @@ -316,11 +322,20 @@ static Flowable processRawResponses(Flowable finalResponses = new ArrayList<>(); if (accumulatedThoughtText.length() > 0) { finalResponses.add( - thinkingResponseFromText(accumulatedThoughtText.toString())); + thinkingResponseFromText(accumulatedThoughtText.toString()).toBuilder() + .usageMetadata( + accumulatedText.length() > 0 + ? null + : finalRawResp.usageMetadata().orElse(null)) + .build()); } if (accumulatedText.length() > 0) { - finalResponses.add(responseFromText(accumulatedText.toString())); + finalResponses.add( + responseFromText(accumulatedText.toString()).toBuilder() + .usageMetadata(finalRawResp.usageMetadata().orElse(null)) + .build()); } + return Flowable.fromIterable(finalResponses); } return Flowable.empty(); diff --git a/core/src/test/java/com/google/adk/models/GeminiTest.java b/core/src/test/java/com/google/adk/models/GeminiTest.java index 07dd675e5..c230f5f68 100644 --- a/core/src/test/java/com/google/adk/models/GeminiTest.java +++ b/core/src/test/java/com/google/adk/models/GeminiTest.java @@ -22,6 +22,7 @@ import com.google.genai.types.Content; import com.google.genai.types.FinishReason; import com.google.genai.types.GenerateContentResponse; +import com.google.genai.types.GenerateContentResponseUsageMetadata; import com.google.genai.types.Part; import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.functions.Predicate; @@ -123,6 +124,76 @@ public void processRawResponses_textThenEmpty_emitsPartialTextThenFullTextAndEmp isEmptyResponse()); } + @Test + public void processRawResponses_withTextChunks_partialResponsesIncludeUsageMetadata() { + GenerateContentResponseUsageMetadata metadata1 = createUsageMetadata(5, 10, 15); + GenerateContentResponseUsageMetadata metadata2 = createUsageMetadata(5, 20, 25); + Flowable rawResponses = + Flowable.just( + toResponseWithText("Hello", metadata1), toResponseWithText(" world", metadata2)); + + Flowable llmResponses = Gemini.processRawResponses(rawResponses); + + assertLlmResponses( + llmResponses, + isPartialTextResponseWithUsageMetadata("Hello", metadata1), + isPartialTextResponseWithUsageMetadata(" world", metadata2)); + } + + @Test + public void processRawResponses_textAndStopReason_finalResponseIncludesUsageMetadata() { + GenerateContentResponseUsageMetadata metadata = createUsageMetadata(10, 20, 30); + Flowable rawResponses = + Flowable.just( + toResponseWithText("Hello"), + toResponseWithText(" world", FinishReason.Known.STOP, metadata)); + + Flowable llmResponses = Gemini.processRawResponses(rawResponses); + + assertLlmResponses( + llmResponses, + isPartialTextResponse("Hello"), + isPartialTextResponseWithUsageMetadata(" world", metadata), + isFinalTextResponseWithUsageMetadata("Hello world", metadata)); + } + + @Test + public void processRawResponses_thoughtChunksAndStop_includeUsageMetadata() { + GenerateContentResponseUsageMetadata metadata1 = createUsageMetadata(5, 10, 15); + GenerateContentResponseUsageMetadata metadata2 = createUsageMetadata(5, 20, 25); + Flowable rawResponses = + Flowable.just( + toResponseWithThoughtText("Thinking", metadata1), + toResponseWithThoughtText(" deeply", FinishReason.Known.STOP, metadata2)); + + Flowable llmResponses = Gemini.processRawResponses(rawResponses); + + assertLlmResponses( + llmResponses, + isPartialThoughtResponseWithUsageMetadata("Thinking", metadata1), + isPartialThoughtResponseWithUsageMetadata(" deeply", metadata2), + isFinalThoughtResponseWithUsageMetadata("Thinking deeply", metadata2)); + } + + @Test + public void processRawResponses_thoughtAndTextWithStop_onlyFinalTextIncludesUsageMetadata() { + GenerateContentResponseUsageMetadata metadata1 = createUsageMetadata(5, 5, 10); + GenerateContentResponseUsageMetadata metadata2 = createUsageMetadata(10, 20, 30); + Flowable rawResponses = + Flowable.just( + toResponseWithThoughtText("Thinking", metadata1), + toResponseWithText("Answer", FinishReason.Known.STOP, metadata2)); + + Flowable llmResponses = Gemini.processRawResponses(rawResponses); + + assertLlmResponses( + llmResponses, + isPartialThoughtResponseWithUsageMetadata("Thinking", metadata1), + isPartialTextResponseWithUsageMetadata("Answer", metadata2), + isFinalThoughtResponseWithNoUsageMetadata("Thinking"), + isFinalTextResponseWithUsageMetadata("Answer", metadata2)); + } + // Helper methods for assertions private void assertLlmResponses( @@ -170,6 +241,67 @@ private static Predicate isEmptyResponse() { }; } + private static Predicate isPartialTextResponseWithUsageMetadata( + String expectedText, GenerateContentResponseUsageMetadata expectedMetadata) { + return response -> { + assertThat(response.partial()).hasValue(true); + assertThat(GeminiUtil.getPart0FromLlmResponse(response).flatMap(Part::text).orElse("")) + .isEqualTo(expectedText); + assertThat(response.usageMetadata()).hasValue(expectedMetadata); + return true; + }; + } + + private static Predicate isPartialThoughtResponseWithUsageMetadata( + String expectedText, GenerateContentResponseUsageMetadata expectedMetadata) { + return response -> { + assertThat(response.partial()).hasValue(true); + assertThat(GeminiUtil.getPart0FromLlmResponse(response).flatMap(Part::text).orElse("")) + .isEqualTo(expectedText); + assertThat(GeminiUtil.getPart0FromLlmResponse(response).flatMap(Part::thought).orElse(false)) + .isTrue(); + assertThat(response.usageMetadata()).hasValue(expectedMetadata); + return true; + }; + } + + private static Predicate isFinalTextResponseWithUsageMetadata( + String expectedText, GenerateContentResponseUsageMetadata expectedMetadata) { + return response -> { + assertThat(response.partial()).isEmpty(); + assertThat(GeminiUtil.getPart0FromLlmResponse(response).flatMap(Part::text).orElse("")) + .isEqualTo(expectedText); + assertThat(response.usageMetadata()).hasValue(expectedMetadata); + return true; + }; + } + + private static Predicate isFinalThoughtResponseWithUsageMetadata( + String expectedText, GenerateContentResponseUsageMetadata expectedMetadata) { + return response -> { + assertThat(response.partial()).isEmpty(); + assertThat(GeminiUtil.getPart0FromLlmResponse(response).flatMap(Part::text).orElse("")) + .isEqualTo(expectedText); + assertThat(GeminiUtil.getPart0FromLlmResponse(response).flatMap(Part::thought).orElse(false)) + .isTrue(); + assertThat(response.usageMetadata()).hasValue(expectedMetadata); + return true; + }; + } + + private static Predicate isFinalThoughtResponseWithNoUsageMetadata( + String expectedText) { + return response -> { + assertThat(response.partial()).isEmpty(); + assertThat(GeminiUtil.getPart0FromLlmResponse(response).flatMap(Part::text).orElse("")) + .isEqualTo(expectedText); + assertThat(GeminiUtil.getPart0FromLlmResponse(response).flatMap(Part::thought).orElse(false)) + .isTrue(); + assertThat(response.usageMetadata()).isEmpty(); + return true; + }; + } + // Helper methods to create responses for testing private GenerateContentResponse toResponseWithText(String text) { @@ -191,4 +323,63 @@ private GenerateContentResponse toResponse(Part part) { private GenerateContentResponse toResponse(Candidate candidate) { return GenerateContentResponse.builder().candidates(candidate).build(); } + + private GenerateContentResponse toResponseWithText( + String text, GenerateContentResponseUsageMetadata usageMetadata) { + return GenerateContentResponse.builder() + .candidates( + Candidate.builder() + .content(Content.builder().parts(Part.fromText(text)).build()) + .build()) + .usageMetadata(usageMetadata) + .build(); + } + + private GenerateContentResponse toResponseWithText( + String text, + FinishReason.Known finishReason, + GenerateContentResponseUsageMetadata usageMetadata) { + return GenerateContentResponse.builder() + .candidates( + Candidate.builder() + .content(Content.builder().parts(Part.fromText(text)).build()) + .finishReason(new FinishReason(finishReason)) + .build()) + .usageMetadata(usageMetadata) + .build(); + } + + private GenerateContentResponse toResponseWithThoughtText( + String text, GenerateContentResponseUsageMetadata usageMetadata) { + Part thoughtPart = Part.fromText(text).toBuilder().thought(true).build(); + return GenerateContentResponse.builder() + .candidates( + Candidate.builder().content(Content.builder().parts(thoughtPart).build()).build()) + .usageMetadata(usageMetadata) + .build(); + } + + private GenerateContentResponse toResponseWithThoughtText( + String text, + FinishReason.Known finishReason, + GenerateContentResponseUsageMetadata usageMetadata) { + Part thoughtPart = Part.fromText(text).toBuilder().thought(true).build(); + return GenerateContentResponse.builder() + .candidates( + Candidate.builder() + .content(Content.builder().parts(thoughtPart).build()) + .finishReason(new FinishReason(finishReason)) + .build()) + .usageMetadata(usageMetadata) + .build(); + } + + private static GenerateContentResponseUsageMetadata createUsageMetadata( + int promptTokens, int candidateTokens, int totalTokens) { + return GenerateContentResponseUsageMetadata.builder() + .promptTokenCount(promptTokens) + .candidatesTokenCount(candidateTokens) + .totalTokenCount(totalTokens) + .build(); + } } From 4b9b99ae7149a465ba2ae9b7496e01f669786553 Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Fri, 20 Mar 2026 02:28:15 -0700 Subject: [PATCH 02/10] feat: update Session.state() and its builder to be of general Map types PiperOrigin-RevId: 886659065 --- core/src/main/java/com/google/adk/sessions/Session.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/com/google/adk/sessions/Session.java b/core/src/main/java/com/google/adk/sessions/Session.java index f8376589a..94504fd96 100644 --- a/core/src/main/java/com/google/adk/sessions/Session.java +++ b/core/src/main/java/com/google/adk/sessions/Session.java @@ -27,8 +27,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; /** A {@link Session} object that encapsulates the {@link State} and {@link Event}s of a session. */ @JsonDeserialize(builder = Session.Builder.class) @@ -101,7 +101,7 @@ public Builder state(State state) { @CanIgnoreReturnValue @JsonProperty("state") - public Builder state(ConcurrentMap state) { + public Builder state(Map state) { this.state = new State(state); return this; } @@ -162,7 +162,7 @@ public String id() { } @JsonProperty("state") - public ConcurrentMap state() { + public Map state() { return state; } From 8ba4bfed3fa7045f3344329de7a39acddc64ee30 Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Fri, 20 Mar 2026 05:09:58 -0700 Subject: [PATCH 03/10] feat: Update return type of App.plugins() from ImmutableList to List PiperOrigin-RevId: 886722180 --- core/src/main/java/com/google/adk/apps/App.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 897e24490..a087b738c 100644 --- a/core/src/main/java/com/google/adk/apps/App.java +++ b/core/src/main/java/com/google/adk/apps/App.java @@ -64,7 +64,7 @@ public BaseAgent rootAgent() { return rootAgent; } - public ImmutableList plugins() { + public List plugins() { return plugins; } From bf5ca82d5ad8adb3bdeb576d79f77eff6f9111d9 Mon Sep 17 00:00:00 2001 From: Maciej Szwaja Date: Fri, 20 Mar 2026 05:14:14 -0700 Subject: [PATCH 04/10] chore: update VersionTest to allow rc versions PiperOrigin-RevId: 886723823 --- core/src/test/java/com/google/adk/VersionTest.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/core/src/test/java/com/google/adk/VersionTest.java b/core/src/test/java/com/google/adk/VersionTest.java index ff7939165..4b6f55c9b 100644 --- a/core/src/test/java/com/google/adk/VersionTest.java +++ b/core/src/test/java/com/google/adk/VersionTest.java @@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assertThat; +import java.util.regex.Pattern; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -25,6 +26,11 @@ @RunWith(JUnit4.class) public class VersionTest { + // from semver.org + private static final Pattern SEM_VER = + Pattern.compile( + "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"); + @Test public void versionShouldMatchProjectVersion() { assertThat(Version.JAVA_ADK_VERSION).isNotNull(); @@ -32,6 +38,11 @@ public void versionShouldMatchProjectVersion() { assertThat(Version.JAVA_ADK_VERSION).isNotEqualTo("unknown"); assertThat(Version.JAVA_ADK_VERSION).isNotEqualTo("${project.version}"); - assertThat(Version.JAVA_ADK_VERSION).matches("\\d+\\.\\d+\\.\\d+(-SNAPSHOT)?"); + assertThat(Version.JAVA_ADK_VERSION).matches("\\d+\\.\\d+\\.\\d+(-SNAPSHOT|-rc\\.\\d+)?"); + } + + @Test + public void versionShouldFollowSemanticVersioning() { + assertThat(Version.JAVA_ADK_VERSION).matches(SEM_VER); } } From 8af5e03811dfd548830df43103c81a592c8bf361 Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Fri, 20 Mar 2026 05:17:08 -0700 Subject: [PATCH 05/10] feat: Return List instead of ImmutableList in CallbackUtil methods PiperOrigin-RevId: 886725038 --- core/src/main/java/com/google/adk/agents/BaseAgent.java | 6 ++++-- core/src/main/java/com/google/adk/agents/CallbackUtil.java | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/core/src/main/java/com/google/adk/agents/BaseAgent.java b/core/src/main/java/com/google/adk/agents/BaseAgent.java index ed6631c50..95fe838cc 100644 --- a/core/src/main/java/com/google/adk/agents/BaseAgent.java +++ b/core/src/main/java/com/google/adk/agents/BaseAgent.java @@ -529,7 +529,8 @@ public B beforeAgentCallback(BeforeAgentCallback beforeAgentCallback) { @CanIgnoreReturnValue public B beforeAgentCallback(List beforeAgentCallback) { - this.beforeAgentCallback = CallbackUtil.getBeforeAgentCallbacks(beforeAgentCallback); + this.beforeAgentCallback = + ImmutableList.copyOf(CallbackUtil.getBeforeAgentCallbacks(beforeAgentCallback)); return self(); } @@ -541,7 +542,8 @@ public B afterAgentCallback(AfterAgentCallback afterAgentCallback) { @CanIgnoreReturnValue public B afterAgentCallback(List afterAgentCallback) { - this.afterAgentCallback = CallbackUtil.getAfterAgentCallbacks(afterAgentCallback); + this.afterAgentCallback = + ImmutableList.copyOf(CallbackUtil.getAfterAgentCallbacks(afterAgentCallback)); return self(); } diff --git a/core/src/main/java/com/google/adk/agents/CallbackUtil.java b/core/src/main/java/com/google/adk/agents/CallbackUtil.java index 11740ae9c..4eb8704b6 100644 --- a/core/src/main/java/com/google/adk/agents/CallbackUtil.java +++ b/core/src/main/java/com/google/adk/agents/CallbackUtil.java @@ -42,7 +42,7 @@ public final class CallbackUtil { * @return normalized async callbacks, or empty list if input is null. */ @CanIgnoreReturnValue - public static ImmutableList getBeforeAgentCallbacks( + public static List getBeforeAgentCallbacks( List beforeAgentCallbacks) { return getCallbacks( beforeAgentCallbacks, @@ -59,7 +59,7 @@ public static ImmutableList getBeforeAgentCallbacks( * @return normalized async callbacks, or empty list if input is null. */ @CanIgnoreReturnValue - public static ImmutableList getAfterAgentCallbacks( + public static List getAfterAgentCallbacks( List afterAgentCallback) { return getCallbacks( afterAgentCallback, From f145c744482b6b25f29a0b718bd452065e39d930 Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Fri, 20 Mar 2026 05:23:16 -0700 Subject: [PATCH 06/10] feat: update requestedAuthConfigs and its builder to be of general Map types PiperOrigin-RevId: 886727168 --- .../com/google/adk/events/EventActions.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) 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 83fd60e54..105300aa0 100644 --- a/core/src/main/java/com/google/adk/events/EventActions.java +++ b/core/src/main/java/com/google/adk/events/EventActions.java @@ -41,7 +41,7 @@ public class EventActions extends JsonBaseModel { private Set deletedArtifactIds; private @Nullable String transferToAgent; private @Nullable Boolean escalate; - private ConcurrentMap> requestedAuthConfigs; + private ConcurrentMap> requestedAuthConfigs; private ConcurrentMap requestedToolConfirmations; private boolean endOfAgent; private @Nullable EventCompaction compaction; @@ -139,13 +139,17 @@ public void setEscalate(@Nullable Boolean escalate) { } @JsonProperty("requestedAuthConfigs") - public ConcurrentMap> requestedAuthConfigs() { + public Map> requestedAuthConfigs() { return requestedAuthConfigs; } public void setRequestedAuthConfigs( - ConcurrentMap> requestedAuthConfigs) { - this.requestedAuthConfigs = requestedAuthConfigs; + Map> requestedAuthConfigs) { + if (requestedAuthConfigs == null) { + this.requestedAuthConfigs = new ConcurrentHashMap<>(); + } else { + this.requestedAuthConfigs = new ConcurrentHashMap<>(requestedAuthConfigs); + } } @JsonProperty("requestedToolConfirmations") @@ -248,7 +252,7 @@ public static class Builder { private Set deletedArtifactIds; private @Nullable String transferToAgent; private @Nullable Boolean escalate; - private ConcurrentMap> requestedAuthConfigs; + private ConcurrentMap> requestedAuthConfigs; private ConcurrentMap requestedToolConfirmations; private boolean endOfAgent = false; private @Nullable EventCompaction compaction; @@ -328,8 +332,12 @@ public Builder escalate(boolean escalate) { @CanIgnoreReturnValue @JsonProperty("requestedAuthConfigs") public Builder requestedAuthConfigs( - ConcurrentMap> value) { - this.requestedAuthConfigs = value; + @Nullable Map> value) { + if (value == null) { + this.requestedAuthConfigs = new ConcurrentHashMap<>(); + } else { + this.requestedAuthConfigs = new ConcurrentHashMap<>(value); + } return this; } From f59215d94fa6732e275def543c68c23247b4b718 Mon Sep 17 00:00:00 2001 From: adk-java-releases-bot Date: Fri, 20 Mar 2026 13:24:24 +0100 Subject: [PATCH 07/10] chore(main): release 1.0.0-rc.1 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 43 +++++++++++++++++++ README.md | 4 +- a2a/pom.xml | 2 +- contrib/firestore-session-service/pom.xml | 2 +- contrib/langchain4j/pom.xml | 2 +- contrib/samples/a2a_basic/pom.xml | 2 +- contrib/samples/a2a_server/pom.xml | 2 +- contrib/samples/configagent/pom.xml | 2 +- contrib/samples/helloworld/pom.xml | 2 +- contrib/samples/mcpfilesystem/pom.xml | 2 +- contrib/samples/pom.xml | 2 +- contrib/spring-ai/pom.xml | 2 +- core/pom.xml | 2 +- .../src/main/java/com/google/adk/Version.java | 2 +- dev/pom.xml | 2 +- maven_plugin/examples/custom_tools/pom.xml | 2 +- maven_plugin/examples/simple-agent/pom.xml | 2 +- maven_plugin/pom.xml | 2 +- pom.xml | 2 +- tutorials/city-time-weather/pom.xml | 2 +- tutorials/live-audio-single-agent/pom.xml | 2 +- 22 files changed, 65 insertions(+), 22 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 6db3039d0..802e9d13f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.9.0" + ".": "1.0.0-rc.1" } diff --git a/CHANGELOG.md b/CHANGELOG.md index ab111e90c..4ef000794 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,48 @@ # Changelog +## [1.0.0-rc.1](https://github.com/google/adk-java/compare/v0.9.0...v1.0.0-rc.1) (2026-03-20) + + +### ⚠ BREAKING CHANGES + +* remove McpToolset constructors taking Optional parameters +* remove deprecated Example processor + +### Features + +* add handling the a2a metadata in the RemoteA2AAgent; Add the enum type for the metadata keys ([e51f911](https://github.com/google/adk-java/commit/e51f9112050955657da0dfc3aedc00f90ad739ec)) +* add type-safe runAsync methods to BaseTool ([b8cb7e2](https://github.com/google/adk-java/commit/b8cb7e2db6d5ce20f4d7a1b237bdc155563cf4bd)) +* Enhance LangChain4j to support MCP tools with parametersJsonSchema ([2c71ba1](https://github.com/google/adk-java/commit/2c71ba1332e052189115cd4644b7a473c31ed414)) +* fixing context propagation for agent transfers ([9a08076](https://github.com/google/adk-java/commit/9a080763d83c319f539d1bacac4595d13b299e7e)) +* Implement basic version of BigQuery Agent Analytics Plugin ([c8ab0f9](https://github.com/google/adk-java/commit/c8ab0f96b09a6c9636728d634c62695fcd622246)) +* init AGENTS.md file ([7ebeb07](https://github.com/google/adk-java/commit/7ebeb07bf2ee72475484d8a31ccf7b4c601dda96)) +* Propagating the otel context ([8556d4a](https://github.com/google/adk-java/commit/8556d4af16ff04c6e3b678dcfc3d4bb232abc550)) +* remove McpToolset constructors taking Optional parameters ([dbb1394](https://github.com/google/adk-java/commit/dbb139439d38157b4b9af38c52824b1e8405a495)) +* Return List instead of ImmutableList in CallbackUtil methods ([8af5e03](https://github.com/google/adk-java/commit/8af5e03811dfd548830df43103c81a592c8bf361)) +* update requestedAuthConfigs and its builder to be of general Map types ([f145c74](https://github.com/google/adk-java/commit/f145c744482b6b25f29a0b718bd452065e39d930)) +* Update return type of App.plugins() from ImmutableList to List ([8ba4bfe](https://github.com/google/adk-java/commit/8ba4bfed3fa7045f3344329de7a39acddc64ee30)) +* Update return type of toolsets() from ImmutableList to List ([cd56902](https://github.com/google/adk-java/commit/cd56902b803d4f7a1f3c718529842823d9e4370a)) +* update Session.state() and its builder to be of general Map types ([4b9b99a](https://github.com/google/adk-java/commit/4b9b99ae7149a465ba2ae9b7496e01f669786553)) +* update stateDelta builder input to Map from ConcurrentMap ([0d1e5c7](https://github.com/google/adk-java/commit/0d1e5c7b0c42cea66b178cf8fedf08a8c20f7fd0)) + + +### Bug Fixes + +* fix null handling in runAsyncImpl ([567fdf0](https://github.com/google/adk-java/commit/567fdf048fee49afc86ca5d7d35f55424a6016ba)) +* improve processRequest_concurrentReadAndWrite_noException test case ([4eb3613](https://github.com/google/adk-java/commit/4eb3613b65cb1334e9432960d0f864ef09829c23)) +* include saveArtifact invocations in event chain ([551c31f](https://github.com/google/adk-java/commit/551c31f495aafde8568461cc0aa0973d7df7e5ac)) +* prevent ConcurrentModificationException when session events are modified by another thread during iteration ([fca43fb](https://github.com/google/adk-java/commit/fca43fbb9684ec8d080e437761f6bb4e38adf255)) +* Relaxing constraints for output schema ([d7e03ee](https://github.com/google/adk-java/commit/d7e03eeb067b83abd2afa3ea9bb5fc1c16143245)) +* Removing deprecated methods in Runner ([0af82e6](https://github.com/google/adk-java/commit/0af82e61a3c0dbbd95166a10b450cb507115ab60)) +* Use ConcurrentHashMap in InvocationReplayState ([94de7f1](https://github.com/google/adk-java/commit/94de7f199f86b39bdb7cce6e9800eb05008a8953)), closes [#1009](https://github.com/google/adk-java/issues/1009) +* workaround for the client config streaming settings are not respected ([#983](https://github.com/google/adk-java/issues/983)) ([3ba04d3](https://github.com/google/adk-java/commit/3ba04d33dc8f2ef8b151abe1be4d1c8b7afcc25a)) + + +### Miscellaneous Chores + +* remove deprecated Example processor ([28a8cd0](https://github.com/google/adk-java/commit/28a8cd04ca9348dbe51a15d2be3a2b5307394174)) +* set version to 1.0.0-rc.1 ([dc5d794](https://github.com/google/adk-java/commit/dc5d794c066571c7d87f006767bd32298e2a3ba8)) + ## [0.9.0](https://github.com/google/adk-java/compare/v0.8.0...v0.9.0) (2026-03-13) diff --git a/README.md b/README.md index de1cfbef7..169c78564 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 - 0.9.0 + 1.0.0-rc.1 com.google.adk google-adk-dev - 0.9.0 + 1.0.0-rc.1 ``` diff --git a/a2a/pom.xml b/a2a/pom.xml index a2f9d9456..bb9d19328 100644 --- a/a2a/pom.xml +++ b/a2a/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 0.9.1-SNAPSHOT + 1.0.0-rc.1 google-adk-a2a diff --git a/contrib/firestore-session-service/pom.xml b/contrib/firestore-session-service/pom.xml index 0079dce24..e01ebfeae 100644 --- a/contrib/firestore-session-service/pom.xml +++ b/contrib/firestore-session-service/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.9.1-SNAPSHOT + 1.0.0-rc.1 ../../pom.xml diff --git a/contrib/langchain4j/pom.xml b/contrib/langchain4j/pom.xml index 3dd2d1132..10d5d9eb9 100644 --- a/contrib/langchain4j/pom.xml +++ b/contrib/langchain4j/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.9.1-SNAPSHOT + 1.0.0-rc.1 ../../pom.xml diff --git a/contrib/samples/a2a_basic/pom.xml b/contrib/samples/a2a_basic/pom.xml index 0eccb733b..22d146c03 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 - 0.9.1-SNAPSHOT + 1.0.0-rc.1 .. diff --git a/contrib/samples/a2a_server/pom.xml b/contrib/samples/a2a_server/pom.xml index 0677ad718..8cebc7ef0 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 - 0.9.1-SNAPSHOT + 1.0.0-rc.1 .. diff --git a/contrib/samples/configagent/pom.xml b/contrib/samples/configagent/pom.xml index 059bd8a38..0486fc639 100644 --- a/contrib/samples/configagent/pom.xml +++ b/contrib/samples/configagent/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-samples - 0.9.1-SNAPSHOT + 1.0.0-rc.1 .. diff --git a/contrib/samples/helloworld/pom.xml b/contrib/samples/helloworld/pom.xml index df5d5e709..39191782b 100644 --- a/contrib/samples/helloworld/pom.xml +++ b/contrib/samples/helloworld/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-samples - 0.9.1-SNAPSHOT + 1.0.0-rc.1 .. diff --git a/contrib/samples/mcpfilesystem/pom.xml b/contrib/samples/mcpfilesystem/pom.xml index 16b139d35..aa1a76333 100644 --- a/contrib/samples/mcpfilesystem/pom.xml +++ b/contrib/samples/mcpfilesystem/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.9.1-SNAPSHOT + 1.0.0-rc.1 ../../.. diff --git a/contrib/samples/pom.xml b/contrib/samples/pom.xml index 4a415113f..cd96853cb 100644 --- a/contrib/samples/pom.xml +++ b/contrib/samples/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 0.9.1-SNAPSHOT + 1.0.0-rc.1 ../.. diff --git a/contrib/spring-ai/pom.xml b/contrib/spring-ai/pom.xml index 08d237ab5..7e99da1c2 100644 --- a/contrib/spring-ai/pom.xml +++ b/contrib/spring-ai/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.9.1-SNAPSHOT + 1.0.0-rc.1 ../../pom.xml diff --git a/core/pom.xml b/core/pom.xml index 02c75f88b..b2776f2be 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 0.9.1-SNAPSHOT + 1.0.0-rc.1 google-adk diff --git a/core/src/main/java/com/google/adk/Version.java b/core/src/main/java/com/google/adk/Version.java index a7aeb8b1f..d6e18c5ad 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 = "0.9.0"; // x-release-please-released-version + public static final String JAVA_ADK_VERSION = "1.0.0-rc.1"; // x-release-please-released-version private Version() {} } diff --git a/dev/pom.xml b/dev/pom.xml index 6cabcba7c..7f9408ae7 100644 --- a/dev/pom.xml +++ b/dev/pom.xml @@ -18,7 +18,7 @@ com.google.adk google-adk-parent - 0.9.1-SNAPSHOT + 1.0.0-rc.1 google-adk-dev diff --git a/maven_plugin/examples/custom_tools/pom.xml b/maven_plugin/examples/custom_tools/pom.xml index f2118f9cc..b76d04309 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 - 0.9.1-SNAPSHOT + 1.0.0-rc.1 jar ADK Custom Tools Example diff --git a/maven_plugin/examples/simple-agent/pom.xml b/maven_plugin/examples/simple-agent/pom.xml index 5c0f4462d..e65412ea8 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 - 0.9.1-SNAPSHOT + 1.0.0-rc.1 jar Simple ADK Agent Example diff --git a/maven_plugin/pom.xml b/maven_plugin/pom.xml index c48331f72..381e03a9e 100644 --- a/maven_plugin/pom.xml +++ b/maven_plugin/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 0.9.1-SNAPSHOT + 1.0.0-rc.1 ../pom.xml diff --git a/pom.xml b/pom.xml index 0be05a629..368bf67c8 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ com.google.adk google-adk-parent - 0.9.1-SNAPSHOT + 1.0.0-rc.1 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 76b7331f3..e2e179e48 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 - 0.9.1-SNAPSHOT + 1.0.0-rc.1 ../../pom.xml diff --git a/tutorials/live-audio-single-agent/pom.xml b/tutorials/live-audio-single-agent/pom.xml index a330cf4bd..8b5e97fb5 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 - 0.9.1-SNAPSHOT + 1.0.0-rc.1 ../../pom.xml From f3eb936772740b7dc7a803a40d0d39fdbccc4af4 Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Fri, 20 Mar 2026 06:59:40 -0700 Subject: [PATCH 08/10] fix: Using App conformant agent names PiperOrigin-RevId: 886764077 --- .../adk/agents/AgentWithMemoryTest.java | 4 +-- .../adk/telemetry/ContextPropagationTest.java | 21 +++++++------- .../com/google/adk/testing/TestUtils.java | 2 +- .../com/google/adk/tools/AgentToolTest.java | 28 +++++++++---------- 4 files changed, 27 insertions(+), 28 deletions(-) diff --git a/core/src/test/java/com/google/adk/agents/AgentWithMemoryTest.java b/core/src/test/java/com/google/adk/agents/AgentWithMemoryTest.java index d5edfc876..361c5eb6b 100644 --- a/core/src/test/java/com/google/adk/agents/AgentWithMemoryTest.java +++ b/core/src/test/java/com/google/adk/agents/AgentWithMemoryTest.java @@ -41,7 +41,7 @@ public final class AgentWithMemoryTest { @Test public void agentRemembersUserNameWithMemoryTool() throws Exception { String userId = "test-user"; - String agentName = "test-agent"; + String agentName = "test_agent"; Part functionCall = Part.builder() @@ -101,7 +101,7 @@ public void agentRemembersUserNameWithMemoryTool() throws Exception { Session updatedSession = runner .sessionService() - .getSession("test-agent", userId, sessionId, Optional.empty()) + .getSession("test_agent", userId, sessionId, Optional.empty()) .blockingGet(); // Save the updated session to memory so we can bring it up on the next request. diff --git a/core/src/test/java/com/google/adk/telemetry/ContextPropagationTest.java b/core/src/test/java/com/google/adk/telemetry/ContextPropagationTest.java index 1ee018848..b13904934 100644 --- a/core/src/test/java/com/google/adk/telemetry/ContextPropagationTest.java +++ b/core/src/test/java/com/google/adk/telemetry/ContextPropagationTest.java @@ -32,7 +32,6 @@ import com.google.adk.runner.Runner; import com.google.adk.sessions.InMemorySessionService; import com.google.adk.sessions.Session; -import com.google.adk.sessions.SessionKey; import com.google.adk.testing.TestLlm; import com.google.adk.testing.TestUtils; import com.google.adk.tools.BaseTool; @@ -653,13 +652,13 @@ public void runnerRunAsync_propagatesContext() throws InterruptedException { @Test public void runnerRunLive_propagatesContext() throws InterruptedException { BaseAgent agent = new TestAgent(); - Runner runner = Runner.builder().agent(agent).appName("test-app").build(); + Runner runner = + Runner.builder().agent(agent).appName("test_app").sessionService(sessionService).build(); Span parentSpan = tracer.spanBuilder("parent").startSpan(); try (Scope s = parentSpan.makeCurrent()) { Session session = - runner - .sessionService() - .createSession("test-app", "test-user", (Map) null, "test-session") + sessionService + .createSession("test_app", "test-user", (Map) null, "test-session") .blockingGet(); Content newMessage = Content.fromParts(Part.fromText("hi")); RunConfig runConfig = RunConfig.builder().build(); @@ -800,12 +799,10 @@ public void testNestedAgentTraceHierarchy() throws InterruptedException { } private void runAgent(BaseAgent agent) throws InterruptedException { - Runner runner = Runner.builder().agent(agent).appName("test-app").build(); + Runner runner = + Runner.builder().agent(agent).appName("test_app").sessionService(sessionService).build(); Session session = - runner - .sessionService() - .createSession(new SessionKey("test-app", "test-user", "test-session")) - .blockingGet(); + sessionService.createSession("test_app", "test-user", null, "test-session").blockingGet(); Content newMessage = Content.fromParts(Part.fromText("hi")); RunConfig runConfig = RunConfig.builder().build(); runner @@ -871,7 +868,9 @@ private SpanData findSpanByName(String name) { private InvocationContext buildInvocationContext() { Session session = - sessionService.createSession("test-app", "test-user", null, "test-session").blockingGet(); + sessionService + .createSession("test_app", "test-user", (Map) null, "test-session") + .blockingGet(); return InvocationContext.builder() .sessionService(sessionService) .session(session) diff --git a/core/src/test/java/com/google/adk/testing/TestUtils.java b/core/src/test/java/com/google/adk/testing/TestUtils.java index 70ae14bf1..daed8d2e4 100644 --- a/core/src/test/java/com/google/adk/testing/TestUtils.java +++ b/core/src/test/java/com/google/adk/testing/TestUtils.java @@ -61,7 +61,7 @@ public static InvocationContext createInvocationContext(BaseAgent agent, RunConf .artifactService(new InMemoryArtifactService()) .invocationId("invocationId") .agent(agent) - .session(sessionService.createSession("test-app", "test-user").blockingGet()) + .session(sessionService.createSession("test_app", "test-user").blockingGet()) .userContent(Content.fromParts(Part.fromText("user content"))) .runConfig(runConfig) .build(); diff --git a/core/src/test/java/com/google/adk/tools/AgentToolTest.java b/core/src/test/java/com/google/adk/tools/AgentToolTest.java index 3a5390027..0f168c5df 100644 --- a/core/src/test/java/com/google/adk/tools/AgentToolTest.java +++ b/core/src/test/java/com/google/adk/tools/AgentToolTest.java @@ -143,7 +143,7 @@ public void declaration_withInputSchema_returnsDeclarationWithSchema() { AgentTool agentTool = AgentTool.create( createTestAgentBuilder(createTestLlm(LlmResponse.builder().build())) - .name("agent name") + .name("agent_name") .description("agent description") .inputSchema(inputSchema) .build()); @@ -153,7 +153,7 @@ public void declaration_withInputSchema_returnsDeclarationWithSchema() { assertThat(declaration) .isEqualTo( FunctionDeclaration.builder() - .name("agent name") + .name("agent_name") .description("agent description") .parameters(inputSchema) .build()); @@ -164,7 +164,7 @@ public void declaration_withoutInputSchema_returnsDeclarationWithRequestParamete AgentTool agentTool = AgentTool.create( createTestAgentBuilder(createTestLlm(LlmResponse.builder().build())) - .name("agent name") + .name("agent_name") .description("agent description") .build()); @@ -173,7 +173,7 @@ public void declaration_withoutInputSchema_returnsDeclarationWithRequestParamete assertThat(declaration) .isEqualTo( FunctionDeclaration.builder() - .name("agent name") + .name("agent_name") .description("agent description") .parameters( Schema.builder() @@ -200,7 +200,7 @@ public void call_withInputSchema_invalidInput_throwsException() throws Exception .build(); LlmAgent testAgent = createTestAgentBuilder(createTestLlm(LlmResponse.builder().build())) - .name("agent name") + .name("agent_name") .description("agent description") .inputSchema(inputSchema) .build(); @@ -256,7 +256,7 @@ public void call_withOutputSchema_invalidOutput_throwsException() throws Excepti "{\"is_valid\": \"invalid type\", " + "\"message\": \"success\"}"))) .build())) - .name("agent name") + .name("agent_name") .description("agent description") .outputSchema(outputSchema) .build(); @@ -301,7 +301,7 @@ public void call_withInputAndOutputSchema_successful() throws Exception { Part.fromText( "{\"is_valid\": true, " + "\"message\": \"success\"}"))) .build())) - .name("agent name") + .name("agent_name") .description("agent description") .inputSchema(inputSchema) .outputSchema(outputSchema) @@ -332,7 +332,7 @@ public void call_withoutSchema_returnsConcatenatedTextFromLastEvent() throws Exc Part.fromText("First text part. "), Part.fromText("Second text part."))) .build()))) - .name("agent name") + .name("agent_name") .description("agent description") .build(); AgentTool agentTool = AgentTool.create(testAgent); @@ -358,7 +358,7 @@ public void call_withThoughts_returnsOnlyNonThoughtText() throws Exception { .build()) .build()); LlmAgent testAgent = - createTestAgentBuilder(testLlm).name("agent name").description("agent description").build(); + createTestAgentBuilder(testLlm).name("agent_name").description("agent description").build(); AgentTool agentTool = AgentTool.create(testAgent); ToolContext toolContext = createToolContext(testAgent); @@ -373,7 +373,7 @@ public void call_emptyModelResponse_returnsEmptyMap() throws Exception { LlmAgent testAgent = createTestAgentBuilder( createTestLlm(LlmResponse.builder().content(Content.builder().build()).build())) - .name("agent name") + .name("agent_name") .description("agent description") .build(); AgentTool agentTool = AgentTool.create(testAgent); @@ -394,7 +394,7 @@ public void call_withInputSchema_argsAreSentToAgent() throws Exception { .build()); LlmAgent testAgent = createTestAgentBuilder(testLlm) - .name("agent name") + .name("agent_name") .description("agent description") .inputSchema( Schema.builder() @@ -422,7 +422,7 @@ public void call_withoutInputSchema_requestIsSentToAgent() throws Exception { .content(Content.fromParts(Part.fromText("test response"))) .build()); LlmAgent testAgent = - createTestAgentBuilder(testLlm).name("agent name").description("agent description").build(); + createTestAgentBuilder(testLlm).name("agent_name").description("agent description").build(); AgentTool agentTool = AgentTool.create(testAgent); ToolContext toolContext = createToolContext(testAgent); @@ -447,7 +447,7 @@ public void call_withStateDeltaInResponse_propagatesStateDelta() throws Exceptio .build()); LlmAgent testAgent = createTestAgentBuilder(testLlm) - .name("agent name") + .name("agent_name") .description("agent description") .afterAgentCallback(afterAgentCallback) .build(); @@ -477,7 +477,7 @@ public void call_withSkipSummarizationAndStateDelta_propagatesStateAndSetsSkipSu .build()); LlmAgent testAgent = createTestAgentBuilder(testLlm) - .name("agent name") + .name("agent_name") .description("agent description") .afterAgentCallback(afterAgentCallback) .build(); From 8ea81f7575580a7316752adc2908efe442511b16 Mon Sep 17 00:00:00 2001 From: adk-java-releases-bot Date: Fri, 20 Mar 2026 07:00:31 -0700 Subject: [PATCH 09/10] chore(main): release 1.0.1-rc.1-SNAPSHOT Merge https://github.com/google/adk-java/pull/1067 :robot: I have created a release *beep* *boop* --- ### Updating meta-information for bleeding-edge SNAPSHOT release. --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-java/pull/1067 from google:release-please--branches--main 0b85f5baba4423b6e73b6dfe2982d8fe22411a98 PiperOrigin-RevId: 886764460 --- a2a/pom.xml | 2 +- contrib/firestore-session-service/pom.xml | 2 +- contrib/langchain4j/pom.xml | 2 +- contrib/samples/a2a_basic/pom.xml | 2 +- contrib/samples/a2a_server/pom.xml | 2 +- contrib/samples/configagent/pom.xml | 2 +- contrib/samples/helloworld/pom.xml | 2 +- contrib/samples/mcpfilesystem/pom.xml | 2 +- contrib/samples/pom.xml | 2 +- contrib/spring-ai/pom.xml | 2 +- core/pom.xml | 2 +- dev/pom.xml | 2 +- maven_plugin/examples/custom_tools/pom.xml | 2 +- maven_plugin/examples/simple-agent/pom.xml | 2 +- maven_plugin/pom.xml | 2 +- pom.xml | 2 +- tutorials/city-time-weather/pom.xml | 2 +- tutorials/live-audio-single-agent/pom.xml | 2 +- 18 files changed, 18 insertions(+), 18 deletions(-) diff --git a/a2a/pom.xml b/a2a/pom.xml index bb9d19328..97c186606 100644 --- a/a2a/pom.xml +++ b/a2a/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 1.0.0-rc.1 + 1.0.1-rc.1-SNAPSHOT google-adk-a2a diff --git a/contrib/firestore-session-service/pom.xml b/contrib/firestore-session-service/pom.xml index e01ebfeae..ed1ecd09b 100644 --- a/contrib/firestore-session-service/pom.xml +++ b/contrib/firestore-session-service/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 1.0.0-rc.1 + 1.0.1-rc.1-SNAPSHOT ../../pom.xml diff --git a/contrib/langchain4j/pom.xml b/contrib/langchain4j/pom.xml index 10d5d9eb9..e88174849 100644 --- a/contrib/langchain4j/pom.xml +++ b/contrib/langchain4j/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 1.0.0-rc.1 + 1.0.1-rc.1-SNAPSHOT ../../pom.xml diff --git a/contrib/samples/a2a_basic/pom.xml b/contrib/samples/a2a_basic/pom.xml index 22d146c03..80881842b 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.0-rc.1 + 1.0.1-rc.1-SNAPSHOT .. diff --git a/contrib/samples/a2a_server/pom.xml b/contrib/samples/a2a_server/pom.xml index 8cebc7ef0..61c44bc97 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.0-rc.1 + 1.0.1-rc.1-SNAPSHOT .. diff --git a/contrib/samples/configagent/pom.xml b/contrib/samples/configagent/pom.xml index 0486fc639..8f57b7f9e 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.0-rc.1 + 1.0.1-rc.1-SNAPSHOT .. diff --git a/contrib/samples/helloworld/pom.xml b/contrib/samples/helloworld/pom.xml index 39191782b..676a2bc96 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.0-rc.1 + 1.0.1-rc.1-SNAPSHOT .. diff --git a/contrib/samples/mcpfilesystem/pom.xml b/contrib/samples/mcpfilesystem/pom.xml index aa1a76333..7275313ab 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.0-rc.1 + 1.0.1-rc.1-SNAPSHOT ../../.. diff --git a/contrib/samples/pom.xml b/contrib/samples/pom.xml index cd96853cb..ff48d6bd3 100644 --- a/contrib/samples/pom.xml +++ b/contrib/samples/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 1.0.0-rc.1 + 1.0.1-rc.1-SNAPSHOT ../.. diff --git a/contrib/spring-ai/pom.xml b/contrib/spring-ai/pom.xml index 7e99da1c2..5f7300896 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.0-rc.1 + 1.0.1-rc.1-SNAPSHOT ../../pom.xml diff --git a/core/pom.xml b/core/pom.xml index b2776f2be..8559f396d 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 1.0.0-rc.1 + 1.0.1-rc.1-SNAPSHOT google-adk diff --git a/dev/pom.xml b/dev/pom.xml index 7f9408ae7..5468a1187 100644 --- a/dev/pom.xml +++ b/dev/pom.xml @@ -18,7 +18,7 @@ com.google.adk google-adk-parent - 1.0.0-rc.1 + 1.0.1-rc.1-SNAPSHOT google-adk-dev diff --git a/maven_plugin/examples/custom_tools/pom.xml b/maven_plugin/examples/custom_tools/pom.xml index b76d04309..aa273d732 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.0-rc.1 + 1.0.1-rc.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 e65412ea8..34aeb8c1c 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.0-rc.1 + 1.0.1-rc.1-SNAPSHOT jar Simple ADK Agent Example diff --git a/maven_plugin/pom.xml b/maven_plugin/pom.xml index 381e03a9e..d0feb41e3 100644 --- a/maven_plugin/pom.xml +++ b/maven_plugin/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 1.0.0-rc.1 + 1.0.1-rc.1-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index 368bf67c8..cbeca1b72 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ com.google.adk google-adk-parent - 1.0.0-rc.1 + 1.0.1-rc.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 e2e179e48..aeb110cf6 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.0-rc.1 + 1.0.1-rc.1-SNAPSHOT ../../pom.xml diff --git a/tutorials/live-audio-single-agent/pom.xml b/tutorials/live-audio-single-agent/pom.xml index 8b5e97fb5..99243893b 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.0-rc.1 + 1.0.1-rc.1-SNAPSHOT ../../pom.xml From 40ca6a7c5163f711e02a54163d6066f7cd86e64d Mon Sep 17 00:00:00 2001 From: Google Team Member Date: Fri, 20 Mar 2026 07:11:54 -0700 Subject: [PATCH 10/10] feat: enabling output_schema and tools to coexist This CL enables the simultaneous use of `output_schema` (structured output) and `tools` for models that do not natively support both features at once (specifically Gemini 1.x and 2.x on Vertex AI). ### Core Logic The CL implements a workaround for models with this limitation: 1. **Synthetic Tooling**: Instead of passing the `output_schema` directly to the model's configuration, it introduces a synthetic tool called `set_model_response`. 2. **Schema Injection**: The parameters of this tool are set to the requested `output_schema`. 3. **Instruction Prompting**: System instructions are appended, directing the model to provide its final response using this specific tool in the required format. 4. **Response Interception**: The `BaseLlmFlow` is updated to check if `set_model_response` was called. If so, it extracts the JSON arguments and converts them into a standard model response event. ### Key Changes * **`OutputSchema.java` (New)**: A new `RequestProcessor` that detects when the workaround is needed, adds the `SetModelResponseTool`, and provides utilities for extracting the structured response. * **`SetModelResponseTool.java` (New)**: A marker tool that simply returns its input arguments, used to "capture" the structured output from the model. * **`ModelNameUtils.java`**: Added logic to identify Gemini 1.x and 2.x models and determine if they can handle native `output_schema` alongside tools. * **`BaseLlmFlow.java`**: Updated the flow logic to detect the synthetic tool response and generate the final output event. * **`Basic.java`**: Updated to prevent native `outputSchema` configuration when the workaround is active. * **`SingleFlow.java`**: Registered the new `OutputSchema` processor. PiperOrigin-RevId: 886769688 --- .../adk/flows/llmflows/BaseLlmFlow.java | 11 +- .../com/google/adk/flows/llmflows/Basic.java | 11 +- .../adk/flows/llmflows/OutputSchema.java | 119 +++++++++++ .../google/adk/flows/llmflows/SingleFlow.java | 1 + .../adk/tools/SetModelResponseTool.java | 63 ++++++ .../com/google/adk/utils/ModelNameUtils.java | 18 +- .../adk/flows/llmflows/OutputSchemaTest.java | 192 ++++++++++++++++++ 7 files changed, 410 insertions(+), 5 deletions(-) create mode 100644 core/src/main/java/com/google/adk/flows/llmflows/OutputSchema.java create mode 100644 core/src/main/java/com/google/adk/tools/SetModelResponseTool.java create mode 100644 core/src/test/java/com/google/adk/flows/llmflows/OutputSchemaTest.java 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 8fabc978d..d4fe1b838 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 @@ -692,9 +692,14 @@ private Flowable buildPostprocessingEvents( Optional toolConfirmationEvent = Functions.generateRequestConfirmationEvent( context, modelResponseEvent, functionResponseEvent); - return toolConfirmationEvent.isPresent() - ? Flowable.just(toolConfirmationEvent.get(), functionResponseEvent) - : Flowable.just(functionResponseEvent); + List events = new ArrayList<>(); + toolConfirmationEvent.ifPresent(events::add); + events.add(functionResponseEvent); + OutputSchema.getStructuredModelResponse(functionResponseEvent) + .ifPresent( + json -> + events.add(OutputSchema.createFinalModelResponseEvent(context, json))); + return Flowable.fromIterable(events); }); } diff --git a/core/src/main/java/com/google/adk/flows/llmflows/Basic.java b/core/src/main/java/com/google/adk/flows/llmflows/Basic.java index 0876a26e8..5aa970be6 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/Basic.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/Basic.java @@ -19,6 +19,7 @@ import com.google.adk.agents.InvocationContext; import com.google.adk.agents.LlmAgent; import com.google.adk.models.LlmRequest; +import com.google.adk.utils.ModelNameUtils; import com.google.common.collect.ImmutableList; import com.google.genai.types.GenerateContentConfig; import com.google.genai.types.LiveConnectConfig; @@ -60,7 +61,15 @@ public Single processRequest( .orElseGet(() -> GenerateContentConfig.builder().build())) .liveConnectConfig(liveConnectConfigBuilder.build()); - agent.outputSchema().ifPresent(builder::outputSchema); + agent + .outputSchema() + .ifPresent( + outputSchema -> { + if (agent.toolsUnion().isEmpty() + || ModelNameUtils.canUseOutputSchemaWithTools(modelName)) { + builder.outputSchema(outputSchema); + } + }); return Single.just( RequestProcessor.RequestProcessingResult.create(builder.build(), ImmutableList.of())); } diff --git a/core/src/main/java/com/google/adk/flows/llmflows/OutputSchema.java b/core/src/main/java/com/google/adk/flows/llmflows/OutputSchema.java new file mode 100644 index 000000000..d1f322f18 --- /dev/null +++ b/core/src/main/java/com/google/adk/flows/llmflows/OutputSchema.java @@ -0,0 +1,119 @@ +/* + * 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 + * + * 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 com.fasterxml.jackson.core.JsonProcessingException; +import com.google.adk.JsonBaseModel; +import com.google.adk.agents.InvocationContext; +import com.google.adk.agents.LlmAgent; +import com.google.adk.events.Event; +import com.google.adk.models.LlmRequest; +import com.google.adk.tools.SetModelResponseTool; +import com.google.adk.tools.ToolContext; +import com.google.adk.utils.ModelNameUtils; +import com.google.common.collect.ImmutableList; +import com.google.genai.types.Content; +import com.google.genai.types.FunctionResponse; +import com.google.genai.types.Part; +import io.reactivex.rxjava3.core.Single; +import java.util.Objects; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Processor that handles output schema for agents with tools. */ +public final class OutputSchema implements RequestProcessor { + + private static final Logger logger = LoggerFactory.getLogger(OutputSchema.class); + + public OutputSchema() {} + + @Override + public Single processRequest( + InvocationContext context, LlmRequest request) { + if (!(context.agent() instanceof LlmAgent)) { + return Single.just(RequestProcessingResult.create(request, ImmutableList.of())); + } + LlmAgent agent = (LlmAgent) context.agent(); + String modelName = request.model().orElse(""); + + if (agent.outputSchema().isEmpty() + || agent.toolsUnion().isEmpty() + || ModelNameUtils.canUseOutputSchemaWithTools(modelName)) { + return Single.just(RequestProcessingResult.create(request, ImmutableList.of())); + } + + // Add the set_model_response tool to handle structured output + SetModelResponseTool setResponseTool = new SetModelResponseTool(agent.outputSchema().get()); + LlmRequest.Builder builder = request.toBuilder(); + + return setResponseTool + .processLlmRequest(builder, ToolContext.builder(context).build()) + .andThen( + Single.fromCallable( + () -> { + builder.appendInstructions( + ImmutableList.of( + "IMPORTANT: You have access to other tools, but you must provide your" + + " final response using the set_model_response tool with the" + + " required structured format. After using any other tools needed" + + " to complete the task, always call set_model_response with your" + + " final answer in the specified schema format.")); + return RequestProcessingResult.create(builder.build(), ImmutableList.of()); + })); + } + + /** + * Check if function response contains set_model_response and extract JSON. + * + * @param functionResponseEvent The function response event to check. + * @return JSON response string if set_model_response was called, Optional.empty() otherwise. + */ + public static Optional getStructuredModelResponse(Event functionResponseEvent) { + for (FunctionResponse funcResponse : functionResponseEvent.functionResponses()) { + if (Objects.equals(funcResponse.name().orElse(""), SetModelResponseTool.NAME)) { + Object response = funcResponse.response(); + // The tool returns the args map directly. + try { + return Optional.of(JsonBaseModel.getMapper().writeValueAsString(response)); + } catch (JsonProcessingException e) { + logger.error("Failed to serialize set_model_response result", e); + return Optional.empty(); + } + } + } + return Optional.empty(); + } + + /** + * Create a final model response event from set_model_response JSON. + * + * @param context The invocation context. + * @param jsonResponse The JSON response from set_model_response tool. + * @return A new Event that looks like a normal model response. + */ + public static Event createFinalModelResponseEvent( + InvocationContext context, String jsonResponse) { + return Event.builder() + .id(Event.generateEventId()) + .invocationId(context.invocationId()) + .author(context.agent().name()) + .branch(context.branch().orElse(null)) + .content(Content.builder().role("model").parts(Part.fromText(jsonResponse)).build()) + .build(); + } +} diff --git a/core/src/main/java/com/google/adk/flows/llmflows/SingleFlow.java b/core/src/main/java/com/google/adk/flows/llmflows/SingleFlow.java index f56cc61c3..41dff3b96 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/SingleFlow.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/SingleFlow.java @@ -27,6 +27,7 @@ public class SingleFlow extends BaseLlmFlow { protected static final ImmutableList REQUEST_PROCESSORS = ImmutableList.of( new Basic(), + new OutputSchema(), new RequestConfirmationLlmRequestProcessor(), new Instructions(), new Identity(), diff --git a/core/src/main/java/com/google/adk/tools/SetModelResponseTool.java b/core/src/main/java/com/google/adk/tools/SetModelResponseTool.java new file mode 100644 index 000000000..e23d6414a --- /dev/null +++ b/core/src/main/java/com/google/adk/tools/SetModelResponseTool.java @@ -0,0 +1,63 @@ +/* + * 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 + * + * 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.tools; + +import com.google.genai.types.FunctionDeclaration; +import com.google.genai.types.Schema; +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. + * + *

This tool allows the model to set its final response when output_schema is configured + * alongside other tools. The model should use this tool to provide its final structured response + * instead of outputting text directly. + */ +public class SetModelResponseTool extends BaseTool { + public static final String NAME = "set_model_response"; + + private final Schema outputSchema; + + public SetModelResponseTool(@Nonnull Schema outputSchema) { + super( + NAME, + "Set your final response using the required output schema. " + + "After using any other tools needed to complete the task, always call" + + " set_model_response with your final answer in the specified schema format."); + this.outputSchema = outputSchema; + } + + @Override + public Optional declaration() { + return Optional.of( + FunctionDeclaration.builder() + .name(name()) + .description(description()) + .parameters(outputSchema) + .build()); + } + + @Override + public Single> runAsync(Map args, ToolContext toolContext) { + // This tool is a marker for the final response, it doesn't do anything but return its arguments + // which will be captured as the final result. + return Single.just(args); + } +} diff --git a/core/src/main/java/com/google/adk/utils/ModelNameUtils.java b/core/src/main/java/com/google/adk/utils/ModelNameUtils.java index c46f6e3a8..cf0f2221e 100644 --- a/core/src/main/java/com/google/adk/utils/ModelNameUtils.java +++ b/core/src/main/java/com/google/adk/utils/ModelNameUtils.java @@ -21,6 +21,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +/** Utility class for model names. */ public final class ModelNameUtils { private static final String GEMINI_PREFIX = "gemini-"; private static final Pattern GEMINI_2_PATTERN = Pattern.compile("^gemini-2\\..*"); @@ -35,11 +36,15 @@ public static boolean isGeminiModel(String modelString) { } public static boolean isGemini2Model(String modelString) { + return matchesModelPattern(modelString, GEMINI_2_PATTERN); + } + + private static boolean matchesModelPattern(String modelString, Pattern pattern) { if (modelString == null) { return false; } String modelName = extractModelName(modelString); - return GEMINI_2_PATTERN.matcher(modelName).matches(); + return pattern.matcher(modelName).matches(); } /** @@ -65,6 +70,17 @@ public static boolean isInstanceOfGemini(Object o) { return false; } + /** + * Returns true if the model supports using output schema together with tools. + * + * @param modelString The model name or path. + * @return true if output schema with tools is supported, false otherwise. + */ + public static boolean canUseOutputSchemaWithTools(String modelString) { + // Current limitation for Vertex AI 2.x models. + return !isGemini2Model(modelString); + } + /** * Extract the actual model name from either simple or path-based format. * diff --git a/core/src/test/java/com/google/adk/flows/llmflows/OutputSchemaTest.java b/core/src/test/java/com/google/adk/flows/llmflows/OutputSchemaTest.java new file mode 100644 index 000000000..ffd56de6c --- /dev/null +++ b/core/src/test/java/com/google/adk/flows/llmflows/OutputSchemaTest.java @@ -0,0 +1,192 @@ +/* + * 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 + * + * 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.createTestLlm; +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.flows.llmflows.RequestProcessor.RequestProcessingResult; +import com.google.adk.models.LlmRequest; +import com.google.adk.models.LlmResponse; +import com.google.adk.testing.TestLlm; +import com.google.adk.tools.BaseTool; +import com.google.adk.tools.SetModelResponseTool; +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.FunctionResponse; +import com.google.genai.types.Part; +import com.google.genai.types.Schema; +import io.reactivex.rxjava3.core.Single; +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 OutputSchemaTest { + + private static final Schema TEST_OUTPUT_SCHEMA = + Schema.builder() + .type("OBJECT") + .properties(ImmutableMap.of("field1", Schema.builder().type("STRING").build())) + .required(ImmutableList.of("field1")) + .build(); + + private OutputSchema outputSchemaProcessor; + private TestLlm testLlm; + private LlmRequest initialRequest; + + @Before + public void setUp() { + outputSchemaProcessor = new OutputSchema(); + testLlm = createTestLlm(LlmResponse.builder().build()); + initialRequest = LlmRequest.builder().model("gemini-2.0-pro").build(); + } + + public static class TestTool extends BaseTool { + public TestTool() { + super("test_tool", "test description"); + } + + @Override + public Single> runAsync(Map args, ToolContext toolContext) { + return Single.just(ImmutableMap.of()); + } + } + + @Test + public void processRequest_noOutputSchema_doesNothing() { + LlmAgent agent = + LlmAgent.builder() + .name("agent") + .model(testLlm) + .tools(ImmutableList.of(new TestTool())) + .build(); + InvocationContext context = createInvocationContext(agent); + + RequestProcessingResult result = + outputSchemaProcessor.processRequest(context, initialRequest).blockingGet(); + + assertThat(result.updatedRequest()).isEqualTo(initialRequest); + assertThat(result.events()).isEmpty(); + } + + @Test + public void processRequest_noTools_doesNothing() { + LlmAgent agent = + LlmAgent.builder().name("agent").model(testLlm).outputSchema(TEST_OUTPUT_SCHEMA).build(); + InvocationContext context = createInvocationContext(agent); + + RequestProcessingResult result = + outputSchemaProcessor.processRequest(context, initialRequest).blockingGet(); + + assertThat(result.updatedRequest()).isEqualTo(initialRequest); + assertThat(result.events()).isEmpty(); + } + + @Test + public void processRequest_withOutputSchemaAndTools_addsSetModelResponseTool() { + LlmAgent agent = + LlmAgent.builder() + .name("agent") + .model(testLlm) + .outputSchema(TEST_OUTPUT_SCHEMA) + .tools(ImmutableList.of(new TestTool())) + .build(); + InvocationContext context = createInvocationContext(agent); + LlmRequest requestWithTools = + LlmRequest.builder() + .model("gemini-2.5-pro") + .tools(ImmutableMap.of("test_tool", new TestTool())) + .build(); + + RequestProcessingResult result = + outputSchemaProcessor.processRequest(context, requestWithTools).blockingGet(); + + LlmRequest updatedRequest = result.updatedRequest(); + assertThat(updatedRequest.tools()).hasSize(2); + assertThat( + updatedRequest.tools().values().stream() + .anyMatch(t -> t instanceof SetModelResponseTool)) + .isTrue(); + assertThat(updatedRequest.tools().values().stream().anyMatch(t -> t.name().equals("test_tool"))) + .isTrue(); + assertThat(updatedRequest.getSystemInstructions()).isNotEmpty(); + assertThat(updatedRequest.getSystemInstructions().get(0)) + .contains("you must provide your final response using the set_model_response tool"); + assertThat(result.events()).isEmpty(); + } + + @Test + public void getStructuredModelResponse_withSetModelResponse_returnsJson() { + FunctionResponse fr = + FunctionResponse.builder() + .name(SetModelResponseTool.NAME) + .response(ImmutableMap.of("field1", "value1")) + .build(); + Event event = + Event.builder() + .content( + Content.builder() + .parts(Part.builder().functionResponse(fr).build()) + .role("model") + .build()) + .build(); + + assertThat(OutputSchema.getStructuredModelResponse(event)).hasValue("{\"field1\":\"value1\"}"); + } + + @Test + public void getStructuredModelResponse_withoutSetModelResponse_returnsEmpty() { + FunctionResponse fr = + FunctionResponse.builder() + .name("other_tool") + .response(ImmutableMap.of("field1", "value1")) + .build(); + Event event = + Event.builder() + .content( + Content.builder() + .parts(Part.builder().functionResponse(fr).build()) + .role("model") + .build()) + .build(); + + assertThat(OutputSchema.getStructuredModelResponse(event)).isEmpty(); + } + + @Test + public void createFinalModelResponseEvent_createsModelResponseEvent() { + LlmAgent agent = LlmAgent.builder().name("agent").model(testLlm).build(); + InvocationContext context = createInvocationContext(agent); + String jsonResponse = "{\"field1\":\"value1\"}"; + + Event event = OutputSchema.createFinalModelResponseEvent(context, jsonResponse); + + assertThat(event.invocationId()).isEqualTo(context.invocationId()); + assertThat(event.author()).isEqualTo("agent"); + assertThat(event.content().get().role()).hasValue("model"); + assertThat(event.content().get().parts().get()).containsExactly(Part.fromText(jsonResponse)); + } +}