diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 000000000..cbde1a76f
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,30 @@
+{
+ "name": "Java 17",
+ "image": "mcr.microsoft.com/devcontainers/java:1-17-bullseye",
+ "features": {
+ "ghcr.io/devcontainers/features/github-cli:1": {},
+ "ghcr.io/devcontainers/features/git-lfs:1": {}
+ },
+ "customizations": {
+ "vscode": {
+ "extensions": [
+ "extension-pack-for-java",
+ "redhat.vscode-xml"
+ ],
+ "settings": {
+ "java.jdt.download.server": "latest",
+ "java.help.firstView": "none",
+ "java.showBuildStatusOnStart": "notification",
+ "java.configuration.updateBuildConfiguration": "interactive",
+ "java.autobuild.enabled": true,
+ "terminal.integrated.focusOnOutput": false
+ }
+ }
+ },
+ "remoteUser": "vscode",
+ "forwardPorts": [8000, 8080, 8081, 8082],
+ "postCreateCommand": "git config --global credential.helper '!gh auth git-credential' && git config --global lfs.locksverify false",
+ "hostRequirements": {
+ "cpus": 4
+ }
+}
diff --git a/.github/workflows/release-please.yaml b/.github/workflows/release-please.yaml
index 6d3142907..258cf90db 100644
--- a/.github/workflows/release-please.yaml
+++ b/.github/workflows/release-please.yaml
@@ -14,4 +14,4 @@ jobs:
steps:
- uses: googleapis/release-please-action@v4
with:
- token: ${{ secrets.GITHUB_TOKEN }}
+ token: ${{ secrets.RELEASE_PLEASE_TOKEN }}
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 09a252282..d6a5f76bd 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "1.2.0"
+ ".": "0.8.0"
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d2d4262d5..6d5b9e5eb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,273 +1,174 @@
# Changelog
-## [1.2.0](https://github.com/redbus-labs/adk-java/compare/v1.1.0...v1.2.0) (2026-03-03)
+## [0.8.0](https://github.com/google/adk-java/compare/v0.7.0...v0.8.0) (2026-03-06)
-### Features
-
-* Add a2a/webservice module and a2a_remote sample to build ([d08a2bc](https://github.com/redbus-labs/adk-java/commit/d08a2bc359442df8dc0ec0b616d54c91095c724a))
-* Add additional A2A files (A2ASendMessageExecutor, webservice, a2a_remote sample) ([22b1a89](https://github.com/redbus-labs/adk-java/commit/22b1a89bd48214eb84f0a16e9f2e795785a5d028))
-
-
-### Bug Fixes
-
-* Resolve cyclic dependency and adapt A2A tests for current codebase ([c23d95c](https://github.com/redbus-labs/adk-java/commit/c23d95c121f7074cae0f3ca7e5d3708a2360c63c))
-
-## [1.1.0](https://github.com/redbus-labs/adk-java/compare/v1.0.0...v1.1.0) (2026-02-25)
+### β BREAKING CHANGES
+* remove methods with Optional params from LiveRequest.Builder
+* remove deprecated methods accepting Optional params in InvocationContext
+* remove deprecated BaseToolset.isToolSelected method
+* remove Optional parameters from LlmResponse.Builder's methods
+* remove support for legacy `transferToAgent`, superseded by `transfer_to_agent`
### Features
-* Add ComputerUse tool ([d733a48](https://github.com/redbus-labs/adk-java/commit/d733a480a7a787cb7c32fd3470ab978ca3eb574c))
-* Extend url_context support to Gemini 3 in Java ADK ([2c9d4dd](https://github.com/redbus-labs/adk-java/commit/2c9d4dd5eafe8efe3a2fb099b58e2d0f1d9cad98))
-* Extend url_context support to Gemini 3 in Java ADK ([5f5869f](https://github.com/redbus-labs/adk-java/commit/5f5869f67200831dcbb7ac10ad0d7f44410bc096))
-* Update AgentExecutor so it builds new runner on execute and there is no need to pass the runner instance ([7218295](https://github.com/redbus-labs/adk-java/commit/72182958586e59ccb3d7490cd207ec2837c5b577))
+* add callbacks functionality to the agent executor ([7e8f9dc](https://github.com/google/adk-java/commit/7e8f9dcf82fe7e62aee625fbfaa8673d238ff184))
+* add example on how to expose agent via A2A protocol ([e3ea378](https://github.com/google/adk-java/commit/e3ea378051e5c4e5e5031657467145779e42db55))
+* Adding a Builder for EventsCompactionConfig ([05fbcfc](https://github.com/google/adk-java/commit/05fbcfc933923ae711cd12e7fc9e587fd8e2685c))
+* Adding a SessionKey for typeSafety ([d899f6f](https://github.com/google/adk-java/commit/d899f6f4ad52c84cb4ac8c90d0dc88c22487029c))
+* Adding plugin(Plugin... p) helper methods on App and Runner builders ([dc1a192](https://github.com/google/adk-java/commit/dc1a192a81a92870aa5a4af27a9dc90e81cdaf67))
+* implement partial event aggregation in RemoteA2AAgent ([e064067](https://github.com/google/adk-java/commit/e0640673d212b9849d312953f192f8da51fae85b))
+* remove deprecated BaseToolset.isToolSelected method ([d2f1145](https://github.com/google/adk-java/commit/d2f11456c3a99edd43b3dc0d04743ae7e9390ded))
+* remove deprecated methods accepting Optional params in InvocationContext ([88153c8](https://github.com/google/adk-java/commit/88153c833697a9b9c6ec735a69f48a92cbdfc54b))
+* remove methods with Optional params from LiveRequest.Builder ([84c62a4](https://github.com/google/adk-java/commit/84c62a48ef7b62641722824fe5ba1200606b7b17))
+* remove Optional parameters from LlmResponse.Builder's methods ([a3ac436](https://github.com/google/adk-java/commit/a3ac436bcfa241e90c07485e5da918ec8dbc2b4a))
### Bug Fixes
-* deep-merge stateDelta maps when merging EventActions ([ff07474](https://github.com/redbus-labs/adk-java/commit/ff07474035baec910f0c3fa83b7b1646d8409ffd))
-* drop explicit gemini-1 model version check in GoogleMapsTool ([7953503](https://github.com/redbus-labs/adk-java/commit/7953503e61c547e40a1e1abbece73a99910766c1))
-* include usage_metadata events in live postprocessing ([8137d66](https://github.com/redbus-labs/adk-java/commit/8137d661d7b29eab066c23b7f302068f82423eb7))
-* remove client-side function call IDs from LlmRequest ([99b5fc2](https://github.com/redbus-labs/adk-java/commit/99b5fc26d791175e4dad2c818191c8c31e4269f6))
+* Allow injecting ObjectMapper in FunctionTool, default to ObjectMapper (re. [#473](https://github.com/google/adk-java/issues/473)) ([71b1070](https://github.com/google/adk-java/commit/71b10701e753bddaa96d5e6579b759d2b9bb3e92))
+* downgrade otel.version to 1.51.0 ([117fedf](https://github.com/google/adk-java/commit/117fedf672bb67c4b078ac75ee81a7710452c5b5))
+* Ensure Gemini 3.1 models have events correctly buffered ([acffdb9](https://github.com/google/adk-java/commit/acffdb96bcd8133af99cb0b9426665ba73a83bbc))
+* Exit from rearrangeEventsForLatestFunctionResponse if size of events is less than 2 ([5bc3ef8](https://github.com/google/adk-java/commit/5bc3ef89e62eb3f32ba7e45657c9e40c88c3a5e9))
+* Fixed issue where events were marked empty if the first part had an empty text; now checks all parts for meaningful content ([a0cba25](https://github.com/google/adk-java/commit/a0cba25d691f4be72bea22b0649ecf2d2c110736))
+* prepare JSON serialization for Jackson 2.20.2 and Spring Boot 4.0.2 upgrades ([8c6591b](https://github.com/google/adk-java/commit/8c6591bc4ad86c376cdd70e1bb64f359fbf22fe9))
-### Documentation
+### Miscellaneous Chores
-* Update a parameter name in a comment ([5262d4a](https://github.com/redbus-labs/adk-java/commit/5262d4ae3eca533e1a695e6e2e71c5845055ed5d))
+* revert: switch release please secret to use adk-java-releases-bot's token ([7eafd1b](https://github.com/google/adk-java/commit/7eafd1bd9b16e9ed83dfbc3d0983cfc415c0aaec))
-## [1.0.0](https://github.com/redbus-labs/adk-java/compare/v0.5.0...v1.0.0) (2026-02-17)
+### Code Refactoring
-### β BREAKING CHANGES
+* remove support for legacy `transferToAgent`, superseded by `transfer_to_agent` ([c1ccb2e](https://github.com/google/adk-java/commit/c1ccb2e9d375fedcd7dbb594300e66a1a0488a91))
+
+## [0.7.0](https://github.com/google/adk-java/compare/v0.6.0...v0.7.0) (2026-02-27)
-* Use RxJava for VertexAiClient
-* update default agent dir for the compiled agent loader to match old compiler loader behavior
-* update basellmflow postprocessing to allow emitting original response prior to generating new events
-* Update StreamableHttpServerParameters to avoid SSE mentions and match other MCP builders' structure
-* do not silently fail when an internal error occurs
-* Allow `beforeModelCallback` to modify the LLM request
### Features
-* add `GoogleMapsTool` to enable Google Maps search integration for Gemini 2 models ([1f0425b](https://github.com/redbus-labs/adk-java/commit/1f0425b77ff4a3146279f392505ecd904c055b25))
-* Add a constructor to McpAsyncToolset to allow injecting McpSessionManager ([995aa62](https://github.com/redbus-labs/adk-java/commit/995aa6281f28a55916c01f68667a022064733c28))
-* Add A2A HTTP E2E demo ([467468a](https://github.com/redbus-labs/adk-java/commit/467468af3e2e09784a20498344cf00aff928b4bb))
-* Add ApigeeLlm as a model that let's ADK Agent developers to connect with an Apigee proxy ([b75608f](https://github.com/redbus-labs/adk-java/commit/b75608ffb3f06a6a712e3e742b5ed9bfaea718e4))
-* Add ApigeeLlm as a model that let's ADK Agent developers to connect with an Apigee proxy ([b3ca86e](https://github.com/redbus-labs/adk-java/commit/b3ca86edb94fb97af1ce44382ba762ce8e19f0d8))
-* Add Compact processor to SingleFlow ([ee459b3](https://github.com/redbus-labs/adk-java/commit/ee459b3198d19972744514d1e74f076ee2bd32a7))
-* Add Compaction RequestProcessor for event compaction in llm flow ([af1fafe](https://github.com/redbus-labs/adk-java/commit/af1fafed0470c8afe81679a495ed61664a2cee1a))
-* Add ContextCacheConfig to InvocationContext ([968a9a8](https://github.com/redbus-labs/adk-java/commit/968a9a8944bd7594efc51ed0b5201804133f350e))
-* Add DeepWiki badge to README ([2a44d51](https://github.com/redbus-labs/adk-java/commit/2a44d51901e634bfed1935fe94d42c8583363bc0))
-* Add event compaction config to InvocationContext ([8f7d7ea](https://github.com/redbus-labs/adk-java/commit/8f7d7eac95cc606b5c5716612d0b08c41f951167))
-* Add event compaction framework in Java ADK ([dd68c85](https://github.com/redbus-labs/adk-java/commit/dd68c8565ae43e30c2dd02bc956173ab199ebb56))
-* add eventId in CallbackContext and ToolContext ([ac05fde](https://github.com/redbus-labs/adk-java/commit/ac05fde31ec6a67baf7cacb6144f5912eca029ac))
-* add ExampleTool to ComponentRegistry ([2e1b09f](https://github.com/redbus-labs/adk-java/commit/2e1b09fdd07fb22839ea91bd109e409b44df4f82))
-* Add fluent A2A server methods to LlmAgent and Builder ([2f84791](https://github.com/redbus-labs/adk-java/commit/2f84791689c5aadf4088e078df7fef8d61e76fe4))
-* add fromConfig method to LongRunningFunctionTool ([43613a4](https://github.com/redbus-labs/adk-java/commit/43613a43f65caa976c67d7e456ccd92bb84863ea))
-* Add include_contents option to LlmAgentConfig to control inclusion of previous event contents in LLM requests ([2bfbc8f](https://github.com/redbus-labs/adk-java/commit/2bfbc8fe03f521745528b7277688e3308adbc9b0))
-* Add MCP Toolset support for agent configuration ([bdc39f7](https://github.com/redbus-labs/adk-java/commit/bdc39f738fda8e9f07f929a6b9b4a12a576e0413))
-* add model version to llm response and event ([a0af70e](https://github.com/redbus-labs/adk-java/commit/a0af70e549205187c37c42fff89320c5bc2d84c2))
-* add ParallelAgent.fromConfig() ([b75608f](https://github.com/redbus-labs/adk-java/commit/b75608ffb3f06a6a712e3e742b5ed9bfaea718e4))
-* add ParallelAgent.fromConfig() ([8aeee2b](https://github.com/redbus-labs/adk-java/commit/8aeee2b74cfa19ea514a23a60eacf6c698d7c50a))
-* add response converters to support multiple A2A client events ([4e8de90](https://github.com/redbus-labs/adk-java/commit/4e8de90f13b995c908fc4c6f742bce836e7209db))
-* Add Spring AI 1.1.0 integration to ADK Java ([#482](https://github.com/redbus-labs/adk-java/issues/482)) ([f0c3c06](https://github.com/redbus-labs/adk-java/commit/f0c3c069df6071f88507dc145eef3e23876de5a9))
-* Add support for configuring agent callbacks in YAML ([27c0172](https://github.com/redbus-labs/adk-java/commit/27c01724d6e96da59379e54e3a9dcc138e494b47))
-* Add support for gpt-oss models in LlmRegistry ([e8c3e56](https://github.com/redbus-labs/adk-java/commit/e8c3e56593eab30e0e344ca7e0bee545cefd0213))
-* Add support for gpt-oss models in LlmRegistry ([862d31f](https://github.com/redbus-labs/adk-java/commit/862d31fbca808b821bca2501e0e468beb8121bb1))
-* Add support for programmatic sub-agent resolution using 'code' key ([c498d91](https://github.com/redbus-labs/adk-java/commit/c498d911a6227bfec6df9516c75bc07b7d34fc99))
-* add support for Streamable HTTP Connections to MCP Tools ([bea3244](https://github.com/redbus-labs/adk-java/commit/bea3244c585012194b80754d476eee1b803dbecd))
-* Add token usage threshold to TailRetentionEventCompactor ([9901307](https://github.com/redbus-labs/adk-java/commit/9901307b1cb9be75f2262f116388f93cdcf3eeb6))
-* Add tokenThreshold and eventRetentionSize to EventsCompactionConfig ([588b00b](https://github.com/redbus-labs/adk-java/commit/588b00bbd327e257a78271bf2d929bc52875115f))
-* Add url_context_tool to Java ADK ([9f887c7](https://github.com/redbus-labs/adk-java/commit/9f887c744d9c2775cad459ec6327b1c9e9dfad05))
-* Add VertexAiSearchTool and AgentTools for search ([b48b194](https://github.com/redbus-labs/adk-java/commit/b48b194448c6799e08e778c4efa2d9c920f0c1fb))
-* Adding a .close() method to Runner, Agent and Plugins ([495bf95](https://github.com/redbus-labs/adk-java/commit/495bf95642b9159aa6040868fcaa97fed166035b))
-* Adding a new `ArtifactService.saveAndReloadArtifact()` method ([59e87d3](https://github.com/redbus-labs/adk-java/commit/59e87d319887c588a1ed7d4ca247cd31dffba2c6))
-* adding a new temporary store of context for callbacks ([ed736cd](https://github.com/redbus-labs/adk-java/commit/ed736cdf84d8db92dfde947b5ee84e7430f3ae6d))
-* Adding autoCreateSession in Runner ([6dd51cc](https://github.com/redbus-labs/adk-java/commit/6dd51cc201b15aaa2cebb5372ece647c4484da06))
-* Adding GlobalInstructionPlugin ([72e20b6](https://github.com/redbus-labs/adk-java/commit/72e20b652b8d697e5dc0605db284e3b637f11bac))
-* Adding OnModelErrorCallback ([dfd2944](https://github.com/redbus-labs/adk-java/commit/dfd294448528a9e429ddbbb8e650e432b34fafb2))
-* adding resume / event management primitives ([2de03a8](https://github.com/redbus-labs/adk-java/commit/2de03a86f97eb602dee55270b910d0d425ae75e9))
-* Adding TODO files for reaching idiomatic java ([4ac1dd2](https://github.com/redbus-labs/adk-java/commit/4ac1dd2b6e480fefd4b0a9198b2e69a9c6334c40))
-* Adding validation to BaseAgent ([5dfc000](https://github.com/redbus-labs/adk-java/commit/5dfc000c9019b4d11a33b35c71c2a04d1f657bf2))
-* Adding validation to BaseAgent and RunConfig ([503caa6](https://github.com/redbus-labs/adk-java/commit/503caa6393635a56c672a6592747bcb6e034b8a1))
-* Adding validation to InvocationContext 'session_service', 'invocation_id', ([0502c21](https://github.com/redbus-labs/adk-java/commit/0502c2141724a238bbf5f7a72e1951cbb401a3e8))
-* Adds `ReplayPlugin` to execute Conformance tests ([4b641aa](https://github.com/redbus-labs/adk-java/commit/4b641aa6f02e7bb65a6e701af7317803a3661d0a))
-* Adds schema definition for the recordings for conformance tests ReplayPlugin and also add a RecordingsLoader to load yaml file ([58462fd](https://github.com/redbus-labs/adk-java/commit/58462fdcc09211a1fc5490362497614de066fcff))
-* Adds schema definition for the recordings for conformance tests⦠([17772b2](https://github.com/redbus-labs/adk-java/commit/17772b2429b66f589052e759f3c2ae19b9ed26d1))
-* ADK Plugin Base Class ([dc29535](https://github.com/redbus-labs/adk-java/commit/dc2953545a633db434f2d83d1f539ffd04bf4014))
-* AgentTool.fromConfig() ([c5cbc6d](https://github.com/redbus-labs/adk-java/commit/c5cbc6d13e37feb0ea717de2a53b5fef1f746ee3))
-* Allow EventsCompactionConfig to have a null summarizer initially ([229654e](https://github.com/redbus-labs/adk-java/commit/229654e20a6ffc733854e3c0de9049bbad494228))
-* create customMetadata() mutable map in BaseTool ([5aa9c83](https://github.com/redbus-labs/adk-java/commit/5aa9c835ee5de0f32262d32c97f3e9c2de65256a))
-* enable LoopAgent configuration ([d1a1cea](https://github.com/redbus-labs/adk-java/commit/d1a1cea4a633f376463d7e47b79bfb67126537ad))
-* Enhance A2A service with console output, session state initialization, and unary RPC support ([2596677](https://github.com/redbus-labs/adk-java/commit/2596677c1b76229dd7fd58912b9d07d03c8cb732))
-* Enhance event processing in PostgresSessionService during appendEvent ([b293128](https://github.com/redbus-labs/adk-java/commit/b293128a1b50ec2be34bc951324c72ae6d23d411))
-* EventAction.stateDelta() now has a remove by key variant ([32a6b62](https://github.com/redbus-labs/adk-java/commit/32a6b625d96e5658be77d5017f10014d8d4036c1))
-* expose meta() and annotations() methods in AbstractMcpTool ([04cd6ea](https://github.com/redbus-labs/adk-java/commit/04cd6eaf5d52e89c14933098afa94b1712af7eb3))
-* Extend google_search support to Gemini 3 in Java ADK ([ddb00ef](https://github.com/redbus-labs/adk-java/commit/ddb00efc1a1f531448b9f4dae28d647c6ffdf420))
-* Fix a handful of small changes related to headers, logging and javadoc ([0b63ca3](https://github.com/redbus-labs/adk-java/commit/0b63ca30294ea05572707c420306ae41bf7d60c7))
-* Forward state delta to parent session ([00d6d30](https://github.com/redbus-labs/adk-java/commit/00d6d3034e07ceaa738a1ff1384d8fd879339b06))
-* HITL - remove the events between the confirmed FC & its response ([3670555](https://github.com/redbus-labs/adk-java/commit/367055544509321e845712b89b793c98e0dc510d))
-* HITL - Revert the "Boolean confirmation" changes, we'll fix it differently ([f65e58b](https://github.com/redbus-labs/adk-java/commit/f65e58bd73ea33b38d5fe43c897b01216ac34ac6))
-* HITL/Introduce ToolConfirmations and integrate them into ToolContext ([d843e00](https://github.com/redbus-labs/adk-java/commit/d843e00b319ff0c35b8fb188f1e52b55e007915f))
-* HITL/Introduce ToolConfirmations and integrate them into ToolContext ([b177111](https://github.com/redbus-labs/adk-java/commit/b1771112395b1ca39519f2a1bf57ed7996183d13))
-* HITL/Introduce ToolConfirmations and integrate them into ToolContext ([4689ed0](https://github.com/redbus-labs/adk-java/commit/4689ed0f5c8b4dbfcdf6240fe652a425b6d8f0b6))
-* HITL/Wire up tool confirmation support ([2bfc95d](https://github.com/redbus-labs/adk-java/commit/2bfc95d7c602e00935066b0d8d0dbde0fa7597be))
-* **HITL:** Declining a proposal now correctly intercepts the run ([9611f89](https://github.com/redbus-labs/adk-java/commit/9611f8967e528c6242e17ad3ad5419e0b25fb3fb))
-* **HITL:** Let ADK resume after HITL approval is present ([d04c072](https://github.com/redbus-labs/adk-java/commit/d04c0726965b3e73f6e5ac2336473808a9d98003))
-* **HITL:** Let ADK resume after HITL approval is present ([9611f89](https://github.com/redbus-labs/adk-java/commit/9611f8967e528c6242e17ad3ad5419e0b25fb3fb))
-* implement SequentialAgent.fromConfig() ([b75608f](https://github.com/redbus-labs/adk-java/commit/b75608ffb3f06a6a712e3e742b5ed9bfaea718e4))
-* implement SequentialAgent.fromConfig() ([82fbdac](https://github.com/redbus-labs/adk-java/commit/82fbdac311af74248d348d91f68106e4d3b0b5c5))
-* Implementation of a session service for the ADK (Agent Development Kit) that uses Google Firestore as the backend for storing session data. ([b75608f](https://github.com/redbus-labs/adk-java/commit/b75608ffb3f06a6a712e3e742b5ed9bfaea718e4))
-* Improving LoggingPlugin ([acfaa04](https://github.com/redbus-labs/adk-java/commit/acfaa04284dec12fa7245caee11cd7a3d8e4342c))
-* Include a2a modules in default build ([1a3f513](https://github.com/redbus-labs/adk-java/commit/1a3f513a7ce084413b7bdd52bda72238cf22b235))
-* Include samples in the build ([5091f44](https://github.com/redbus-labs/adk-java/commit/5091f443751a40d652e2def0a81e13522a575cf1))
-* Integrate event compaction in Java ADK runner ([54c826c](https://github.com/redbus-labs/adk-java/commit/54c826c80c2bfe09056396c2a21f8241f9d2898b))
-* Integrating Plugin with ADK ([833f8f9](https://github.com/redbus-labs/adk-java/commit/833f8f9d8e82df82de25072f0b911297c88ef56c))
-* Integrating Plugin with ADK ([c037893](https://github.com/redbus-labs/adk-java/commit/c037893fe3554e37112ad22641b0a4578b06de0f))
-* Introduce ExampleTool for few-shot examples in LlmAgent ([2162f89](https://github.com/redbus-labs/adk-java/commit/2162f8908232e42abcdf2d7a8fa848933619fc3e))
-* Introduce TailRetentionEventCompactor to compact and retain the tail of the event stream ([efe58d6](https://github.com/redbus-labs/adk-java/commit/efe58d6e0e5e0ff35d39e56bcb0f57cc6ccc7ccc))
-* Introduce the `App` class for defining agentic applications ([d7c5c6f](https://github.com/redbus-labs/adk-java/commit/d7c5c6f4bdc2c2b06448af72bc311abf36b8e726))
-* introduces context caching configuration for apps, ported from Python ADK ([12defee](https://github.com/redbus-labs/adk-java/commit/12defeedbaf6048bc83d484f421131051b7e81a5))
-* listSessions returns sessions with empty state ([d843e00](https://github.com/redbus-labs/adk-java/commit/d843e00b319ff0c35b8fb188f1e52b55e007915f))
-* Make StreamableHttpServerParameters class non-final to allow subclassing ([bc3ae43](https://github.com/redbus-labs/adk-java/commit/bc3ae4349b1734a532d46368567a9476468e28fa))
-* mark a2a module as experimental ([626f171](https://github.com/redbus-labs/adk-java/commit/626f1714cf6fea451589dcdfbe1705afeb250930))
-* new ContextFilterPlugin ([f8e9bc3](https://github.com/redbus-labs/adk-java/commit/f8e9bc30350082f048cb0ded6226f27f80655602))
-* prettier replay diffs ([b75608f](https://github.com/redbus-labs/adk-java/commit/b75608ffb3f06a6a712e3e742b5ed9bfaea718e4))
-* prettier replay diffs ([ae72dde](https://github.com/redbus-labs/adk-java/commit/ae72ddecd87cc45be8cea6e8121af58599cc63ea))
-* Refactor EventsCompactionConfig to require a summarizer ([864d606](https://github.com/redbus-labs/adk-java/commit/864d6066eb98af6567592055f7cd24cb78defaf3))
-* refactor remote A2A agent to use A2A SDK client ([7792233](https://github.com/redbus-labs/adk-java/commit/7792233832e95dfe1ae93b04d91bd7507c37cc8d))
-* Refine bug and feature request issue templates ([3e74c9a](https://github.com/redbus-labs/adk-java/commit/3e74c9a960cba6582e914d36925516039d57913c))
-* register GoogleMapsTool in ComponentRegistry ([464f0b2](https://github.com/redbus-labs/adk-java/commit/464f0b2fc0231dbe161b0b5fe524687bb304cd49))
-* Reorder compaction events in chronological order ([66e2296](https://github.com/redbus-labs/adk-java/commit/66e22964e67d0756e3351dae93e18aa5ae73f22e))
-* Setting up data structures for pause/resume/rewind ([c6c52c4](https://github.com/redbus-labs/adk-java/commit/c6c52c43439468eb87fc6a029fa25a46a35dd6e7))
-* Skip post-invocation compaction if parameters not set ([76f86c5](https://github.com/redbus-labs/adk-java/commit/76f86c54eb1a242e604f7b43e3ee18940168b6ec))
-* SSE implementation with HttpServer (default) and Spring (alternative) ([c0c41a9](https://github.com/redbus-labs/adk-java/commit/c0c41a90ef43ae4534613421d2b1f168920e3920))
-* SSE implementation with HttpServer (default) and Spring (alternative) ([91d5937](https://github.com/redbus-labs/adk-java/commit/91d59375a3579dad1f9a587ef0d10f2dc958af54))
-* Support configuring tool execution mode in RunConfig ([ad901e2](https://github.com/redbus-labs/adk-java/commit/ad901e25e26f807ad4c30e6aea1bee377f824739))
-* Support configuring tool execution mode in RunConfig ([154a7d6](https://github.com/redbus-labs/adk-java/commit/154a7d62b4027cd70abe1b9585ac2f34a3fd02d6))
-* Support function calls in LLM event summarizer ([55144ac](https://github.com/redbus-labs/adk-java/commit/55144aca3c1d77e06cf7101cf2504311c0585ed1))
-* support stdio_connection_params in McpToolset config ([cc1588a](https://github.com/redbus-labs/adk-java/commit/cc1588a3e669dc670595ecbdebb12dc9d2ae40f0))
-* Support toolFilters in McpAsyncToolset identical to McpToolset ([99b767a](https://github.com/redbus-labs/adk-java/commit/99b767a5229902958c18d94a228c24d044314613))
-* Supports `-DextraPlugins` in maven_plugin WebMojo to allow start AdkWebServer with extraPlugin for conformance tests ([224552a](https://github.com/redbus-labs/adk-java/commit/224552aa8ad859396ae3f06ccad053210b043a50))
-* Supports `stateDelta` in ADK Java AgentRunRequest for ADK web server ([d606ef1](https://github.com/redbus-labs/adk-java/commit/d606ef18b0bbb6f275312d996f237b5eb621e415))
-* Supports `stateDelta` in ADK Java AgentRunRequest for ADK web server ([54bee7b](https://github.com/redbus-labs/adk-java/commit/54bee7ba4de7d59599eaa665659c61e524143efe))
-* Token count estimation fallback for tail retention compaction ([3338565](https://github.com/redbus-labs/adk-java/commit/3338565cff976fdad1eda1fccafef58c9d4a51ba))
-* **transcription:** Add audio transcription capability ([0324fb4](https://github.com/redbus-labs/adk-java/commit/0324fb4c692078be11044cf7b567e7eb5cb2151f))
-* **transcription:** Add audio transcription capability ([8f6e3b2](https://github.com/redbus-labs/adk-java/commit/8f6e3b24b1978be705b5512794af3265ea5e9d28))
-* Update event compaction logic to include events after compaction end times ([ea12505](https://github.com/redbus-labs/adk-java/commit/ea12505d7c4e22a237db5a8d3f78564ace0b216b))
-* Update ReadonlyContext to expose the session userId ([f19bd99](https://github.com/redbus-labs/adk-java/commit/f19bd99086e2e65650c94272073615348bca770e))
-* Updating Baseline Code executors ([a3f1763](https://github.com/redbus-labs/adk-java/commit/a3f176322c47354d5c18d8371cb38bd2dd719904))
-* updating Telemetry ([5ba63f4](https://github.com/redbus-labs/adk-java/commit/5ba63f4015d369bc58ad7dfe76198acf003e7450))
-* Updating the Tracing implementation and updating BaseAgent.runLive ([8acb1ea](https://github.com/redbus-labs/adk-java/commit/8acb1eafb099723dfae065d8b9339bb5180aa26f))
-* use Credentials' request metadata to populate headers ([e01df11](https://github.com/redbus-labs/adk-java/commit/e01df116e311016df92e69487c0a6607b00384bc))
+* Add ComputerUse tool ([d733a48](https://github.com/google/adk-java/commit/d733a480a7a787cb7c32fd3470ab978ca3eb574c))
+* add the AgentExecutor config ([e0f7137](https://github.com/google/adk-java/commit/e0f7137253c9bd929fe3ea899e32f4b61f994986))
+* drop gemini-1 support in GoogleSearchTool ([15255b4](https://github.com/google/adk-java/commit/15255b48285819c7d3aedb4470e91f37d1bcfaf4))
+* Extend url_context support to Gemini 3 in Java ADK ([2c9d4dd](https://github.com/google/adk-java/commit/2c9d4dd5eafe8efe3a2fb099b58e2d0f1d9cad98))
+* Extend url_context support to Gemini 3 in Java ADK ([5f5869f](https://github.com/google/adk-java/commit/5f5869f67200831dcbb7ac10ad0d7f44410bc096))
+* Handle final and error TaskStatusUpdateEvents ([746e857](https://github.com/google/adk-java/commit/746e857d97c6f356ffe5c20be0ccae85d5a8f989))
+* remove model restrictions in BuiltInCodeExecutionTool ([1a593a9](https://github.com/google/adk-java/commit/1a593a996607904eed24b64bc63eecd7708710af))
+* Update AgentExecutor so it builds new runner on execute and there is no need to pass the runner instance ([7218295](https://github.com/google/adk-java/commit/72182958586e59ccb3d7490cd207ec2837c5b577))
### Bug Fixes
-* `deltaState` should be appended with `newMessage` event and `beforeRunCallback` should be called after that ([c175fe2](https://github.com/redbus-labs/adk-java/commit/c175fe20d258a5ff3641f3723a5a601c5aaf7840))
-* add missing avgLogprobs, finishReason and usageMetadata fields ([4dd09e7](https://github.com/redbus-labs/adk-java/commit/4dd09e7a3c008b0da11dbf25e1fed0efa9c5f2d7))
-* Add name and description to configagent pom.xml ([4948bfc](https://github.com/redbus-labs/adk-java/commit/4948bfc9a35ea22660f37a6afc3474fab220b630))
-* Add OpenTelemetry context propagation to span creation ([717d3e4](https://github.com/redbus-labs/adk-java/commit/717d3e4b33ef7d25c607cea996309a69b7ae27fb))
-* ADK Session State Serialization error with "removed" sentinels ([939de25](https://github.com/redbus-labs/adk-java/commit/939de25223b854293c001e81cd4cb02c0070b091))
-* Align InMemorySessionService listSessions with Python implementa⦠([442fd4a](https://github.com/redbus-labs/adk-java/commit/442fd4af3156d6c3b8b5d0277ca65f9da1b5de4f))
-* Align InMemorySessionService listSessions with Python implementation ([9434949](https://github.com/redbus-labs/adk-java/commit/94349499d03f3a131af4464def4b208db52a8feb))
-* Allow `beforeModelCallback` to modify the LLM request ([8e10df2](https://github.com/redbus-labs/adk-java/commit/8e10df2a543a6ffc1eb91c9ff135ade19bcd975c))
-* allow using legacy "transferToAgent(agentName)" to maintain backwards compatibility ([5d8f85b](https://github.com/redbus-labs/adk-java/commit/5d8f85b79ccb9c2bd79fc1fe7e63541f83bdb02b))
-* Always use a mutable HashMap for default function arguments ([c6c9557](https://github.com/redbus-labs/adk-java/commit/c6c9557ff28feece54265fcff82478156afbe67f))
-* ApigeeLLM support for Built-in tools like GoogleSearch, BuiltInCodeExecutor when calling Gemini models through Apigee ([f0da2b4](https://github.com/redbus-labs/adk-java/commit/f0da2b436fe2b6d1e2a4271486f946bfbac6a857))
-* Avoid ClassCastException and reduce copy/pasta π in FunctionTool ([639b04a](https://github.com/redbus-labs/adk-java/commit/639b04a97743b81a7051274259eae53542a44a33))
-* avoid timing out slow agents when using the run_sse endpoint ([0f4df64](https://github.com/redbus-labs/adk-java/commit/0f4df64858e588e45be70435c73e3c00a1394b1d))
-* Clarify load_artifact prompt ([5f854ba](https://github.com/redbus-labs/adk-java/commit/5f854bacf8ee075105093a6c84273a74df21aca3))
-* do not silently fail when an internal error occurs ([073d5e8](https://github.com/redbus-labs/adk-java/commit/073d5e801779d6390bc88572e51d74eec47f20b6))
-* do not silently ignore exceptions thrown from runLive() ([6b25025](https://github.com/redbus-labs/adk-java/commit/6b25025c23230ea419c7e6e61e331d23a75ed951))
-* emit multiple LlmResponses in GeminiLlmConnection ([7bf55f1](https://github.com/redbus-labs/adk-java/commit/7bf55f1be6381ae5319bb0532f32c0287461546d))
-* Events for HITL are now emitted correctly ([9611f89](https://github.com/redbus-labs/adk-java/commit/9611f8967e528c6242e17ad3ad5419e0b25fb3fb))
-* Exclude model thoughts when saving LLM output to state ([44d6a21](https://github.com/redbus-labs/adk-java/commit/44d6a215feaccfb3d9940340353b7495270a81e0))
-* fix linter error ([f49260e](https://github.com/redbus-labs/adk-java/commit/f49260e05c5d36b85066caf299fda9346b6ff788))
-* Fixes the instruction appending logic to be consistent with adk python ([1ca24c5](https://github.com/redbus-labs/adk-java/commit/1ca24c59bc9a71d4d2c76740d8392a27eca24df3))
-* Fixing a problem with serializing sessions that broke integration with Vertex AI Session Service ([8190ed3](https://github.com/redbus-labs/adk-java/commit/8190ed3d78667875ee0772e52b7075dcdaa14963))
-* Fixing a regression in InMemorySessionService ([d11bedf](https://github.com/redbus-labs/adk-java/commit/d11bedf42976242d1c3dd6b99ebae0babe59535c))
-* Fixing Vertex session storage ([5607f64](https://github.com/redbus-labs/adk-java/commit/5607f644c95a053bf381c2021879e6f31d5c6bde))
-* Gemini thoughts not correctly accumulated when streaming enabled ([2df44de](https://github.com/redbus-labs/adk-java/commit/2df44de6f83bd17812f87ad5e5c0bf881ce99e74))
-* HITL endless loop when asking for approvals ([9611f89](https://github.com/redbus-labs/adk-java/commit/9611f8967e528c6242e17ad3ad5419e0b25fb3fb))
-* Ignore case when determining if the last message comes from a user ([cce4774](https://github.com/redbus-labs/adk-java/commit/cce4774126180f4022d658138b59223dc5c1c9a9))
-* improve gemini text aggregation in streaming responses ([b75608f](https://github.com/redbus-labs/adk-java/commit/b75608ffb3f06a6a712e3e742b5ed9bfaea718e4))
-* improve gemini text aggregation in streaming responses ([9bb0207](https://github.com/redbus-labs/adk-java/commit/9bb020703eded122561b80b02ee9e9c9cca61246))
-* Include output schema in MCP tool declarations and add filesystem sample ([6d5edd5](https://github.com/redbus-labs/adk-java/commit/6d5edd54c4fa91dc4d8531aa6ec8a4866cba450c))
-* Increase default MCP client timeouts to 5 minutes ([d46673e](https://github.com/redbus-labs/adk-java/commit/d46673e23960360491b69d20fff2e399b0606d09))
-* initial state for session creation ([b75608f](https://github.com/redbus-labs/adk-java/commit/b75608ffb3f06a6a712e3e742b5ed9bfaea718e4))
-* initial state for session creation ([adc716e](https://github.com/redbus-labs/adk-java/commit/adc716ec702df051a0ecd8f0947bc434f7512b00))
-* initial state for session creation ([13db9d2](https://github.com/redbus-labs/adk-java/commit/13db9d2148db120a3c2ffcaf08eaafc40c4fef5f))
-* InMemorySessionService mergeWithGlobalState not called in appendEvent ([03d043f](https://github.com/redbus-labs/adk-java/commit/03d043fa7db5206c7b9a7cc6dd1bd4161fc40dcb))
-* javadocs in ResponseConverter ([be35b22](https://github.com/redbus-labs/adk-java/commit/be35b2277e8291336013623cb9f0c86f62ed1f43))
-* Make FunctionResponses respect the order of FunctionCalls ([a99c75b](https://github.com/redbus-labs/adk-java/commit/a99c75bf79d86866db26135568bf36b685886659))
-* Make FunctionTool slightly more null safe, and use Text Blocks ([a2295a3](https://github.com/redbus-labs/adk-java/commit/a2295a3320aa23c43228a8db9166dede07919004))
-* make system instructions parts concatenation & identity preprocessor more consistent with python adk version ([e06747e](https://github.com/redbus-labs/adk-java/commit/e06747eb58ed3effb1d79b31d4bee44649ace078))
-* make system instructions parts concatenation & identity preprocessor more consistent with python adk version ([9360f24](https://github.com/redbus-labs/adk-java/commit/9360f24b7eef36b282ba8508a6382314f4efb9d9))
-* Making stepsCompleted thread-safe ([d432c64](https://github.com/redbus-labs/adk-java/commit/d432c6414128cf83eb0211eb18ef058dbbcd1807))
-* Merging of events in rearrangeEventsForAsyncFunctionResponsesInHistory ([67c29e3](https://github.com/redbus-labs/adk-java/commit/67c29e3a33bda22d8a18a17c99e5abc891bf19f8))
-* Mutate EventActions in-place in AgentTool ([ded5a4e](https://github.com/redbus-labs/adk-java/commit/ded5a4e760055d3d2bcd74d3bd8f21517821e7d0))
-* pass mutable function args map to beforeToolCallback ([e989ae1](https://github.com/redbus-labs/adk-java/commit/e989ae1337a84fd6686504050d2a3bf2db15c32c))
-* populate finishReason in LlmResponse ([dace210](https://github.com/redbus-labs/adk-java/commit/dace2106cd2451d8271c842da13daff65de0922e))
-* preserve other fields of a part when updating function call ([b75608f](https://github.com/redbus-labs/adk-java/commit/b75608ffb3f06a6a712e3e742b5ed9bfaea718e4))
-* preserve other fields of a part when updating function call ([c2c4e46](https://github.com/redbus-labs/adk-java/commit/c2c4e46b731953895cc307617390583bd20d687a))
-* propagate thought signatures in BaseLlmFlow ([0b8b35b](https://github.com/redbus-labs/adk-java/commit/0b8b35bf92cfb5977adb2b9749d38d9246c9b54e))
-* Propagate trace context across async boundaries ([279c977](https://github.com/redbus-labs/adk-java/commit/279c977d9eefda39159dd4bd86acea03a47c6101))
-* recursively extract input/output schema for AgentTool ([7019d39](https://github.com/redbus-labs/adk-java/commit/7019d39e490cef1b4b443d1755547a3a701bc964))
-* Reduce the logging level ([dd601ca](https://github.com/redbus-labs/adk-java/commit/dd601ca8ed939d42fa186113bf0dca31c6e4a6db))
-* refine agent transfer instructions and tool definition ([12bf4ef](https://github.com/redbus-labs/adk-java/commit/12bf4effcbf4b2e5ee12d569fb3d7cbaf4c706dc))
-* register url_context tool in ComponentRegistry ([d9dd5db](https://github.com/redbus-labs/adk-java/commit/d9dd5dbde872e247e66c8cfbf40adfaf2c3ad554))
-* Remove checking ToolConfirmation from Functions to align with Python SDK ([0724330](https://github.com/redbus-labs/adk-java/commit/0724330c66d26b2e80e458663ca88bb333c40c2c))
-* Remove obsolete [@param](https://github.com/param) tags from SessionController Javadoc ([a77971a](https://github.com/redbus-labs/adk-java/commit/a77971a9ac983acbceab15db7eeb36460a0ba759))
-* Replace [@api](https://github.com/api)Note with <p> in Javadoc comments. ([ac16d53](https://github.com/redbus-labs/adk-java/commit/ac16d53db0d7b0d2a3aa3a12c1db1f819d7c6c21))
-* restore invocationContext() method ([c9e2a5b](https://github.com/redbus-labs/adk-java/commit/c9e2a5b37b31f5fa0e0a193076f7dc836320de97))
-* restore old default method behavior gemini utils ([b75608f](https://github.com/redbus-labs/adk-java/commit/b75608ffb3f06a6a712e3e742b5ed9bfaea718e4))
-* restore old default method behavior gemini utils ([a440454](https://github.com/redbus-labs/adk-java/commit/a4404542d134af5cc3d5750c8638eb3f74f33025))
-* restore old default method behavior gemini utils ([c38ebef](https://github.com/redbus-labs/adk-java/commit/c38ebef4525c07b448f8f329933e416e6678ad86))
-* Return Completable from `saveArtifact` in `CallbackContext` ([84e755c](https://github.com/redbus-labs/adk-java/commit/84e755c90dc55e7c437317166872696078c13fbd))
-* revert: Merging of events in rearrangeEventsForAsyncFunctionResponsesInHistory ([101adce](https://github.com/redbus-labs/adk-java/commit/101adce314dd65328af6ad9281afb46f9b160c1a))
-* Saving session state to postgres while appendEvent happens. ([26e2d60](https://github.com/redbus-labs/adk-java/commit/26e2d6047cadfc8337ad1254c30f192f0194ce68))
-* support non-map return values returned from Function Tools by automatically wrapping them into {"result": <value>} ([85ba370](https://github.com/redbus-labs/adk-java/commit/85ba37053099e9cddfab992f616c84142d87bf31))
-* Support parameterized List parameters for Function tools ([89fb519](https://github.com/redbus-labs/adk-java/commit/89fb519f1567d519367ee41bb993d4c293cb10c7))
-* Update A2aService.java header date to January 18, 2026 ([9b130f5](https://github.com/redbus-labs/adk-java/commit/9b130f5da2cec38e0592e6263a62c097d89e5573))
-* update ADkWebServer start() to override socket property needed to support Gemini Live API message size ([882d4d9](https://github.com/redbus-labs/adk-java/commit/882d4d9498b82fcbbadb5b234d3f09f0a6001d4f))
-* Update AgentTool to drop thought parts from response ([2a86ae8](https://github.com/redbus-labs/adk-java/commit/2a86ae8122c93d1e31ab65e4fe89eda3ca256c72))
-* update basellmflow postprocessing to allow emitting original response prior to generating new events ([3e760e0](https://github.com/redbus-labs/adk-java/commit/3e760e05af8c4900287a0008997cb388fc7cca5c))
-* update converters package classes ([b66e4a5](https://github.com/redbus-labs/adk-java/commit/b66e4a5280688a9533ed314103a0b290191a51cf))
-* update default agent dir for the compiled agent loader to match old compiler loader behavior ([e43bba7](https://github.com/redbus-labs/adk-java/commit/e43bba791cb3ec3777d9af22ef086cd126e73154))
-* update EmbeddingModelDiscoveryTest package statement ([adeb9dc](https://github.com/redbus-labs/adk-java/commit/adeb9dca945004334f4af6a6442e41dd856d1612))
-* Update HITL/Tool workflows to correctly pause and resume runner operations ([2906eb5](https://github.com/redbus-labs/adk-java/commit/2906eb516327f375e1ef60147ea1a6c3dfd6c11c))
-* Update package name to com.example.helloworld ([c7d01f0](https://github.com/redbus-labs/adk-java/commit/c7d01f09cbad1b9adea2dd0f0a5770bab0ab4378))
-* Update pom.xml files ([1d47235](https://github.com/redbus-labs/adk-java/commit/1d4723586592bf1d1fc662166705467f28fb5155))
-* Update Spring AI to 1.1.0 and disable Ollama tests for CI ([03e5d11](https://github.com/redbus-labs/adk-java/commit/03e5d116e1d2cb7807b6048189d25c7467a6a111))
-* update test utils for latest GenAI SDK version ([1556cc2](https://github.com/redbus-labs/adk-java/commit/1556cc2c6924b47856291385ce2ab7aeb12133e3))
-* Updated BasePlugin JavaDoc for name parameter ([2e59550](https://github.com/redbus-labs/adk-java/commit/2e59550eff9ad50e81c310ba83b9d49af6bb8987))
-* Use JsonBaseModel in FunctionTool (re. [#473](https://github.com/redbus-labs/adk-java/issues/473)) ([e60bddf](https://github.com/redbus-labs/adk-java/commit/e60bddf6502bbfac5af3fb993acc100439c0d90f))
-* use SLF4J's logger in FunctionTool exception's handling ([de2f64f](https://github.com/redbus-labs/adk-java/commit/de2f64fa71d723b3396636b408a3b406191ad3e7))
+* change Session events list to a threadsafe implementation by default ([0b5ac92](https://github.com/google/adk-java/commit/0b5ac9214926200c3d65d64d8c10489847c29291))
+* deep-merge stateDelta maps when merging EventActions ([ff07474](https://github.com/google/adk-java/commit/ff07474035baec910f0c3fa83b7b1646d8409ffd))
+* drop explicit gemini-1 model version check in GoogleMapsTool ([7953503](https://github.com/google/adk-java/commit/7953503e61c547e40a1e1abbece73a99910766c1))
+* LlmAgent model name resolution and improve Gemini-3 model detection logic ([313ce85](https://github.com/google/adk-java/commit/313ce8590982346bb8ac631b4bf88da76fb849a4))
+* make a mutable copy of function args for the beforeToolCallback invocations ([64d3a77](https://github.com/google/adk-java/commit/64d3a775d68610d20c084678ffdc559cd467e627))
### Documentation
-* Add GEMINI.md for vibecoding with Gemini CLI ([51073d5](https://github.com/redbus-labs/adk-java/commit/51073d5f25835c620ee40f2141c7e5179b8e6aeb))
-* Adds missing comments ([743b85c](https://github.com/redbus-labs/adk-java/commit/743b85cd4de14267b8a26d50b4567d3a3be3af00))
-* Adjust heading levels in WebMojo Javadoc ([62ed9d8](https://github.com/redbus-labs/adk-java/commit/62ed9d85e35b29fd67e94f1192f3e6e077ad5c11))
-* Fix formatAsSearchResponse reference in EventProcessor example ([c8df0c7](https://github.com/redbus-labs/adk-java/commit/c8df0c7bb3375c5773575681fcf4efbc891c3132))
-* Remove search/MRI references from documentation ([3f1c590](https://github.com/redbus-labs/adk-java/commit/3f1c590448b3a0a9c2f2831a628dc037cffcc8ad))
-* Update comment in Runner ([fe00ef8](https://github.com/redbus-labs/adk-java/commit/fe00ef87f9c7cdf3d1005a411055b90cebdd0c98))
-* update ComponentRegistry's doc ([441c9a6](https://github.com/redbus-labs/adk-java/commit/441c9a67cff6f2160db5c4dc50b3f7c6200a386f))
-* Update example path to be generic (remove search reference) ([303437b](https://github.com/redbus-labs/adk-java/commit/303437b816cc2ce9492537d9448fca1bc8eb29a9))
-* Update GEMINI.md with style of not using fully qualified name ([8911f26](https://github.com/redbus-labs/adk-java/commit/8911f26ffa00a9d66cbee1b45d828562875c023b))
+* Update a parameter name in a comment ([5262d4a](https://github.com/google/adk-java/commit/5262d4ae3eca533e1a695e6e2e71c5845055ed5d))
+## [0.6.0](https://github.com/google/adk-java/compare/v0.5.0...v0.6.0) (2026-02-19)
-### Miscellaneous Chores
-* Update StreamableHttpServerParameters to avoid SSE mentions and match other MCP builders' structure ([6f26e30](https://github.com/redbus-labs/adk-java/commit/6f26e30ef935bc9999ad2317e873243873683cc9))
+### Features
+* Add Compact processor to SingleFlow ([ee459b3](https://github.com/google/adk-java/commit/ee459b3198d19972744514d1e74f076ee2bd32a7))
+* Add Compaction RequestProcessor for event compaction in llm flow ([af1fafe](https://github.com/google/adk-java/commit/af1fafed0470c8afe81679a495ed61664a2cee1a))
+* Add ContextCacheConfig to InvocationContext ([968a9a8](https://github.com/google/adk-java/commit/968a9a8944bd7594efc51ed0b5201804133f350e))
+* Add event compaction config to InvocationContext ([8f7d7ea](https://github.com/google/adk-java/commit/8f7d7eac95cc606b5c5716612d0b08c41f951167))
+* Add event compaction framework in Java ADK ([dd68c85](https://github.com/google/adk-java/commit/dd68c8565ae43e30c2dd02bc956173ab199ebb56))
+* add eventId in CallbackContext and ToolContext ([ac05fde](https://github.com/google/adk-java/commit/ac05fde31ec6a67baf7cacb6144f5912eca029ac))
+* add ExampleTool to ComponentRegistry ([2e1b09f](https://github.com/google/adk-java/commit/2e1b09fdd07fb22839ea91bd109e409b44df4f82))
+* add response converters to support multiple A2A client events ([4e8de90](https://github.com/google/adk-java/commit/4e8de90f13b995c908fc4c6f742bce836e7209db))
+* Add token usage threshold to TailRetentionEventCompactor ([9901307](https://github.com/google/adk-java/commit/9901307b1cb9be75f2262f116388f93cdcf3eeb6))
+* Add tokenThreshold and eventRetentionSize to EventsCompactionConfig ([588b00b](https://github.com/google/adk-java/commit/588b00bbd327e257a78271bf2d929bc52875115f))
+* Add VertexAiSearchTool and AgentTools for search ([b48b194](https://github.com/google/adk-java/commit/b48b194448c6799e08e778c4efa2d9c920f0c1fb))
+* Adding a .close() method to Runner, Agent and Plugins ([495bf95](https://github.com/google/adk-java/commit/495bf95642b9159aa6040868fcaa97fed166035b))
+* Adding a new `ArtifactService.saveAndReloadArtifact()` method ([59e87d3](https://github.com/google/adk-java/commit/59e87d319887c588a1ed7d4ca247cd31dffba2c6))
+* adding a new temporary store of context for callbacks ([ed736cd](https://github.com/google/adk-java/commit/ed736cdf84d8db92dfde947b5ee84e7430f3ae6d))
+* Adding autoCreateSession in Runner ([6dd51cc](https://github.com/google/adk-java/commit/6dd51cc201b15aaa2cebb5372ece647c4484da06))
+* Adding GlobalInstructionPlugin ([72e20b6](https://github.com/google/adk-java/commit/72e20b652b8d697e5dc0605db284e3b637f11bac))
+* Adding OnModelErrorCallback ([dfd2944](https://github.com/google/adk-java/commit/dfd294448528a9e429ddbbb8e650e432b34fafb2))
+* adding resume / event management primitives ([2de03a8](https://github.com/google/adk-java/commit/2de03a86f97eb602dee55270b910d0d425ae75e9))
+* Adding TODO files for reaching idiomatic java ([4ac1dd2](https://github.com/google/adk-java/commit/4ac1dd2b6e480fefd4b0a9198b2e69a9c6334c40))
+* Adding validation to BaseAgent ([5dfc000](https://github.com/google/adk-java/commit/5dfc000c9019b4d11a33b35c71c2a04d1f657bf2))
+* Adding validation to BaseAgent and RunConfig ([503caa6](https://github.com/google/adk-java/commit/503caa6393635a56c672a6592747bcb6e034b8a1))
+* Adding validation to InvocationContext 'session_service', 'invocation_id', ([0502c21](https://github.com/google/adk-java/commit/0502c2141724a238bbf5f7a72e1951cbb401a3e8))
+* Allow EventsCompactionConfig to have a null summarizer initially ([229654e](https://github.com/google/adk-java/commit/229654e20a6ffc733854e3c0de9049bbad494228))
+* enable LoopAgent configuration ([d1a1cea](https://github.com/google/adk-java/commit/d1a1cea4a633f376463d7e47b79bfb67126537ad))
+* EventAction.stateDelta() now has a remove by key variant ([32a6b62](https://github.com/google/adk-java/commit/32a6b625d96e5658be77d5017f10014d8d4036c1))
+* Extend google_search support to Gemini 3 in Java ADK ([ddb00ef](https://github.com/google/adk-java/commit/ddb00efc1a1f531448b9f4dae28d647c6ffdf420))
+* Fix a handful of small changes related to headers, logging and javadoc ([0b63ca3](https://github.com/google/adk-java/commit/0b63ca30294ea05572707c420306ae41bf7d60c7))
+* Forward state delta to parent session ([00d6d30](https://github.com/google/adk-java/commit/00d6d3034e07ceaa738a1ff1384d8fd879339b06))
+* HITL - remove the events between the confirmed FC & its response ([3670555](https://github.com/google/adk-java/commit/367055544509321e845712b89b793c98e0dc510d))
+* HITL - Revert the "Boolean confirmation" changes, we'll fix it differently ([f65e58b](https://github.com/google/adk-java/commit/f65e58bd73ea33b38d5fe43c897b01216ac34ac6))
+* **HITL:** Declining a proposal now correctly intercepts the run ([9611f89](https://github.com/google/adk-java/commit/9611f8967e528c6242e17ad3ad5419e0b25fb3fb))
+* **HITL:** Let ADK resume after HITL approval is present ([9611f89](https://github.com/google/adk-java/commit/9611f8967e528c6242e17ad3ad5419e0b25fb3fb))
+* Improving LoggingPlugin ([acfaa04](https://github.com/google/adk-java/commit/acfaa04284dec12fa7245caee11cd7a3d8e4342c))
+* Integrate event compaction in Java ADK runner ([54c826c](https://github.com/google/adk-java/commit/54c826c80c2bfe09056396c2a21f8241f9d2898b))
+* Introduce TailRetentionEventCompactor to compact and retain the tail of the event stream ([efe58d6](https://github.com/google/adk-java/commit/efe58d6e0e5e0ff35d39e56bcb0f57cc6ccc7ccc))
+* Introduce the `App` class for defining agentic applications ([d7c5c6f](https://github.com/google/adk-java/commit/d7c5c6f4bdc2c2b06448af72bc311abf36b8e726))
+* introduces context caching configuration for apps, ported from Python ADK ([12defee](https://github.com/google/adk-java/commit/12defeedbaf6048bc83d484f421131051b7e81a5))
+* new ContextFilterPlugin ([f8e9bc3](https://github.com/google/adk-java/commit/f8e9bc30350082f048cb0ded6226f27f80655602))
+* Refactor EventsCompactionConfig to require a summarizer ([864d606](https://github.com/google/adk-java/commit/864d6066eb98af6567592055f7cd24cb78defaf3))
+* refactor remote A2A agent to use A2A SDK client ([7792233](https://github.com/google/adk-java/commit/7792233832e95dfe1ae93b04d91bd7507c37cc8d))
+* Refine bug and feature request issue templates ([3e74c9a](https://github.com/google/adk-java/commit/3e74c9a960cba6582e914d36925516039d57913c))
+* register GoogleMapsTool in ComponentRegistry ([464f0b2](https://github.com/google/adk-java/commit/464f0b2fc0231dbe161b0b5fe524687bb304cd49))
+* Reorder compaction events in chronological order ([66e2296](https://github.com/google/adk-java/commit/66e22964e67d0756e3351dae93e18aa5ae73f22e))
+* Setting up data structures for pause/resume/rewind ([c6c52c4](https://github.com/google/adk-java/commit/c6c52c43439468eb87fc6a029fa25a46a35dd6e7))
+* Skip post-invocation compaction if parameters not set ([76f86c5](https://github.com/google/adk-java/commit/76f86c54eb1a242e604f7b43e3ee18940168b6ec))
+* Support function calls in LLM event summarizer ([55144ac](https://github.com/google/adk-java/commit/55144aca3c1d77e06cf7101cf2504311c0585ed1))
+* support stdio_connection_params in McpToolset config ([cc1588a](https://github.com/google/adk-java/commit/cc1588a3e669dc670595ecbdebb12dc9d2ae40f0))
+* Token count estimation fallback for tail retention compaction ([3338565](https://github.com/google/adk-java/commit/3338565cff976fdad1eda1fccafef58c9d4a51ba))
+* Update event compaction logic to include events after compaction end times ([ea12505](https://github.com/google/adk-java/commit/ea12505d7c4e22a237db5a8d3f78564ace0b216b))
+* Updating Baseline Code executors ([a3f1763](https://github.com/google/adk-java/commit/a3f176322c47354d5c18d8371cb38bd2dd719904))
+* updating Telemetry ([5ba63f4](https://github.com/google/adk-java/commit/5ba63f4015d369bc58ad7dfe76198acf003e7450))
+* Updating the Tracing implementation and updating BaseAgent.runLive ([8acb1ea](https://github.com/google/adk-java/commit/8acb1eafb099723dfae065d8b9339bb5180aa26f))
+* use Credentials' request metadata to populate headers ([e01df11](https://github.com/google/adk-java/commit/e01df116e311016df92e69487c0a6607b00384bc))
-### Code Refactoring
-* Use RxJava for VertexAiClient ([391e049](https://github.com/redbus-labs/adk-java/commit/391e0493317c2d875400e751c5043eec3d4ef031))
+### Bug Fixes
+
+* Add name and description to configagent pom.xml ([4948bfc](https://github.com/google/adk-java/commit/4948bfc9a35ea22660f37a6afc3474fab220b630))
+* Align InMemorySessionService listSessions with Python implementation ([9434949](https://github.com/google/adk-java/commit/94349499d03f3a131af4464def4b208db52a8feb))
+* Always use a mutable HashMap for default function arguments ([c6c9557](https://github.com/google/adk-java/commit/c6c9557ff28feece54265fcff82478156afbe67f))
+* emit multiple LlmResponses in GeminiLlmConnection ([7bf55f1](https://github.com/google/adk-java/commit/7bf55f1be6381ae5319bb0532f32c0287461546d))
+* Events for HITL are now emitted correctly ([9611f89](https://github.com/google/adk-java/commit/9611f8967e528c6242e17ad3ad5419e0b25fb3fb))
+* fix linter error ([f49260e](https://github.com/google/adk-java/commit/f49260e05c5d36b85066caf299fda9346b6ff788))
+* Fixing a problem with serializing sessions that broke integration with Vertex AI Session Service ([8190ed3](https://github.com/google/adk-java/commit/8190ed3d78667875ee0772e52b7075dcdaa14963))
+* Fixing a regression in InMemorySessionService ([d11bedf](https://github.com/google/adk-java/commit/d11bedf42976242d1c3dd6b99ebae0babe59535c))
+* Fixing Vertex session storage ([5607f64](https://github.com/google/adk-java/commit/5607f644c95a053bf381c2021879e6f31d5c6bde))
+* HITL endless loop when asking for approvals ([9611f89](https://github.com/google/adk-java/commit/9611f8967e528c6242e17ad3ad5419e0b25fb3fb))
+* include usage_metadata events in live postprocessing ([8137d66](https://github.com/google/adk-java/commit/8137d661d7b29eab066c23b7f302068f82423eb7))
+* javadocs in ResponseConverter ([be35b22](https://github.com/google/adk-java/commit/be35b2277e8291336013623cb9f0c86f62ed1f43))
+* Make FunctionResponses respect the order of FunctionCalls ([a99c75b](https://github.com/google/adk-java/commit/a99c75bf79d86866db26135568bf36b685886659))
+* Making stepsCompleted thread-safe ([d432c64](https://github.com/google/adk-java/commit/d432c6414128cf83eb0211eb18ef058dbbcd1807))
+* Merging of events in rearrangeEventsForAsyncFunctionResponsesInHistory ([67c29e3](https://github.com/google/adk-java/commit/67c29e3a33bda22d8a18a17c99e5abc891bf19f8))
+* Mutate EventActions in-place in AgentTool ([ded5a4e](https://github.com/google/adk-java/commit/ded5a4e760055d3d2bcd74d3bd8f21517821e7d0))
+* pass mutable function args map to beforeToolCallback ([e989ae1](https://github.com/google/adk-java/commit/e989ae1337a84fd6686504050d2a3bf2db15c32c))
+* populate finishReason in LlmResponse ([dace210](https://github.com/google/adk-java/commit/dace2106cd2451d8271c842da13daff65de0922e))
+* Propagate trace context across async boundaries ([279c977](https://github.com/google/adk-java/commit/279c977d9eefda39159dd4bd86acea03a47c6101))
+* recursively extract input/output schema for AgentTool ([7019d39](https://github.com/google/adk-java/commit/7019d39e490cef1b4b443d1755547a3a701bc964))
+* Reduce the logging level ([dd601ca](https://github.com/google/adk-java/commit/dd601ca8ed939d42fa186113bf0dca31c6e4a6db))
+* Remove checking ToolConfirmation from Functions to align with Python SDK ([0724330](https://github.com/google/adk-java/commit/0724330c66d26b2e80e458663ca88bb333c40c2c))
+* remove client-side function call IDs from LlmRequest ([99b5fc2](https://github.com/google/adk-java/commit/99b5fc26d791175e4dad2c818191c8c31e4269f6))
+* Remove obsolete [@param](https://github.com/param) tags from SessionController Javadoc ([a77971a](https://github.com/google/adk-java/commit/a77971a9ac983acbceab15db7eeb36460a0ba759))
+* Replace [@api](https://github.com/api)Note with <p> in Javadoc comments. ([ac16d53](https://github.com/google/adk-java/commit/ac16d53db0d7b0d2a3aa3a12c1db1f819d7c6c21))
+* restore invocationContext() method ([c9e2a5b](https://github.com/google/adk-java/commit/c9e2a5b37b31f5fa0e0a193076f7dc836320de97))
+* revert: Merging of events in rearrangeEventsForAsyncFunctionResponsesInHistory ([101adce](https://github.com/google/adk-java/commit/101adce314dd65328af6ad9281afb46f9b160c1a))
+* update converters package classes ([b66e4a5](https://github.com/google/adk-java/commit/b66e4a5280688a9533ed314103a0b290191a51cf))
+* update EmbeddingModelDiscoveryTest package statement ([adeb9dc](https://github.com/google/adk-java/commit/adeb9dca945004334f4af6a6442e41dd856d1612))
+* Updated BasePlugin JavaDoc for name parameter ([2e59550](https://github.com/google/adk-java/commit/2e59550eff9ad50e81c310ba83b9d49af6bb8987))
+
+
+### Documentation
+
+* Update comment in Runner ([fe00ef8](https://github.com/google/adk-java/commit/fe00ef87f9c7cdf3d1005a411055b90cebdd0c98))
## [0.3.0](https://github.com/google/adk-java/compare/v0.2.0...v0.3.0) (2025-09-17)
diff --git a/README.md b/README.md
index df8c871ee..4a5dab81f 100644
--- a/README.md
+++ b/README.md
@@ -1,97 +1,3 @@
-
-# Capability Supported
-
-Of course. Here is the table with the 4th column for "Bedrock API" added.
-
-| Feature | Gemini | Anthropic | AWS Bedrock API | Ollama | Azure OAI (redBus) | Bedrock+Anthropic |
-| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
-| **Chat** | β | β | β | β | β | β |
-| **Tools/Function** | β | β | β | β | β | β |
-| **Chat Stream** | β | β | β | β | β | β |
-| **Image (Input)** | β (Multimodal models) | β | β (Via models like Claude 3) | β | β | β (Claude 3 models) |
-| **Image Gen (Output)** | β | β | β (Via Titan, Stable Diffusion) | β | β | β (Via other models like Titan Image Generator) |
-| **Audio Streaming (Input)** | β (Some APIs/integrations) | β | β (Via Amazon Transcribe) | β | β |β (Via services like Amazon Transcribe) |
-| **Transcription** | β (Some APIs/integrations) | β | β (Via Amazon Transcribe) | β | β | β (Via Amazon Transcribe) |
-| **Persistent session (MapDB)** | β | β | β | β | β | β |
-| **Agents as Tool/Function** | β | β | β | β | β | β |
-| **Interoperability (A2A)** | β | β | β | β | β | β |
-| **Interoperability (Tools/Functions)** | β | β | β | β | β | β |
-| **Interoperability (Agents as Tool/Function)** | β | β | β | β | β | β |
-| **Agent Workflow** | β | β | β | β | β | β |
-| **Parallel Agents** | β | β | β | β | β | β |
-| **Sequential Agents** | β | β | β | β | β | β |
-| **Agent Orchestration** | β | β | β | β | β | β |
-| **Hierarchical Task Decomposition** | β | β | β | β | β | β |
-
-
-# Core Differences
-
-## Persistent session storage added,
-
-| Store | Chat | Stream | Artifact |
-| :--- | :--- | :--- | :--- |
-| **MapDB** | β | β | β |
-| **MongoDB** | β | β | β |
-| **Postgres** | β | β | β |
-
-### MapDbSessionService("map.db")
-
-```
- public BaseSessionService sessionService() {
-
- try {
- // TODO: Add logic to select service based on config (e.g., DB URL)
- log.info("Using MapDbSessionService");
- return new MapDbSessionService("map.db");
- } catch (Exception ex) {
- java.util.logging.Logger.getLogger(AdkWebServer.class.getName()).log(Level.SEVERE, null, ex);
- }
-
- // TODO: Add logic to select service based on config (e.g., DB URL)
- log.info("Using InMemorySessionService");
- return new InMemorySessionService();
- }
-```
-
-## Ollama API Supported,
-
-### OllamaBaseLM("qwen3:0.6b")
-```
- LlmAgent coordinator = LlmAgent.builder()
- .name("Coordinator")
- . model(new com.google.adk.models.OllamaBaseLM("qwen3:0.6b"))//
- .instruction("You are an assistant. Delegate requests to appropriate agent")
- .description("Main coordinator.")
- .build();
-```
-
-## Secondary Auth Over Azure API
-
-### RedbusADG("40")
-
-```
-LlmAgent.builder()
- .name(NAME)
- .model(new com.google.adk.models.OllamaBaseLM("qwen3:0.6b"))//.model(new RedbusADG("40"))
- .description("Agent to calculate trigonometric functions (sine, cosine, tangent) for given angles.") // Updated description
- .instruction(
- "You are a helpful agent who can calculate trigonometric functions (sine, cosine, and"
- + " tangent). Use the provided tools to perform these calculations."
- + " When the user provides an angle, identify the value and the unit (degrees or radians)."
- + " Call the appropriate tool based on the requested function (sin, cos, tan) and provide the angle value and unit."
- + " Ensure the angle unit is explicitly passed to the tool as 'degrees' or 'radians'.") // Updated instruction
- .tools(
- // Register the new trigonometry tools
- FunctionTool.create(TrigonometryAgent.class, "calculateSine"),
- FunctionTool.create(TrigonometryAgent.class, "calculateCosine"),
- FunctionTool.create(TrigonometryAgent.class, "calculateTangent")
- // Removed FunctionTool.create for getCurrentTime and getWeather
- )
- .build();
-```
-
-
-
# Agent Development Kit (ADK) for Java
[](LICENSE)
@@ -144,13 +50,13 @@ If you're using Maven, add the following to your dependencies:
com.google.adkgoogle-adk
- 1.2.0
+ 0.8.0com.google.adkgoogle-adk-dev
- 1.2.0
+ 0.8.0
```
diff --git a/a2a/pom.xml b/a2a/pom.xml
index c05ca3009..9e498250a 100644
--- a/a2a/pom.xml
+++ b/a2a/pom.xml
@@ -5,7 +5,7 @@
com.google.adkgoogle-adk-parent
- 1.2.0
+ 0.8.1-SNAPSHOTgoogle-adk-a2a
@@ -19,17 +19,22 @@
0.3.2.Final${project.version}33.0.0-jre
- 2.19.03.1.5
+ 2.19.01.0.0
- 2.0.172.38.0
+ 1.76.2
+ 2.0.171.4.44.13.2
- 1.62.2
+
+ com.google.adk
+ google-adk
+ ${google.adk.version}
+ io.grpcgrpc-netty-shaded
@@ -51,11 +56,6 @@
gson2.10.1
-
- com.google.adk
- google-adk
- ${google.adk.version}
- com.google.guavaguava
@@ -76,6 +76,11 @@
jackson-databind${jackson.version}
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ ${jackson.version}
+ com.fasterxml.jackson.modulejackson-module-parameter-names
@@ -129,13 +134,13 @@
test
- org.junit.jupiter
- junit-jupiter-api
+ org.mockito
+ mockito-coretest
- org.mockito
- mockito-core
+ org.junit.jupiter
+ junit-jupiter-apitest
@@ -149,7 +154,6 @@
test
-
@@ -159,6 +163,14 @@
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ ${java.version}
+
+ org.apache.maven.pluginsmaven-surefire-plugin
@@ -187,7 +199,7 @@
grpc-java
- io.grpc:protoc-gen-grpc-java:1.48.1:exe:${os.detected.classifier}
+ io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
diff --git a/a2a/src/main/java/com/google/adk/a2a/RemoteA2AAgent.java b/a2a/src/main/java/com/google/adk/a2a/RemoteA2AAgent.java
index 5e6e341d7..b391f2985 100644
--- a/a2a/src/main/java/com/google/adk/a2a/RemoteA2AAgent.java
+++ b/a2a/src/main/java/com/google/adk/a2a/RemoteA2AAgent.java
@@ -2,7 +2,11 @@
import static com.google.common.base.Strings.nullToEmpty;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.google.adk.a2a.common.A2AClientError;
+import com.google.adk.a2a.common.A2AMetadata;
import com.google.adk.a2a.converters.EventConverter;
import com.google.adk.a2a.converters.ResponseConverter;
import com.google.adk.agents.BaseAgent;
@@ -11,21 +15,31 @@
import com.google.adk.events.Event;
import com.google.common.collect.ImmutableList;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import com.google.genai.types.Content;
+import com.google.genai.types.CustomMetadata;
+import com.google.genai.types.Part;
import io.a2a.client.Client;
import io.a2a.client.ClientEvent;
+import io.a2a.client.MessageEvent;
import io.a2a.client.TaskEvent;
import io.a2a.client.TaskUpdateEvent;
import io.a2a.spec.A2AClientException;
import io.a2a.spec.AgentCard;
import io.a2a.spec.Message;
+import io.a2a.spec.TaskArtifactUpdateEvent;
import io.a2a.spec.TaskState;
+import io.a2a.spec.TaskStatusUpdateEvent;
import io.reactivex.rxjava3.core.BackpressureStrategy;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.FlowableEmitter;
+import java.time.Instant;
+import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
import java.util.Optional;
-import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.UUID;
import java.util.function.BiConsumer;
+import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -54,6 +68,8 @@
public class RemoteA2AAgent extends BaseAgent {
private static final Logger logger = LoggerFactory.getLogger(RemoteA2AAgent.class);
+ private static final ObjectMapper objectMapper =
+ new ObjectMapper().registerModule(new JavaTimeModule());
private final AgentCard agentCard;
private final Client a2aClient;
@@ -173,61 +189,303 @@ protected Flowable runAsyncImpl(InvocationContext invocationContext) {
}
Message originalMessage = a2aMessageOpt.get();
+ String requestJson = serializeMessageToJson(originalMessage);
return Flowable.create(
emitter -> {
- FlowableEmitter flowableEmitter = emitter.serialize();
- AtomicBoolean done = new AtomicBoolean(false);
+ StreamHandler handler =
+ new StreamHandler(
+ emitter.serialize(), invocationContext, requestJson, streaming, name());
ImmutableList> consumers =
- ImmutableList.of(
- (event, unused) ->
- handleClientEvent(event, flowableEmitter, invocationContext, done));
- a2aClient.sendMessage(
- originalMessage, consumers, e -> handleClientError(e, flowableEmitter, done), null);
+ ImmutableList.of(handler::handleEvent);
+ a2aClient.sendMessage(originalMessage, consumers, handler::handleError, null);
},
BackpressureStrategy.BUFFER);
}
- private void handleClientError(Throwable e, FlowableEmitter emitter, AtomicBoolean done) {
- // Mark the flow as done if it is already cancelled.
- done.compareAndSet(false, emitter.isCancelled());
+ private @Nullable String serializeMessageToJson(Message message) {
+ try {
+ return objectMapper.writeValueAsString(message);
+ } catch (JsonProcessingException e) {
+ logger.warn("Failed to serialize request", e);
+ return null;
+ }
+ }
- // If the flow is already done, stop processing and exit the consumer.
- if (done.get()) {
- return;
+ private static class StreamHandler {
+ private final FlowableEmitter emitter;
+ private final InvocationContext invocationContext;
+ private final String requestJson;
+ private final boolean streaming;
+ private final String agentName;
+ private boolean done = false;
+ private final StringBuilder textBuffer = new StringBuilder();
+ private final StringBuilder thoughtsBuffer = new StringBuilder();
+
+ StreamHandler(
+ FlowableEmitter emitter,
+ InvocationContext invocationContext,
+ String requestJson,
+ boolean streaming,
+ String agentName) {
+ this.emitter = emitter;
+ this.invocationContext = invocationContext;
+ this.requestJson = requestJson;
+ this.streaming = streaming;
+ this.agentName = agentName;
}
- // If the error is raised, complete the flow with an error.
- if (!done.getAndSet(true)) {
+
+ synchronized void handleError(Throwable e) {
+ // Mark the flow as done if it is already cancelled.
+ if (!done) {
+ done = emitter.isCancelled();
+ }
+
+ // If the flow is already done, stop processing.
+ if (done) {
+ return;
+ }
+ // If the error is raised, complete the flow with an error.
+ done = true;
emitter.tryOnError(new A2AClientError("Failed to communicate with the remote agent", e));
}
- }
- private void handleClientEvent(
- ClientEvent clientEvent,
- FlowableEmitter emitter,
- InvocationContext invocationContext,
- AtomicBoolean done) {
- // Mark the flow as done if it is already cancelled.
- done.compareAndSet(false, emitter.isCancelled());
-
- // If the flow is already done, stop processing and exit the consumer.
- if (done.get()) {
- return;
+ // TODO: b/483038527 - The synchronized block might block the thread, we should optimize for
+ // performance in the future.
+ synchronized void handleEvent(ClientEvent clientEvent, AgentCard unused) {
+ // Mark the flow as done if it is already cancelled.
+ if (!done) {
+ done = emitter.isCancelled();
+ }
+
+ // If the flow is already done, stop processing.
+ if (done) {
+ return;
+ }
+
+ Optional eventOpt =
+ ResponseConverter.clientEventToEvent(clientEvent, invocationContext);
+ eventOpt.ifPresent(
+ event -> {
+ addMetadata(event, clientEvent);
+
+ if (isCompleted(clientEvent)) {
+ // Terminal event, check if we can merge.
+ boolean mergeResult = mergeAggregatedContentIntoEvent(event);
+ if (!mergeResult) {
+ emitAggregatedEventAndClearBuffer(null);
+ }
+ } else {
+ boolean isPartial = event.partial().orElse(false);
+ if (isPartial) {
+ if (shouldResetBuffer(clientEvent)) {
+ clearBuffer();
+ }
+ boolean addedToBuffer = bufferContent(event, clientEvent);
+ if (!addedToBuffer) {
+ // Partial event with no content to buffer (e.g. tool call).
+ // Flush buffer before emitting this event.
+ emitAggregatedEventAndClearBuffer(null);
+ }
+ } else {
+ // Intermediate non-partial.
+ emitAggregatedEventAndClearBuffer(null);
+ }
+ }
+ emitter.onNext(event);
+ });
+
+ // For non-streaming communication, complete the flow; for streaming, wait until the client
+ // marks the completion.
+ if (isCompleted(clientEvent) || !streaming) {
+ // Only complete the flow once.
+ if (!done) {
+ emitAggregatedEventAndClearBuffer(clientEvent);
+ done = true;
+ emitter.onComplete();
+ }
+ }
+ }
+
+ private void addMetadata(Event event, ClientEvent clientEvent) {
+ ImmutableList.Builder eventMetadataBuilder = ImmutableList.builder();
+ event.customMetadata().ifPresent(eventMetadataBuilder::addAll);
+ if (requestJson != null) {
+ eventMetadataBuilder.add(
+ CustomMetadata.builder()
+ .key(A2AMetadata.Key.REQUEST.getValue())
+ .stringValue(requestJson)
+ .build());
+ }
+ try {
+ if (clientEvent != null) {
+ eventMetadataBuilder.add(
+ CustomMetadata.builder()
+ .key(A2AMetadata.Key.RESPONSE.getValue())
+ .stringValue(objectMapper.writeValueAsString(clientEvent))
+ .build());
+ }
+ } catch (JsonProcessingException e) {
+ // metadata serialization is not critical for agent execution, so we just log and continue.
+ logger.warn("Failed to serialize response metadata", e);
+ }
+ event.setCustomMetadata(eventMetadataBuilder.build());
+ }
+
+ /**
+ * Buffers the content from the event into the text and thoughts buffers.
+ *
+ * @return true if the event has content that was added to the buffer, false otherwise.
+ */
+ private boolean bufferContent(Event event, ClientEvent clientEvent) {
+ if (!shouldBuffer(clientEvent)) {
+ return false;
+ }
+
+ boolean updated = false;
+ for (Part part : eventParts(event)) {
+ if (part.text().isPresent()) {
+ String t = part.text().get();
+ if (part.thought().orElse(false)) {
+ thoughtsBuffer.append(t);
+ updated = true;
+ } else {
+ textBuffer.append(t);
+ updated = true;
+ }
+ }
+ }
+ return updated;
+ }
+
+ /**
+ * Determines if the event should be buffered.
+ *
+ *
Buffering is used to aggregate content from partial events. We buffer events that can
+ * contain content which is streamed in chunks, like {@link MessageEvent} or {@link
+ * TaskArtifactUpdateEvent}. Events that do not contain content to be aggregated, like {@link
+ * TaskStatusUpdateEvent} or {@link TaskEvent} without artifacts, should not be buffered.
+ */
+ private boolean shouldBuffer(ClientEvent event) {
+ if (event instanceof TaskUpdateEvent taskUpdateEvent) {
+ Object innerEvent = taskUpdateEvent.getUpdateEvent();
+ return !(innerEvent instanceof TaskStatusUpdateEvent);
+ }
+ if (event instanceof TaskEvent taskEvent) {
+ return !taskEvent.getTask().getArtifacts().isEmpty();
+ }
+ return true;
+ }
+
+ /**
+ * Determines if text buffers should be reset before processing new content.
+ *
+ *
When receiving artifact updates via {@link TaskArtifactUpdateEvent}, if {@code append} is
+ * false, it indicates the new content should replace any prior chunks. If this is not the
+ * {@code last_chunk}, it means we are at the beginning of receiving a new set of chunks, so we
+ * need to reset buffers to avoid appending to stale content from a prior update.
+ */
+ private boolean shouldResetBuffer(ClientEvent event) {
+ if (event instanceof TaskUpdateEvent taskUpdateEvent) {
+ Object innerEvent = taskUpdateEvent.getUpdateEvent();
+ if (innerEvent instanceof TaskArtifactUpdateEvent artifactEvent) {
+ return Objects.equals(artifactEvent.isAppend(), false)
+ && Objects.equals(artifactEvent.isLastChunk(), false);
+ }
+ }
+ return false;
}
- Optional event = ResponseConverter.clientEventToEvent(clientEvent, invocationContext);
- if (event.isPresent()) {
- emitter.onNext(event.get());
+ private void clearBuffer() {
+ thoughtsBuffer.setLength(0);
+ textBuffer.setLength(0);
}
- // For non-streaming communication, complete the flow; for streaming, wait until the client
- // marks the completion.
- if (isCompleted(clientEvent) || !streaming) {
- // Only complete the flow once.
- if (!done.getAndSet(true)) {
- emitter.onComplete();
+ private void emitAggregatedEventAndClearBuffer(@Nullable ClientEvent triggerEvent) {
+ if (thoughtsBuffer.length() > 0 || textBuffer.length() > 0) {
+ List parts = new ArrayList<>();
+ if (thoughtsBuffer.length() > 0) {
+ parts.add(Part.builder().thought(true).text(thoughtsBuffer.toString()).build());
+ }
+ if (textBuffer.length() > 0) {
+ parts.add(Part.builder().text(textBuffer.toString()).build());
+ }
+ Content aggregatedContent = Content.builder().role("model").parts(parts).build();
+ emitter.onNext(createAggregatedEvent(aggregatedContent, triggerEvent));
+ clearBuffer();
}
}
+
+ private boolean mergeAggregatedContentIntoEvent(Event event) {
+ if (thoughtsBuffer.isEmpty() && textBuffer.isEmpty()) {
+ return false;
+ }
+ boolean hasContent =
+ event.content().isPresent()
+ && !event.content().get().parts().orElse(ImmutableList.of()).isEmpty();
+ if (hasContent) {
+ return false;
+ }
+
+ List parts = new ArrayList<>();
+ if (thoughtsBuffer.length() > 0) {
+ parts.add(Part.builder().thought(true).text(thoughtsBuffer.toString()).build());
+ }
+ if (textBuffer.length() > 0) {
+ parts.add(Part.builder().text(textBuffer.toString()).build());
+ }
+ Content aggregatedContent = Content.builder().role("model").parts(parts).build();
+
+ event.setContent(aggregatedContent);
+
+ ImmutableList.Builder newMetadata = ImmutableList.builder();
+ event.customMetadata().ifPresent(newMetadata::addAll);
+ newMetadata.add(
+ CustomMetadata.builder()
+ .key(A2AMetadata.Key.AGGREGATED.getValue())
+ .stringValue("true")
+ .build());
+ event.setCustomMetadata(newMetadata.build());
+
+ clearBuffer();
+ return true;
+ }
+
+ private Event createAggregatedEvent(Content content, @Nullable ClientEvent triggerEvent) {
+ ImmutableList.Builder aggMetadataBuilder = ImmutableList.builder();
+ aggMetadataBuilder.add(
+ CustomMetadata.builder()
+ .key(A2AMetadata.Key.AGGREGATED.getValue())
+ .stringValue("true")
+ .build());
+ if (requestJson != null) {
+ aggMetadataBuilder.add(
+ CustomMetadata.builder()
+ .key(A2AMetadata.Key.REQUEST.getValue())
+ .stringValue(requestJson)
+ .build());
+ }
+ if (triggerEvent != null) {
+ try {
+ aggMetadataBuilder.add(
+ CustomMetadata.builder()
+ .key(A2AMetadata.Key.RESPONSE.getValue())
+ .stringValue(objectMapper.writeValueAsString(triggerEvent))
+ .build());
+ } catch (JsonProcessingException e) {
+ logger.warn("Failed to serialize response metadata for aggregated event", e);
+ }
+ }
+
+ return Event.builder()
+ .id(UUID.randomUUID().toString())
+ .invocationId(invocationContext.invocationId())
+ .author(agentName)
+ .content(content)
+ .timestamp(Instant.now().toEpochMilli())
+ .customMetadata(aggMetadataBuilder.build())
+ .build();
+ }
}
private static boolean isCompleted(ClientEvent event) {
@@ -240,6 +498,10 @@ private static boolean isCompleted(ClientEvent event) {
return executionState.equals(TaskState.COMPLETED);
}
+ private static ImmutableList eventParts(Event event) {
+ return ImmutableList.copyOf(event.content().flatMap(Content::parts).orElse(ImmutableList.of()));
+ }
+
@Override
protected Flowable runLiveImpl(InvocationContext invocationContext) {
throw new UnsupportedOperationException(
diff --git a/a2a/src/main/java/com/google/adk/a2a/common/A2AMetadata.java b/a2a/src/main/java/com/google/adk/a2a/common/A2AMetadata.java
new file mode 100644
index 000000000..5c75faeac
--- /dev/null
+++ b/a2a/src/main/java/com/google/adk/a2a/common/A2AMetadata.java
@@ -0,0 +1,24 @@
+package com.google.adk.a2a.common;
+
+/** Constants and utilities for A2A metadata keys. */
+public final class A2AMetadata {
+
+ /** Enum for A2A custom metadata keys. */
+ public enum Key {
+ REQUEST("a2a:request"),
+ RESPONSE("a2a:response"),
+ AGGREGATED("a2a:aggregated");
+
+ private final String value;
+
+ Key(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+ }
+
+ private A2AMetadata() {}
+}
diff --git a/a2a/src/main/java/com/google/adk/a2a/common/GenAiFieldMissingException.java b/a2a/src/main/java/com/google/adk/a2a/common/GenAiFieldMissingException.java
new file mode 100644
index 000000000..a5947dcb8
--- /dev/null
+++ b/a2a/src/main/java/com/google/adk/a2a/common/GenAiFieldMissingException.java
@@ -0,0 +1,12 @@
+package com.google.adk.a2a.common;
+
+/** Exception thrown when the the genai class has an empty field. */
+public class GenAiFieldMissingException extends RuntimeException {
+ public GenAiFieldMissingException(String message) {
+ super(message);
+ }
+
+ public GenAiFieldMissingException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/A2ADataPartMetadataType.java b/a2a/src/main/java/com/google/adk/a2a/converters/A2ADataPartMetadataType.java
new file mode 100644
index 000000000..b5b53c49a
--- /dev/null
+++ b/a2a/src/main/java/com/google/adk/a2a/converters/A2ADataPartMetadataType.java
@@ -0,0 +1,19 @@
+package com.google.adk.a2a.converters;
+
+/** Enum for the type of A2A DataPart metadata. */
+public enum A2ADataPartMetadataType {
+ FUNCTION_RESPONSE("function_response"),
+ FUNCTION_CALL("function_call"),
+ CODE_EXECUTION_RESULT("code_execution_result"),
+ EXECUTABLE_CODE("executable_code");
+
+ private final String type;
+
+ private A2ADataPartMetadataType(String type) {
+ this.type = type;
+ }
+
+ public String getType() {
+ return type;
+ }
+}
diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/EventConverter.java b/a2a/src/main/java/com/google/adk/a2a/converters/EventConverter.java
index f5b1178c0..1a49b0070 100644
--- a/a2a/src/main/java/com/google/adk/a2a/converters/EventConverter.java
+++ b/a2a/src/main/java/com/google/adk/a2a/converters/EventConverter.java
@@ -3,14 +3,11 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.adk.agents.InvocationContext;
-import com.google.adk.events.Event;
import com.google.common.collect.ImmutableList;
import com.google.genai.types.Content;
-import com.google.genai.types.Part;
import io.a2a.spec.Message;
-import io.a2a.spec.TextPart;
-import java.util.ArrayList;
-import java.util.List;
+import io.a2a.spec.Part;
+import java.util.Collection;
import java.util.Optional;
import java.util.UUID;
import org.slf4j.Logger;
@@ -28,47 +25,35 @@ public final class EventConverter {
private EventConverter() {}
/**
- * Aggregation mode for converting events to A2A messages.
+ * Converts an ADK InvocationContext to an A2A Message.
*
- *
AS_IS: Parts are aggregated as-is.
+ *
It combines all the events in the session, plus the user content, converted into A2A Parts,
+ * into a single A2A Message.
*
- *
EXTERNAL_HANDOFF: Parts are aggregated as-is, except for function responses, which are
- * converted to text parts with the function name and response map.
+ *
If the context has no events, or no suitable content to build the message, an empty optional
+ * is returned.
+ *
+ * @param context The ADK InvocationContext to convert.
+ * @return The converted A2A Message.
*/
- public enum AggregationMode {
- AS_IS,
- EXTERNAL_HANDOFF
- }
-
- public static ImmutableList> contentToParts(Optional content) {
- if (content.isPresent() && content.get().parts().isPresent()) {
- return content.get().parts().get().stream()
- .map(PartConverter::fromGenaiPart)
- .flatMap(Optional::stream)
- .collect(toImmutableList());
- }
- return ImmutableList.of();
- }
-
public static Optional convertEventsToA2AMessage(InvocationContext context) {
- return convertEventsToA2AMessage(context, AggregationMode.AS_IS);
- }
-
- public static Optional convertEventsToA2AMessage(
- InvocationContext context, AggregationMode mode) {
if (context.session().events().isEmpty()) {
logger.warn("No events in session, cannot convert to A2A message.");
return Optional.empty();
}
- List> parts = new ArrayList<>();
- for (Event event : context.session().events()) {
- appendContentParts(event.content(), mode, parts);
- }
+ ImmutableList.Builder> partsBuilder = ImmutableList.builder();
context
- .userContent()
- .ifPresent(content -> appendContentParts(Optional.of(content), mode, parts));
+ .session()
+ .events()
+ .forEach(
+ event ->
+ partsBuilder.addAll(
+ contentToParts(event.content(), event.partial().orElse(false))));
+ partsBuilder.addAll(contentToParts(context.userContent(), false));
+
+ ImmutableList> parts = partsBuilder.build();
if (parts.isEmpty()) {
logger.warn("No suitable content found to build A2A request message.");
@@ -83,37 +68,11 @@ public static Optional convertEventsToA2AMessage(
.build());
}
- private static void appendContentParts(
- Optional contentOpt, AggregationMode mode, List> target) {
- if (contentOpt.isEmpty() || contentOpt.get().parts().isEmpty()) {
- return;
- }
-
- for (Part part : contentOpt.get().parts().get()) {
- if (part.text().isPresent()) {
- target.add(new TextPart(part.text().get()));
- continue;
- }
-
- if (part.functionCall().isPresent()) {
- if (mode == AggregationMode.AS_IS) {
- PartConverter.convertGenaiPartToA2aPart(part).ifPresent(target::add);
- }
- continue;
- }
-
- if (part.functionResponse().isPresent()) {
- if (mode == AggregationMode.AS_IS) {
- PartConverter.convertGenaiPartToA2aPart(part).ifPresent(target::add);
- } else {
- String name = part.functionResponse().get().name().orElse("");
- String mapStr = String.valueOf(part.functionResponse().get().response().orElse(null));
- target.add(new TextPart(String.format("%s response: %s", name, mapStr)));
- }
- continue;
- }
-
- PartConverter.fromGenaiPart(part).ifPresent(target::add);
- }
+ public static ImmutableList> contentToParts(
+ Optional content, boolean isPartial) {
+ return content.flatMap(Content::parts).stream()
+ .flatMap(Collection::stream)
+ .map(part -> PartConverter.fromGenaiPart(part, isPartial))
+ .collect(toImmutableList());
}
}
diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java b/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java
index c6ef06400..05125d170 100644
--- a/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java
+++ b/a2a/src/main/java/com/google/adk/a2a/converters/PartConverter.java
@@ -4,13 +4,18 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.adk.a2a.common.GenAiFieldMissingException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.genai.types.Blob;
+import com.google.genai.types.CodeExecutionResult;
import com.google.genai.types.Content;
+import com.google.genai.types.ExecutableCode;
import com.google.genai.types.FileData;
import com.google.genai.types.FunctionCall;
import com.google.genai.types.FunctionResponse;
+import com.google.genai.types.Language;
+import com.google.genai.types.Outcome;
import com.google.genai.types.Part;
import io.a2a.spec.DataPart;
import io.a2a.spec.FileContent;
@@ -34,18 +39,33 @@
* use in production code.
*/
public final class PartConverter {
+
private static final Logger logger = LoggerFactory.getLogger(PartConverter.class);
private static final ObjectMapper objectMapper = new ObjectMapper();
-
// Constants for metadata types. By convention metadata keys are prefixed with "adk_" to align
// with the Python and Golang libraries.
public static final String A2A_DATA_PART_METADATA_TYPE_KEY = "adk_type";
public static final String A2A_DATA_PART_METADATA_IS_LONG_RUNNING_KEY = "adk_is_long_running";
- public static final String A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL = "function_call";
- public static final String A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE = "function_response";
- public static final String A2A_DATA_PART_METADATA_TYPE_CODE_EXECUTION_RESULT =
- "code_execution_result";
- public static final String A2A_DATA_PART_METADATA_TYPE_EXECUTABLE_CODE = "executable_code";
+ public static final String A2A_DATA_PART_METADATA_IS_PARTIAL_KEY = "adk_partial";
+ public static final String LANGUAGE_KEY = "language";
+ public static final String OUTCOME_KEY = "outcome";
+ public static final String CODE_KEY = "code";
+ public static final String OUTPUT_KEY = "output";
+ public static final String NAME_KEY = "name";
+ public static final String ARGS_KEY = "args";
+ public static final String RESPONSE_KEY = "response";
+ public static final String ID_KEY = "id";
+ public static final String WILL_CONTINUE_KEY = "willContinue";
+ public static final String PARTIAL_ARGS_KEY = "partialArgs";
+ public static final String SCHEDULING_KEY = "scheduling";
+ public static final String PARTS_KEY = "parts";
+
+ public static Optional toTextPart(io.a2a.spec.Part> part) {
+ if (part instanceof TextPart textPart) {
+ return Optional.of(textPart);
+ }
+ return Optional.empty();
+ }
/** Convert an A2A JSON part into a Google GenAI part representation. */
public static Optional toGenaiPart(io.a2a.spec.Part> a2aPart) {
@@ -77,30 +97,6 @@ public static ImmutableList toGenaiParts(
.collect(toImmutableList());
}
- /**
- * Convert a Google GenAI Part to an A2A Part.
- *
- * @param part The GenAI part to convert.
- * @return Optional containing the converted A2A Part, or empty if conversion fails.
- */
- public static Optional convertGenaiPartToA2aPart(Part part) {
- if (part == null) {
- return Optional.empty();
- }
-
- if (part.text().isPresent()) {
- // Text parts are handled directly in the Message content, not as DataPart
- return Optional.empty();
- } else if (part.functionCall().isPresent()) {
- return createDataPartFromFunctionCall(part.functionCall().get());
- } else if (part.functionResponse().isPresent()) {
- return createDataPartFromFunctionResponse(part.functionResponse().get());
- }
-
- logger.warn("Cannot convert unsupported part for Google GenAI part: " + part);
- return Optional.empty();
- }
-
private static Optional convertFilePartToGenAiPart(
FilePart filePart) {
FileContent fileContent = filePart.getFile();
@@ -146,11 +142,11 @@ private static Optional convertDataPartToGenAiPart(
String metadataType = metadata.getOrDefault(A2A_DATA_PART_METADATA_TYPE_KEY, "").toString();
- if ((data.containsKey("name") && data.containsKey("args"))
- || metadataType.equals(A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL)) {
- String functionName = String.valueOf(data.getOrDefault("name", ""));
- String functionId = String.valueOf(data.getOrDefault("id", ""));
- Map args = coerceToMap(data.get("args"));
+ if ((data.containsKey(NAME_KEY) && data.containsKey(ARGS_KEY))
+ || metadataType.equals(A2ADataPartMetadataType.FUNCTION_CALL.getType())) {
+ String functionName = String.valueOf(data.getOrDefault(NAME_KEY, null));
+ String functionId = String.valueOf(data.getOrDefault(ID_KEY, null));
+ Map args = coerceToMap(data.get(ARGS_KEY));
return Optional.of(
com.google.genai.types.Part.builder()
.functionCall(
@@ -158,11 +154,11 @@ private static Optional convertDataPartToGenAiPart(
.build());
}
- if ((data.containsKey("name") && data.containsKey("response"))
- || metadataType.equals(A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE)) {
- String functionName = String.valueOf(data.getOrDefault("name", ""));
- String functionId = String.valueOf(data.getOrDefault("id", ""));
- Map response = coerceToMap(data.get("response"));
+ if ((data.containsKey(NAME_KEY) && data.containsKey(RESPONSE_KEY))
+ || metadataType.equals(A2ADataPartMetadataType.FUNCTION_RESPONSE.getType())) {
+ String functionName = String.valueOf(data.getOrDefault(NAME_KEY, ""));
+ String functionId = String.valueOf(data.getOrDefault(ID_KEY, ""));
+ Map response = coerceToMap(data.get(RESPONSE_KEY));
return Optional.of(
com.google.genai.types.Part.builder()
.functionResponse(
@@ -174,6 +170,35 @@ private static Optional convertDataPartToGenAiPart(
.build());
}
+ if ((data.containsKey(CODE_KEY) && data.containsKey(LANGUAGE_KEY))
+ || metadataType.equals(A2ADataPartMetadataType.EXECUTABLE_CODE.getType())) {
+ String code = String.valueOf(data.getOrDefault(CODE_KEY, ""));
+ String language =
+ String.valueOf(
+ data.getOrDefault(LANGUAGE_KEY, Language.Known.LANGUAGE_UNSPECIFIED.toString())
+ .toString());
+ return Optional.of(
+ com.google.genai.types.Part.builder()
+ .executableCode(
+ ExecutableCode.builder().code(code).language(new Language(language)).build())
+ .build());
+ }
+
+ if ((data.containsKey(OUTCOME_KEY) && data.containsKey(OUTPUT_KEY))
+ || metadataType.equals(A2ADataPartMetadataType.CODE_EXECUTION_RESULT.getType())) {
+ String outcome =
+ String.valueOf(data.getOrDefault(OUTCOME_KEY, Outcome.Known.OUTCOME_OK).toString());
+ String output = String.valueOf(data.getOrDefault(OUTPUT_KEY, ""));
+ return Optional.of(
+ com.google.genai.types.Part.builder()
+ .codeExecutionResult(
+ CodeExecutionResult.builder()
+ .outcome(new Outcome(outcome))
+ .output(output)
+ .build())
+ .build());
+ }
+
try {
String json = objectMapper.writeValueAsString(data);
return Optional.of(com.google.genai.types.Part.builder().text(json).build());
@@ -199,73 +224,154 @@ public static Content messageToContent(Message message) {
*
* @return Optional containing the converted A2A Part, or empty if conversion fails.
*/
- private static Optional createDataPartFromFunctionCall(FunctionCall functionCall) {
- Map data = new HashMap<>();
- data.put("name", functionCall.name().orElse(""));
- data.put("id", functionCall.id().orElse(""));
- data.put("args", functionCall.args().orElse(ImmutableMap.of()));
-
- ImmutableMap metadata =
- ImmutableMap.of(A2A_DATA_PART_METADATA_TYPE_KEY, A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL);
+ private static DataPart createDataPartFromFunctionCall(
+ FunctionCall functionCall, ImmutableMap.Builder metadata) {
+ ImmutableMap.Builder data = ImmutableMap.builder();
+ addValueIfPresent(data, NAME_KEY, functionCall.name());
+ addValueIfPresent(data, ID_KEY, functionCall.id());
+ addValueIfPresent(data, ARGS_KEY, functionCall.args());
+ addValueIfPresent(data, WILL_CONTINUE_KEY, functionCall.willContinue());
+ addValueIfPresent(data, PARTIAL_ARGS_KEY, functionCall.partialArgs());
+
+ metadata.put(A2A_DATA_PART_METADATA_TYPE_KEY, A2ADataPartMetadataType.FUNCTION_CALL.getType());
+
+ return new DataPart(data.buildOrThrow(), metadata.buildOrThrow());
+ }
- return Optional.of(new DataPart(data, metadata));
+ private static void addValueIfPresent(
+ ImmutableMap.Builder data, String key, Optional> value) {
+ value.ifPresent(v -> data.put(key, v));
}
/**
* Creates an A2A DataPart from a Google GenAI FunctionResponse.
*
* @param functionResponse The GenAI FunctionResponse to convert.
- * @return Optional containing the converted A2A Part, or empty if conversion fails.
+ * @return The converted A2A Part.
+ */
+ private static DataPart createDataPartFromFunctionResponse(
+ FunctionResponse functionResponse, ImmutableMap.Builder metadata) {
+ ImmutableMap.Builder data = ImmutableMap.builder();
+ addValueIfPresent(data, NAME_KEY, functionResponse.name());
+ addValueIfPresent(data, ID_KEY, functionResponse.id());
+ addValueIfPresent(data, RESPONSE_KEY, functionResponse.response());
+ addValueIfPresent(data, WILL_CONTINUE_KEY, functionResponse.willContinue());
+ addValueIfPresent(data, SCHEDULING_KEY, functionResponse.scheduling());
+ addValueIfPresent(data, PARTS_KEY, functionResponse.parts());
+
+ metadata.put(
+ A2A_DATA_PART_METADATA_TYPE_KEY, A2ADataPartMetadataType.FUNCTION_RESPONSE.getType());
+
+ return new DataPart(data.buildOrThrow(), metadata.buildOrThrow());
+ }
+
+ /**
+ * Creates an A2A DataPart from a Google GenAI CodeExecutionResult.
+ *
+ * @param codeExecutionResult The GenAI CodeExecutionResult to convert.
+ * @return The converted A2A Part.
+ */
+ private static DataPart createDataPartFromCodeExecutionResult(
+ CodeExecutionResult codeExecutionResult, ImmutableMap.Builder metadata) {
+ ImmutableMap.Builder data = ImmutableMap.builder();
+ data.put(
+ OUTCOME_KEY,
+ codeExecutionResult
+ .outcome()
+ .map(Outcome::toString)
+ .orElse(new Outcome(Outcome.Known.OUTCOME_UNSPECIFIED).toString()));
+ addValueIfPresent(data, OUTPUT_KEY, codeExecutionResult.output());
+
+ metadata.put(
+ A2A_DATA_PART_METADATA_TYPE_KEY, A2ADataPartMetadataType.CODE_EXECUTION_RESULT.getType());
+
+ return new DataPart(data.buildOrThrow(), metadata.buildOrThrow());
+ }
+
+ /**
+ * Creates an A2A DataPart from a Google GenAI ExecutableCode.
+ *
+ * @param executableCode The GenAI ExecutableCode to convert.
+ * @return The converted A2A Part.
*/
- private static Optional createDataPartFromFunctionResponse(
- FunctionResponse functionResponse) {
- Map data = new HashMap<>();
- data.put("name", functionResponse.name().orElse(""));
- data.put("id", functionResponse.id().orElse(""));
- data.put("response", functionResponse.response().orElse(ImmutableMap.of()));
-
- ImmutableMap metadata =
- ImmutableMap.of(
- A2A_DATA_PART_METADATA_TYPE_KEY, A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE);
-
- return Optional.of(new DataPart(data, metadata));
+ private static DataPart createDataPartFromExecutableCode(
+ ExecutableCode executableCode, ImmutableMap.Builder metadata) {
+ ImmutableMap.Builder data = ImmutableMap.builder();
+ data.put(
+ LANGUAGE_KEY,
+ executableCode
+ .language()
+ .map(Language::toString)
+ .orElse(Language.Known.LANGUAGE_UNSPECIFIED.toString()));
+ addValueIfPresent(data, CODE_KEY, executableCode.code());
+
+ metadata.put(
+ A2A_DATA_PART_METADATA_TYPE_KEY, A2ADataPartMetadataType.EXECUTABLE_CODE.getType());
+
+ return new DataPart(data.buildOrThrow(), metadata.buildOrThrow());
}
private PartConverter() {}
/** Convert a GenAI part into the A2A JSON representation. */
- public static Optional> fromGenaiPart(Part part) {
+ public static io.a2a.spec.Part> fromGenaiPart(Part part, boolean isPartial) {
if (part == null) {
- return Optional.empty();
+ throw new GenAiFieldMissingException("GenAI part cannot be null");
+ }
+ ImmutableMap.Builder metadata = ImmutableMap.builder();
+ if (isPartial) {
+ metadata.put(A2A_DATA_PART_METADATA_IS_PARTIAL_KEY, true);
}
if (part.text().isPresent()) {
- return Optional.of(new TextPart(part.text().get(), new HashMap<>()));
+ addValueIfPresent(metadata, "thought", part.thought());
+ return new TextPart(part.text().get(), metadata.buildOrThrow());
}
- if (part.fileData().isPresent()) {
- FileData fileData = part.fileData().get();
- String uri = fileData.fileUri().orElse(null);
- String mime = fileData.mimeType().orElse(null);
- String name = fileData.displayName().orElse(null);
- return Optional.of(new FilePart(new FileWithUri(mime, name, uri), new HashMap<>()));
+ if (part.fileData().isPresent() || part.inlineData().isPresent()) {
+ return filePartToA2A(part, metadata);
}
- if (part.inlineData().isPresent()) {
- Blob blob = part.inlineData().get();
- byte[] bytes = blob.data().orElse(null);
- String encoded = bytes != null ? Base64.getEncoder().encodeToString(bytes) : null;
- String mime = blob.mimeType().orElse(null);
- String name = blob.displayName().orElse(null);
- return Optional.of(new FilePart(new FileWithBytes(mime, name, encoded), new HashMap<>()));
+ if (part.functionCall().isPresent()
+ || part.functionResponse().isPresent()
+ || part.executableCode().isPresent()
+ || part.codeExecutionResult().isPresent()) {
+ return dataPartToA2A(part, metadata);
}
- if (part.functionCall().isPresent() || part.functionResponse().isPresent()) {
- return convertGenaiPartToA2aPart(part).map(data -> data);
+ throw new IllegalArgumentException("Unsupported GenAI part type: " + part);
+ }
+
+ private static DataPart dataPartToA2A(Part part, ImmutableMap.Builder metadata) {
+
+ if (part.functionCall().isPresent()) {
+ return createDataPartFromFunctionCall(part.functionCall().get(), metadata);
+ } else if (part.functionResponse().isPresent()) {
+ return createDataPartFromFunctionResponse(part.functionResponse().get(), metadata);
+ } else if (part.codeExecutionResult().isPresent()) {
+ return createDataPartFromCodeExecutionResult(part.codeExecutionResult().get(), metadata);
+ } else if (part.executableCode().isPresent()) {
+ return createDataPartFromExecutableCode(part.executableCode().get(), metadata);
}
- logger.warn("Unsupported GenAI part type for JSON export: {}", part);
- return Optional.empty();
+ throw new IllegalArgumentException("Unsupported GenAI data part type: " + part);
+ }
+
+ private static FilePart filePartToA2A(Part part, ImmutableMap.Builder metadata) {
+ if (part.fileData().isPresent()) {
+ FileData fileData = part.fileData().get();
+ String uri = fileData.fileUri().orElse(null);
+ String mime = fileData.mimeType().orElse(null);
+ String name = fileData.displayName().orElse(null);
+ return new FilePart(new FileWithUri(mime, name, uri), metadata.buildOrThrow());
+ }
+ Blob blob = part.inlineData().get();
+ byte[] bytes = blob.data().orElse(null);
+ String encoded = bytes != null ? Base64.getEncoder().encodeToString(bytes) : null;
+ addValueIfPresent(metadata, "video_metadata", part.videoMetadata());
+ return new FilePart(
+ new FileWithBytes(blob.mimeType().orElse(null), blob.displayName().orElse(null), encoded),
+ metadata.buildOrThrow());
}
@SuppressWarnings("unchecked") // safe conversion from objectMapper.readValue
diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/RequestConverter.java b/a2a/src/main/java/com/google/adk/a2a/converters/RequestConverter.java
index 57f7aeffd..b289ced6c 100644
--- a/a2a/src/main/java/com/google/adk/a2a/converters/RequestConverter.java
+++ b/a2a/src/main/java/com/google/adk/a2a/converters/RequestConverter.java
@@ -164,10 +164,10 @@ private static String extractAuthorFromMetadata(Part> a2aPart) {
Optional.ofNullable(dataPart.getMetadata()).orElse(ImmutableMap.of());
String type =
metadata.getOrDefault(PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, "").toString();
- if (type.equals(PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL)) {
+ if (type.equals(A2ADataPartMetadataType.FUNCTION_CALL.getType())) {
return "model";
}
- if (type.equals(PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE)) {
+ if (type.equals(A2ADataPartMetadataType.FUNCTION_RESPONSE.getType())) {
return "user";
}
Map data = Optional.ofNullable(dataPart.getData()).orElse(ImmutableMap.of());
diff --git a/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java b/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java
index 61ab84c90..6d73a4482 100644
--- a/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java
+++ b/a2a/src/main/java/com/google/adk/a2a/converters/ResponseConverter.java
@@ -2,21 +2,19 @@
import static com.google.common.collect.ImmutableList.toImmutableList;
-import com.google.adk.a2a.common.A2AClientError;
import com.google.adk.agents.InvocationContext;
import com.google.adk.events.Event;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.genai.types.Content;
+import com.google.genai.types.Part;
import io.a2a.client.ClientEvent;
import io.a2a.client.MessageEvent;
import io.a2a.client.TaskEvent;
import io.a2a.client.TaskUpdateEvent;
-import io.a2a.spec.EventKind;
-import io.a2a.spec.JSONRPCError;
+import io.a2a.spec.Artifact;
import io.a2a.spec.Message;
-import io.a2a.spec.SendMessageResponse;
import io.a2a.spec.Task;
import io.a2a.spec.TaskArtifactUpdateEvent;
import io.a2a.spec.TaskState;
@@ -28,7 +26,6 @@
import java.util.Objects;
import java.util.Optional;
import java.util.UUID;
-import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -45,119 +42,6 @@ public final class ResponseConverter {
private ResponseConverter() {}
- /**
- * Converts a {@link SendMessageResponse} containing a {@link Message} result into ADK events.
- *
- *
Non-message results are ignored in the message-only integration and logged for awareness.
- */
- public static List sendMessageResponseToEvents(
- SendMessageResponse response, String invocationId, String branch) {
- if (response == null) {
- logger.warn("SendMessageResponse was null; returning no events.");
- return ImmutableList.of();
- }
-
- EventKind result = response.getResult();
- if (result == null) {
-
- JSONRPCError error = response.getError();
- if (error != null) {
- throw new A2AClientError(
- String.format("SendMessageResponse error for invocation %s", invocationId), error);
- }
-
- throw new A2AClientError(
- String.format("SendMessageResponse result was null for invocation %s", invocationId));
- }
-
- if (result instanceof Message message) {
- return messageToEvents(message, invocationId, branch);
- }
-
- throw new IllegalArgumentException(
- String.format(
- "SendMessageResponse result was neither a Message nor an error for invocation %s",
- invocationId));
- }
-
- /** Converts an A2A message back to ADK events. */
- public static List messageToEvents(Message message, String invocationId, String branch) {
- List events = new ArrayList<>();
-
- for (io.a2a.spec.Part> part : message.getParts()) {
- PartConverter.toGenaiPart(part)
- .ifPresent(
- genaiPart ->
- events.add(
- Event.builder()
- .id(UUID.randomUUID().toString())
- .invocationId(invocationId)
- .author(message.getRole() == Message.Role.AGENT ? "agent" : "user")
- .branch(branch)
- .content(
- Content.builder()
- .role(message.getRole() == Message.Role.AGENT ? "model" : "user")
- .parts(ImmutableList.of(genaiPart))
- .build())
- .timestamp(Instant.now().toEpochMilli())
- .build()));
- }
- return events;
- }
-
- private static Message emptyAgentMessage(String contextId) {
- Message.Builder builder =
- new Message.Builder()
- .messageId(UUID.randomUUID().toString())
- .role(Message.Role.AGENT)
- .parts(ImmutableList.of(new TextPart("")));
- if (contextId != null) {
- builder.contextId(contextId);
- }
- return builder.build();
- }
-
- /** Converts a list of ADK events into a single aggregated A2A message. */
- public static Message eventsToMessage(List events, String contextId, String taskId) {
- if (events == null || events.isEmpty()) {
- return emptyAgentMessage(contextId);
- }
-
- if (events.size() == 1) {
- return eventToMessage(events.get(0), contextId);
- }
-
- List> parts = new ArrayList<>();
- for (Event event : events) {
- parts.addAll(eventParts(event));
- }
-
- Message.Builder builder =
- new Message.Builder()
- .messageId(taskId != null ? taskId : UUID.randomUUID().toString())
- .role(Message.Role.AGENT)
- .parts(parts);
- if (contextId != null) {
- builder.contextId(contextId);
- }
- return builder.build();
- }
-
- /** Converts a single ADK event into an A2A message. */
- public static Message eventToMessage(Event event, String contextId) {
- List> parts = eventParts(event);
-
- Message.Builder builder =
- new Message.Builder()
- .messageId(event.id() != null ? event.id() : UUID.randomUUID().toString())
- .role(event.author().equalsIgnoreCase("user") ? Message.Role.USER : Message.Role.AGENT)
- .parts(parts);
- if (contextId != null) {
- builder.contextId(contextId);
- }
- return builder.build();
- }
-
/**
* Converts a A2A {@link ClientEvent} to an ADK {@link Event}, based on the event type. Returns an
* empty optional if the event should be ignored (e.g. if the event is not a final update for
@@ -174,6 +58,7 @@ public static Optional clientEventToEvent(
} else if (event instanceof TaskUpdateEvent updateEvent) {
return handleTaskUpdate(updateEvent, invocationContext);
}
+ logger.warn("Unsupported ClientEvent type: {}", event.getClass());
throw new IllegalArgumentException("Unsupported ClientEvent type: " + event.getClass());
}
@@ -189,26 +74,54 @@ private static Optional handleTaskUpdate(
var updateEvent = event.getUpdateEvent();
if (updateEvent instanceof TaskArtifactUpdateEvent artifactEvent) {
- if (Objects.equals(artifactEvent.isAppend(), false)
- || Objects.equals(artifactEvent.isLastChunk(), true)) {
- return Optional.of(taskToEvent(event.getTask(), context));
- }
- return Optional.empty();
+ boolean isAppend = Objects.equals(artifactEvent.isAppend(), true);
+ boolean isLastChunk = Objects.equals(artifactEvent.isLastChunk(), true);
+
+ Event eventPart = artifactToEvent(artifactEvent.getArtifact(), context);
+ eventPart.setPartial(isAppend || !isLastChunk);
+ // append=true, lastChunk=false: emit as partial, update aggregation
+ // append=false, lastChunk=false: emit as partial, reset aggregation
+ // append=true, lastChunk=true: emit as partial, update aggregation and emit as non-partial
+ // append=false, lastChunk=true: emit as non-partial, drop aggregation
+ return Optional.of(eventPart);
}
+
if (updateEvent instanceof TaskStatusUpdateEvent statusEvent) {
var status = statusEvent.getStatus();
- return Optional.ofNullable(status.message())
- .map(
- value ->
- messageToEvent(
- value,
- context,
- PENDING_STATES.contains(event.getTask().getStatus().state())));
+ var taskState = event.getTask().getStatus().state();
+
+ Optional messageEvent =
+ Optional.ofNullable(status.message())
+ .map(
+ value -> {
+ if (taskState == TaskState.FAILED) {
+ return messageToFailedEvent(value, context);
+ }
+ return messageToEvent(value, context, PENDING_STATES.contains(taskState));
+ });
+
+ if (statusEvent.isFinal()) {
+ return messageEvent
+ .map(Event::toBuilder)
+ .or(() -> Optional.of(remoteAgentEventBuilder(context)))
+ .map(builder -> builder.turnComplete(true))
+ .map(builder -> builder.partial(false))
+ .map(Event.Builder::build);
+ } else {
+ return messageEvent;
+ }
}
throw new IllegalArgumentException(
"Unsupported TaskUpdateEvent type: " + updateEvent.getClass());
}
+ /** Converts an artifact to an ADK event. */
+ public static Event artifactToEvent(Artifact artifact, InvocationContext invocationContext) {
+ Message message =
+ new Message.Builder().role(Message.Role.AGENT).parts(artifact.parts()).build();
+ return messageToEvent(message, invocationContext);
+ }
+
/** Converts an A2A message back to ADK events. */
public static Event messageToEvent(Message message, InvocationContext invocationContext) {
return remoteAgentEventBuilder(invocationContext)
@@ -216,6 +129,16 @@ public static Event messageToEvent(Message message, InvocationContext invocation
.build();
}
+ /** Converts an A2A message for a failed task to ADK event filling in the error message. */
+ public static Event messageToFailedEvent(Message message, InvocationContext invocationContext) {
+ Event.Builder builder = remoteAgentEventBuilder(invocationContext);
+ Optional.ofNullable(Iterables.getFirst(message.getParts(), null))
+ .flatMap(PartConverter::toTextPart)
+ .ifPresent(textPart -> builder.errorMessage(textPart.getText()));
+
+ return builder.build();
+ }
+
/**
* Converts an A2A message back to ADK events. For streaming task in pending state it sets the
* thought field to true, to mark them as thought updates.
@@ -223,7 +146,7 @@ public static Event messageToEvent(Message message, InvocationContext invocation
public static Event messageToEvent(
Message message, InvocationContext invocationContext, boolean isPending) {
- ImmutableList genaiParts =
+ ImmutableList genaiParts =
PartConverter.toGenaiParts(message.getParts()).stream()
.map(part -> part.toBuilder().thought(isPending).build())
.collect(toImmutableList());
@@ -258,19 +181,6 @@ public static Event taskToEvent(Task task, InvocationContext invocationContext)
return emptyEvent(invocationContext);
}
- private static List> eventParts(Event event) {
- List> parts = new ArrayList<>();
- Optional content = event.content();
- if (content.isEmpty() || content.get().parts().isEmpty()) {
- return parts;
- }
-
- for (com.google.genai.types.Part genaiPart : content.get().parts().get()) {
- PartConverter.fromGenaiPart(genaiPart).ifPresent(parts::add);
- }
- return parts;
- }
-
private static Event emptyEvent(InvocationContext invocationContext) {
Event.Builder builder =
Event.builder()
@@ -283,7 +193,7 @@ private static Event emptyEvent(InvocationContext invocationContext) {
return builder.build();
}
- private static Content fromModelParts(List parts) {
+ private static Content fromModelParts(List parts) {
return Content.builder().role("model").parts(parts).build();
}
@@ -296,14 +206,69 @@ private static Event.Builder remoteAgentEventBuilder(InvocationContext invocatio
.timestamp(Instant.now().toEpochMilli());
}
- /** Simple REST-friendly wrapper to carry either a message result or a task result. */
- public record MessageSendResult(@Nullable Message message, @Nullable Task task) {
- public static MessageSendResult fromMessage(Message message) {
- return new MessageSendResult(message, null);
+ private static Message emptyAgentMessage(String contextId) {
+ Message.Builder builder =
+ new Message.Builder()
+ .messageId(UUID.randomUUID().toString())
+ .role(Message.Role.AGENT)
+ .parts(ImmutableList.of(new TextPart("")));
+ if (contextId != null) {
+ builder.contextId(contextId);
+ }
+ return builder.build();
+ }
+
+ /** Converts a list of ADK events into a single aggregated A2A message. */
+ public static Message eventsToMessage(List events, String contextId, String taskId) {
+ if (events == null || events.isEmpty()) {
+ return emptyAgentMessage(contextId);
+ }
+
+ if (events.size() == 1) {
+ return eventToMessage(events.get(0), contextId);
+ }
+
+ List> parts = new ArrayList<>();
+ for (Event event : events) {
+ parts.addAll(eventParts(event));
+ }
+
+ Message.Builder builder =
+ new Message.Builder()
+ .messageId(taskId != null ? taskId : UUID.randomUUID().toString())
+ .role(Message.Role.AGENT)
+ .parts(parts);
+ if (contextId != null) {
+ builder.contextId(contextId);
+ }
+ return builder.build();
+ }
+
+ /** Converts a single ADK event into an A2A message. */
+ public static Message eventToMessage(Event event, String contextId) {
+ List> parts = eventParts(event);
+
+ Message.Builder builder =
+ new Message.Builder()
+ .messageId(event.id() != null ? event.id() : UUID.randomUUID().toString())
+ .role(event.author().equalsIgnoreCase("user") ? Message.Role.USER : Message.Role.AGENT)
+ .parts(parts);
+ if (contextId != null) {
+ builder.contextId(contextId);
+ }
+ return builder.build();
+ }
+
+ private static List> eventParts(Event event) {
+ List> parts = new ArrayList<>();
+ Optional content = event.content();
+ if (content.isEmpty() || content.get().parts().isEmpty()) {
+ return parts;
}
- public static MessageSendResult fromTask(Task task) {
- return new MessageSendResult(null, task);
+ for (com.google.genai.types.Part genaiPart : content.get().parts().get()) {
+ parts.add(PartConverter.fromGenaiPart(genaiPart, false));
}
+ return parts;
}
}
diff --git a/a2a/src/main/java/com/google/adk/a2a/AgentExecutor.java b/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java
similarity index 50%
rename from a2a/src/main/java/com/google/adk/a2a/AgentExecutor.java
rename to a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java
index 0fbeb0a72..b7b4e9953 100644
--- a/a2a/src/main/java/com/google/adk/a2a/AgentExecutor.java
+++ b/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java
@@ -1,9 +1,10 @@
-package com.google.adk.a2a;
+package com.google.adk.a2a.executor;
+
+import static java.util.Objects.requireNonNull;
import com.google.adk.a2a.converters.EventConverter;
import com.google.adk.a2a.converters.PartConverter;
import com.google.adk.agents.BaseAgent;
-import com.google.adk.agents.RunConfig;
import com.google.adk.apps.App;
import com.google.adk.artifacts.BaseArtifactService;
import com.google.adk.events.Event;
@@ -13,19 +14,28 @@
import com.google.adk.sessions.BaseSessionService;
import com.google.adk.sessions.Session;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.genai.types.Content;
+import com.google.genai.types.CustomMetadata;
import io.a2a.server.agentexecution.RequestContext;
import io.a2a.server.events.EventQueue;
import io.a2a.server.tasks.TaskUpdater;
+import io.a2a.spec.Artifact;
import io.a2a.spec.InvalidAgentResponseError;
import io.a2a.spec.Message;
import io.a2a.spec.Part;
+import io.a2a.spec.TaskArtifactUpdateEvent;
+import io.a2a.spec.TaskState;
+import io.a2a.spec.TaskStatus;
+import io.a2a.spec.TaskStatusUpdateEvent;
import io.a2a.spec.TextPart;
+import io.reactivex.rxjava3.core.Completable;
+import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.core.Maybe;
+import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.CompositeDisposable;
import io.reactivex.rxjava3.disposables.Disposable;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -41,15 +51,11 @@
* use in production code.
*/
public class AgentExecutor implements io.a2a.server.agentexecution.AgentExecutor {
-
private static final Logger logger = LoggerFactory.getLogger(AgentExecutor.class);
private static final String USER_ID_PREFIX = "A2A_USER_";
- private static final RunConfig DEFAULT_RUN_CONFIG =
- RunConfig.builder().setStreamingMode(RunConfig.StreamingMode.NONE).setMaxLlmCalls(20).build();
-
private final Map activeTasks = new ConcurrentHashMap<>();
private final Runner.Builder runnerBuilder;
- private final RunConfig runConfig;
+ private final AgentExecutorConfig agentExecutorConfig;
private AgentExecutor(
App app,
@@ -59,7 +65,10 @@ private AgentExecutor(
BaseSessionService sessionService,
BaseMemoryService memoryService,
List extends Plugin> plugins,
- RunConfig runConfig) {
+ AgentExecutorConfig agentExecutorConfig) {
+ requireNonNull(agentExecutorConfig);
+ this.agentExecutorConfig = agentExecutorConfig;
+
this.runnerBuilder =
Runner.builder()
.agent(agent)
@@ -73,7 +82,6 @@ private AgentExecutor(
}
// Check that the runner is configured correctly and can be built.
var unused = runnerBuilder.build();
- this.runConfig = runConfig == null ? DEFAULT_RUN_CONFIG : runConfig;
}
/** Builder for {@link AgentExecutor}. */
@@ -85,7 +93,13 @@ public static class Builder {
private BaseSessionService sessionService;
private BaseMemoryService memoryService;
private List extends Plugin> plugins = ImmutableList.of();
- private RunConfig runConfig;
+ private AgentExecutorConfig agentExecutorConfig;
+
+ @CanIgnoreReturnValue
+ public Builder agentExecutorConfig(AgentExecutorConfig agentExecutorConfig) {
+ this.agentExecutorConfig = agentExecutorConfig;
+ return this;
+ }
@CanIgnoreReturnValue
public Builder app(App app) {
@@ -129,16 +143,16 @@ public Builder plugins(List extends Plugin> plugins) {
return this;
}
- @CanIgnoreReturnValue
- public Builder runConfig(RunConfig runConfig) {
- this.runConfig = runConfig;
- return this;
- }
-
- @CanIgnoreReturnValue
public AgentExecutor build() {
return new AgentExecutor(
- app, agent, appName, artifactService, sessionService, memoryService, plugins, runConfig);
+ app,
+ agent,
+ appName,
+ artifactService,
+ sessionService,
+ memoryService,
+ plugins,
+ agentExecutorConfig);
}
}
@@ -156,45 +170,88 @@ public void execute(RequestContext ctx, EventQueue eventQueue) {
if (message == null) {
throw new IllegalArgumentException("Message cannot be null");
}
-
// Submits a new task if there is no active task.
if (ctx.getTask() == null) {
updater.submit();
}
-
// Group all reactive work for this task into one container
CompositeDisposable taskDisposables = new CompositeDisposable();
// Check if the task with the task id is already running, put if absent.
if (activeTasks.putIfAbsent(ctx.getTaskId(), taskDisposables) != null) {
throw new IllegalStateException(String.format("Task %s already running", ctx.getTaskId()));
}
-
- EventProcessor p = new EventProcessor();
+ EventProcessor p = new EventProcessor(agentExecutorConfig.outputMode());
Content content = PartConverter.messageToContent(message);
- Runner runner = runnerBuilder.build();
+ Single skipExecution =
+ agentExecutorConfig.beforeExecuteCallback() != null
+ ? agentExecutorConfig.beforeExecuteCallback().call(ctx)
+ : Single.just(false);
+ Runner runner = runnerBuilder.build();
taskDisposables.add(
- prepareSession(ctx, runner.appName(), runner.sessionService())
+ skipExecution
.flatMapPublisher(
- session -> {
- updater.startWork();
- return runner.runAsync(getUserId(ctx), session.id(), content, runConfig);
+ skip -> {
+ if (skip) {
+ cancel(ctx, eventQueue);
+ return Flowable.empty();
+ }
+ return Maybe.defer(
+ () -> {
+ return prepareSession(ctx, runner.appName(), runner.sessionService());
+ })
+ .flatMapPublisher(
+ session -> {
+ updater.startWork();
+ return runner.runAsync(
+ getUserId(ctx),
+ session.id(),
+ content,
+ agentExecutorConfig.runConfig());
+ });
})
- .subscribe(
+ .concatMap(
event -> {
- p.process(event, updater);
- },
+ return p.process(event, ctx, agentExecutorConfig.afterEventCallback(), eventQueue)
+ .toFlowable();
+ })
+ // Ignore all events from the runner, since they are already processed.
+ .ignoreElements()
+ .materialize()
+ .flatMapCompletable(
+ notification -> {
+ Throwable error = notification.getError();
+ if (error != null) {
+ logger.error("Runner failed to execute", error);
+ }
+ return handleExecutionEnd(ctx, error, eventQueue);
+ })
+ .doFinally(() -> cleanupTask(ctx.getTaskId()))
+ .subscribe(
+ () -> {},
error -> {
- logger.error("Runner failed with {}", error);
- updater.fail(failedMessage(ctx, error));
- cleanupTask(ctx.getTaskId());
- },
- () -> {
- updater.complete();
- cleanupTask(ctx.getTaskId());
+ logger.error("Failed to handle execution end", error);
}));
}
+ private Completable handleExecutionEnd(
+ RequestContext ctx, Throwable error, EventQueue eventQueue) {
+ TaskState state = error != null ? TaskState.FAILED : TaskState.COMPLETED;
+ Message message = error != null ? failedMessage(ctx, error) : null;
+ TaskStatusUpdateEvent initialEvent =
+ new TaskStatusUpdateEvent.Builder()
+ .taskId(ctx.getTaskId())
+ .contextId(ctx.getContextId())
+ .isFinal(true)
+ .status(new TaskStatus(state, message, null))
+ .build();
+ Maybe afterExecute =
+ agentExecutorConfig.afterExecuteCallback() != null
+ ? agentExecutorConfig.afterExecuteCallback().call(ctx, initialEvent)
+ : Maybe.just(initialEvent);
+ return afterExecute.doOnSuccess(event -> eventQueue.enqueueEvent(event)).ignoreElement();
+ }
+
private void cleanupTask(String taskId) {
Disposable d = activeTasks.remove(taskId);
if (d != null) {
@@ -229,34 +286,84 @@ private static Message failedMessage(RequestContext context, Throwable e) {
// Processor that will process all events related to the one runner invocation.
private static class EventProcessor {
+ private final String runArtifactId;
+ private final AgentExecutorConfig.OutputMode outputMode;
+ private final Map lastAgentPartialArtifact = new ConcurrentHashMap<>();
// All artifacts related to the invocation should have the same artifact id.
- private EventProcessor() {
- artifactId = UUID.randomUUID().toString();
+ private EventProcessor(AgentExecutorConfig.OutputMode outputMode) {
+ this.runArtifactId = UUID.randomUUID().toString();
+ this.outputMode = outputMode;
}
- private final String artifactId;
-
- private void process(Event event, TaskUpdater updater) {
+ private Maybe process(
+ Event event,
+ RequestContext ctx,
+ Callbacks.AfterEventCallback callback,
+ EventQueue eventQueue) {
if (event.errorCode().isPresent()) {
- throw new InvalidAgentResponseError(
- null, // Uses default code -32006
- "Agent returned an error: " + event.errorCode().get(),
- null);
+ return Maybe.error(
+ new InvalidAgentResponseError(
+ null, // Uses default code -32006
+ "Agent returned an error: " + event.errorCode().get(),
+ null));
+ }
+ ImmutableList> parts =
+ EventConverter.contentToParts(event.content(), event.partial().orElse(false));
+ Map metadata = new HashMap<>();
+ if (event.customMetadata().isPresent()) {
+ for (CustomMetadata cm : event.customMetadata().get()) {
+ if (cm.key().isPresent() && cm.stringValue().isPresent()) {
+ metadata.put(cm.key().get(), cm.stringValue().get());
+ }
+ }
}
- ImmutableList> parts = EventConverter.contentToParts(event.content());
-
- // Mark all parts as partial if the event is partial.
- if (event.partial().orElse(false)) {
- parts.forEach(
- part -> {
- Map metadata = part.getMetadata();
- metadata.put("adk_partial", true);
- });
+ Boolean append = true;
+ Boolean lastChunk = false;
+ String artifactId = runArtifactId;
+
+ if (outputMode == AgentExecutorConfig.OutputMode.ARTIFACT_PER_EVENT) {
+ String author = event.author();
+ boolean isPartial = event.partial().orElse(false);
+
+ if (lastAgentPartialArtifact.containsKey(author)) {
+ artifactId = lastAgentPartialArtifact.get(author);
+ append = isPartial;
+ } else {
+ artifactId = UUID.randomUUID().toString();
+ append = isPartial;
+ }
+
+ lastChunk = !isPartial;
+
+ if (isPartial) {
+ lastAgentPartialArtifact.put(author, artifactId);
+ } else {
+ lastAgentPartialArtifact.remove(author);
+ }
}
- updater.addArtifact(parts, artifactId, null, ImmutableMap.of());
+ TaskArtifactUpdateEvent initialEvent =
+ new TaskArtifactUpdateEvent.Builder()
+ .taskId(ctx.getTaskId())
+ .contextId(ctx.getContextId())
+ .lastChunk(lastChunk)
+ .append(append)
+ .artifact(
+ new Artifact.Builder()
+ .artifactId(artifactId)
+ .parts(parts)
+ .metadata(metadata)
+ .build())
+ .build();
+
+ Maybe afterEvent =
+ callback != null ? callback.call(ctx, initialEvent, event) : Maybe.just(initialEvent);
+ return afterEvent.doOnSuccess(
+ finalEvent -> {
+ eventQueue.enqueueEvent(finalEvent);
+ });
}
}
}
diff --git a/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutorConfig.java b/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutorConfig.java
new file mode 100644
index 000000000..ba0177dc4
--- /dev/null
+++ b/a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutorConfig.java
@@ -0,0 +1,72 @@
+package com.google.adk.a2a.executor;
+
+import com.google.adk.a2a.executor.Callbacks.AfterEventCallback;
+import com.google.adk.a2a.executor.Callbacks.AfterExecuteCallback;
+import com.google.adk.a2a.executor.Callbacks.BeforeExecuteCallback;
+import com.google.adk.agents.RunConfig;
+import com.google.auto.value.AutoValue;
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import org.jspecify.annotations.Nullable;
+
+/** Configuration for the {@link AgentExecutor}. */
+@AutoValue
+public abstract class AgentExecutorConfig {
+
+ /**
+ * Output mode for the agent executor.
+ *
+ *
ARTIFACT_PER_RUN: The agent executor will return one artifact per run.
+ *
+ *