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](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE) @@ -144,13 +50,13 @@ If you're using Maven, add the following to your dependencies: com.google.adk google-adk - 1.2.0 + 0.8.0 com.google.adk google-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.adk google-adk-parent - 1.2.0 + 0.8.1-SNAPSHOT google-adk-a2a @@ -19,17 +19,22 @@ 0.3.2.Final ${project.version} 33.0.0-jre - 2.19.0 3.1.5 + 2.19.0 1.0.0 - 2.0.17 2.38.0 + 1.76.2 + 2.0.17 1.4.4 4.13.2 - 1.62.2 + + com.google.adk + google-adk + ${google.adk.version} + io.grpc grpc-netty-shaded @@ -51,11 +56,6 @@ gson 2.10.1 - - com.google.adk - google-adk - ${google.adk.version} - com.google.guava guava @@ -76,6 +76,11 @@ jackson-databind ${jackson.version} + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + com.fasterxml.jackson.module jackson-module-parameter-names @@ -129,13 +134,13 @@ test - org.junit.jupiter - junit-jupiter-api + org.mockito + mockito-core test - org.mockito - mockito-core + org.junit.jupiter + junit-jupiter-api test @@ -149,7 +154,6 @@ test - @@ -159,6 +163,14 @@ + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + ${java.version} + + org.apache.maven.plugins maven-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 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 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 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. + * + *

ARTIFACT_PER_EVENT: The agent executor will return one artifact per event. + */ + public enum OutputMode { + ARTIFACT_PER_RUN, + ARTIFACT_PER_EVENT + } + + private static final RunConfig DEFAULT_RUN_CONFIG = + RunConfig.builder().setStreamingMode(RunConfig.StreamingMode.NONE).setMaxLlmCalls(20).build(); + + public abstract RunConfig runConfig(); + + public abstract OutputMode outputMode(); + + public abstract @Nullable BeforeExecuteCallback beforeExecuteCallback(); + + public abstract @Nullable AfterExecuteCallback afterExecuteCallback(); + + public abstract @Nullable AfterEventCallback afterEventCallback(); + + public abstract Builder toBuilder(); + + public static Builder builder() { + return new AutoValue_AgentExecutorConfig.Builder() + .runConfig(DEFAULT_RUN_CONFIG) + .outputMode(OutputMode.ARTIFACT_PER_RUN); + } + + /** Builder for {@link AgentExecutorConfig}. */ + @AutoValue.Builder + public abstract static class Builder { + @CanIgnoreReturnValue + public abstract Builder runConfig(RunConfig runConfig); + + @CanIgnoreReturnValue + public abstract Builder outputMode(OutputMode outputMode); + + @CanIgnoreReturnValue + public abstract Builder beforeExecuteCallback(BeforeExecuteCallback beforeExecuteCallback); + + @CanIgnoreReturnValue + public abstract Builder afterExecuteCallback(AfterExecuteCallback afterExecuteCallback); + + @CanIgnoreReturnValue + public abstract Builder afterEventCallback(AfterEventCallback afterEventCallback); + + abstract AgentExecutorConfig autoBuild(); + + public AgentExecutorConfig build() { + return autoBuild(); + } + } +} diff --git a/a2a/src/main/java/com/google/adk/a2a/executor/Callbacks.java b/a2a/src/main/java/com/google/adk/a2a/executor/Callbacks.java new file mode 100644 index 000000000..666f1d8a0 --- /dev/null +++ b/a2a/src/main/java/com/google/adk/a2a/executor/Callbacks.java @@ -0,0 +1,68 @@ +package com.google.adk.a2a.executor; + +import com.google.adk.events.Event; +import io.a2a.server.agentexecution.RequestContext; +import io.a2a.spec.TaskArtifactUpdateEvent; +import io.a2a.spec.TaskStatusUpdateEvent; +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.Single; + +/** Functional interfaces for agent executor lifecycle callbacks. */ +public final class Callbacks { + + private Callbacks() {} + + interface BeforeExecuteCallbackBase {} + + /** Async callback interface for actions to be performed before an execution is started. */ + @FunctionalInterface + public interface BeforeExecuteCallback extends BeforeExecuteCallbackBase { + /** + * Callback which will be called before an execution is started. It can be used to instrument a + * context or prevent the execution by returning an error. + * + * @param ctx the request context + * @return a {@link Single} that completes with a boolean indicating whether the execution + * should be prevented + */ + Single call(RequestContext ctx); + } + + interface AfterExecuteCallbackBase {} + + /** + * Async callback interface for actions to be performed after an execution is completed or failed. + */ + @FunctionalInterface + public interface AfterExecuteCallback extends AfterExecuteCallbackBase { + /** + * Callback which will be called after an execution resolved into a completed or failed task. + * This gives an opportunity to enrich the event with additional metadata or log it. + * + * @param ctx the request context + * @param finalUpdateEvent the final update event + * @return a {@link Maybe} that completes when the callback is done + */ + Maybe call(RequestContext ctx, TaskStatusUpdateEvent finalUpdateEvent); + } + + interface AfterEventCallbackBase {} + + /** Async callback interface for actions to be performed after an event is processed. */ + @FunctionalInterface + public interface AfterEventCallback extends AfterEventCallbackBase { + /** + * Callback which will be called after an ADK event is successfully converted to an A2A event. + * This gives an opportunity to enrich the event with additional metadata or abort the execution + * by returning an error. The callback is not invoked for errors originating from ADK or event + * processing. + * + * @param ctx the request context + * @param processedEvent the processed task artifact update event + * @param event the ADK event + * @return a {@link Maybe} that completes when the callback is done + */ + Maybe call( + RequestContext ctx, TaskArtifactUpdateEvent processedEvent, Event event); + } +} diff --git a/a2a/src/test/java/com/google/adk/a2a/AgentExecutorTest.java b/a2a/src/test/java/com/google/adk/a2a/AgentExecutorTest.java deleted file mode 100644 index 44daf13d1..000000000 --- a/a2a/src/test/java/com/google/adk/a2a/AgentExecutorTest.java +++ /dev/null @@ -1,82 +0,0 @@ -package com.google.adk.a2a; - -import static org.junit.Assert.assertThrows; - -import com.google.adk.agents.BaseAgent; -import com.google.adk.agents.InvocationContext; -import com.google.adk.apps.App; -import com.google.adk.artifacts.InMemoryArtifactService; -import com.google.adk.events.Event; -import com.google.adk.sessions.InMemorySessionService; -import com.google.common.collect.ImmutableList; -import io.reactivex.rxjava3.core.Flowable; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public final class AgentExecutorTest { - - private TestAgent testAgent; - - @Before - public void setUp() { - testAgent = new TestAgent(); - } - - @Test - public void createAgentExecutor_noAgent_succeeds() { - var unused = - new AgentExecutor.Builder() - .app(App.builder().name("test_app").rootAgent(testAgent).build()) - .sessionService(new InMemorySessionService()) - .artifactService(new InMemoryArtifactService()) - .build(); - } - - @Test - public void createAgentExecutor_withAgentAndApp_throwsException() { - assertThrows( - IllegalStateException.class, - () -> { - new AgentExecutor.Builder() - .agent(testAgent) - .app(App.builder().name("test_app").rootAgent(testAgent).build()) - .sessionService(new InMemorySessionService()) - .artifactService(new InMemoryArtifactService()) - .build(); - }); - } - - @Test - public void createAgentExecutor_withEmptyAgentAndApp_throwsException() { - assertThrows( - IllegalStateException.class, - () -> { - new AgentExecutor.Builder() - .sessionService(new InMemorySessionService()) - .artifactService(new InMemoryArtifactService()) - .build(); - }); - } - - private static final class TestAgent extends BaseAgent { - private final Flowable eventsToEmit = Flowable.empty(); - - TestAgent() { - // BaseAgent constructor: name, description, examples, tools, model - super("test_agent", "test", ImmutableList.of(), null, null); - } - - @Override - protected Flowable runAsyncImpl(InvocationContext invocationContext) { - return eventsToEmit; - } - - @Override - protected Flowable runLiveImpl(InvocationContext invocationContext) { - return eventsToEmit; - } - } -} diff --git a/a2a/src/test/java/com/google/adk/a2a/RemoteA2AAgentTest.java b/a2a/src/test/java/com/google/adk/a2a/RemoteA2AAgentTest.java new file mode 100644 index 000000000..87eaa2321 --- /dev/null +++ b/a2a/src/test/java/com/google/adk/a2a/RemoteA2AAgentTest.java @@ -0,0 +1,872 @@ +package com.google.adk.a2a; + +import static com.google.common.truth.Truth.assertThat; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +import com.google.adk.a2a.common.A2AMetadata; +import com.google.adk.agents.BaseAgent; +import com.google.adk.agents.CallbackContext; +import com.google.adk.agents.Callbacks; +import com.google.adk.agents.InvocationContext; +import com.google.adk.agents.RunConfig; +import com.google.adk.artifacts.InMemoryArtifactService; +import com.google.adk.events.Event; +import com.google.adk.plugins.PluginManager; +import com.google.adk.sessions.InMemorySessionService; +import com.google.adk.sessions.Session; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.genai.types.Content; +import com.google.genai.types.CustomMetadata; +import com.google.genai.types.FunctionResponse; +import com.google.genai.types.Part; +import io.a2a.client.Client; +import io.a2a.client.ClientEvent; +import io.a2a.client.TaskEvent; +import io.a2a.client.TaskUpdateEvent; +import io.a2a.spec.AgentCapabilities; +import io.a2a.spec.AgentCard; +import io.a2a.spec.Artifact; +import io.a2a.spec.DataPart; +import io.a2a.spec.FilePart; +import io.a2a.spec.FileWithUri; +import io.a2a.spec.Message; +import io.a2a.spec.Task; +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.Flowable; +import io.reactivex.rxjava3.core.Maybe; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; + +@RunWith(JUnit4.class) +public final class RemoteA2AAgentTest { + + private Client mockClient; + private AgentCard agentCard; + private InvocationContext invocationContext; + private Session session; + + @Before + public void setUp() { + mockClient = mock(Client.class); + agentCard = + new AgentCard.Builder() + .name("remote-agent") + .description("Remote Agent") + .version("1.0.0") + .url("http://example.com") + .capabilities(new AgentCapabilities.Builder().streaming(true).build()) + .defaultInputModes(ImmutableList.of("text")) + .defaultOutputModes(ImmutableList.of("text")) + .skills(ImmutableList.of()) + .build(); + + when(mockClient.getAgentCard()).thenReturn(agentCard); + + session = + Session.builder("session-1") + .appName("demo") + .userId("user") + .events( + ImmutableList.of( + Event.builder() + .id("event-1") + .author("user") + .content( + Content.builder() + .role("user") + .parts(ImmutableList.of(Part.builder().text("Hello").build())) + .build()) + .build())) + .build(); + + invocationContext = + InvocationContext.builder() + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .pluginManager(new PluginManager()) + .invocationId("invocation-1") + .agent(new TestAgent()) + .session(session) + .runConfig(RunConfig.builder().build()) + .endInvocation(false) + .build(); + } + + @Test + public void runAsync_aggregatesPartialEvents() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + consumer.accept(createPartialEvent("Hello ", true, false), agentCard); + consumer.accept(createPartialEvent("World!", true, false), agentCard); + consumer.accept(createFinalEvent("Final artifact content"), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(4); + assertText(events.get(0), "Hello "); + assertNotAggregated(events.get(0)); + assertText(events.get(1), "World!"); + assertNotAggregated(events.get(1)); + Event aggregatedEvent = events.get(2); + assertThat(aggregatedEvent.content().get().parts().get()).hasSize(1); + assertThought(aggregatedEvent, false); + assertText(aggregatedEvent, "Hello World!"); + assertAggregated(aggregatedEvent); + Event finalEvent = events.get(3); + assertText(finalEvent, "Final artifact content"); + assertRequestMetadata(finalEvent); + assertResponseMetadata(finalEvent); + } + + @Test + public void runAsync_aggregatesInterleavedFunctionCalls() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + consumer.accept(createPartialEvent("Hello ", true, false), agentCard); + consumer.accept(createPartialFunctionCallEvent("get_weather", "call_1"), agentCard); + consumer.accept(createPartialEvent("World!", true, false), agentCard); + consumer.accept(createFinalEvent("Final"), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(6); + assertText(events.get(0), "Hello "); + assertNotAggregated(events.get(0)); + assertAggregated(events.get(1)); // Flushed Aggregation + assertText(events.get(1), "Hello "); + assertThat(events.get(2).content().get().parts().get().get(0).functionCall()).isPresent(); + assertThat( + events + .get(2) + .content() + .get() + .parts() + .get() + .get(0) + .functionCall() + .get() + .name() + .orElse("")) + .isEqualTo("get_weather"); + assertText(events.get(3), "World!"); + assertNotAggregated(events.get(3)); + assertText(events.get(5), "Final"); + assertNotAggregated(events.get(5)); + assertRequestMetadata(events.get(5)); + assertResponseMetadata(events.get(5)); + } + + @Test + public void runAsync_aggregatesFiles() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + consumer.accept(createPartialEvent("Here is a file: ", true, false), agentCard); + consumer.accept( + createPartialFileEvent("http://example.com/file.txt", "text/plain"), agentCard); + consumer.accept(createFinalEvent("Done"), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(4); + assertText(events.get(0), "Here is a file: "); + assertNotAggregated(events.get(0)); + assertAggregated(events.get(1)); // Flushed Aggregation + assertText(events.get(1), "Here is a file: "); + Part filePart = events.get(2).content().get().parts().get().get(0); + assertThat(filePart.fileData()).isPresent(); + assertThat(filePart.fileData().get().fileUri().orElse("")) + .isEqualTo("http://example.com/file.txt"); + assertRequestMetadata(events.get(2)); + assertResponseMetadata(events.get(2)); + + assertText(events.get(3), "Done"); + assertRequestMetadata(events.get(3)); + assertResponseMetadata(events.get(3)); + } + + @Test + public void runAsync_handlesTasksWithStatusMessage() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + Task task = + new Task.Builder() + .id("task-1") + .contextId("context-1") + .status( + new TaskStatus( + TaskState.COMPLETED, + new Message.Builder() + .role(Message.Role.AGENT) + .parts(ImmutableList.of(new TextPart("hello"))) + .build(), + null)) + .build(); + consumer.accept(new TaskEvent(task), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(1); + assertText(events.get(0), "hello"); + assertRequestMetadata(events.get(0)); + assertResponseMetadata(events.get(0)); + } + + @Test + public void runAsync_handlesTasksWithMultipartArtifact() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + Artifact artifact = + new Artifact.Builder() + .artifactId("artifact-1") + .parts(ImmutableList.of(new TextPart("hello"), new TextPart("world"))) + .build(); + Task task = + new Task.Builder() + .id("task-1") + .contextId("context-1") + .status(new TaskStatus(TaskState.COMPLETED)) + .artifacts(ImmutableList.of(artifact)) + .build(); + consumer.accept(new TaskEvent(task), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(1); + assertThat(events.get(0).content().get().parts().get()).hasSize(2); + assertText(events.get(0), 0, "hello"); + assertText(events.get(0), 1, "world"); + assertRequestMetadata(events.get(0)); + assertResponseMetadata(events.get(0)); + } + + @Test + public void runAsync_handlesNonFinalStatusUpdatesAsThoughts() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + Task task1 = + new Task.Builder() + .id("task-1") + .contextId("context-1") + .status(new TaskStatus(TaskState.SUBMITTED)) + .build(); + consumer.accept( + new TaskUpdateEvent( + task1, + new TaskStatusUpdateEvent.Builder() + .taskId("task-1") + .contextId("context-1") + .status( + new TaskStatus( + TaskState.SUBMITTED, + new Message.Builder() + .role(Message.Role.AGENT) + .parts(ImmutableList.of(new TextPart("submitted..."))) + .build(), + null)) + .build()), + agentCard); + Task task2 = + new Task.Builder() + .id("task-1") + .contextId("context-1") + .status(new TaskStatus(TaskState.WORKING)) + .build(); + consumer.accept( + new TaskUpdateEvent( + task2, + new TaskStatusUpdateEvent.Builder() + .taskId("task-1") + .contextId("context-1") + .status( + new TaskStatus( + TaskState.WORKING, + new Message.Builder() + .role(Message.Role.AGENT) + .parts(ImmutableList.of(new TextPart("working..."))) + .build(), + null)) + .build()), + agentCard); + Task task3 = + new Task.Builder() + .id("task-1") + .contextId("context-1") + .status(new TaskStatus(TaskState.COMPLETED)) + .artifacts( + ImmutableList.of( + new Artifact.Builder() + .artifactId("a1") + .parts(ImmutableList.of(new TextPart("done"))) + .build())) + .build(); + consumer.accept(new TaskEvent(task3), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(3); + assertText(events.get(0), "submitted..."); + assertThought(events.get(0), true); + assertRequestMetadata(events.get(0)); + assertResponseMetadata(events.get(0)); + assertText(events.get(1), "working..."); + assertThought(events.get(1), true); + assertRequestMetadata(events.get(1)); + assertResponseMetadata(events.get(1)); + assertText(events.get(2), "done"); + assertThought(events.get(2), false); + assertRequestMetadata(events.get(2)); + assertResponseMetadata(events.get(2)); + } + + @Test + @SuppressWarnings("unchecked") // cast for Mockito + public void runAsync_constructsRequestWithHistory() { + RemoteA2AAgent agent = createAgent(); + Session historySession = + Session.builder("session-2") + .appName("demo") + .userId("user") + .events( + ImmutableList.of( + Event.builder() + .id("e1") + .author("user") + .content( + Content.builder() + .role("user") + .parts(ImmutableList.of(Part.builder().text("hello").build())) + .build()) + .build(), + Event.builder() + .id("e2") + .author("model") + .content( + Content.builder() + .role("model") + .parts(ImmutableList.of(Part.builder().text("hi").build())) + .build()) + .build(), + Event.builder() + .id("e3") + .author("user") + .content( + Content.builder() + .role("user") + .parts( + ImmutableList.of(Part.builder().text("how are you?").build())) + .build()) + .build())) + .build(); + InvocationContext context = + InvocationContext.builder() + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .pluginManager(new PluginManager()) + .invocationId("invocation-2") + .agent(new TestAgent()) + .session(historySession) + .runConfig(RunConfig.builder().build()) + .build(); + mockStreamResponse( + consumer -> { + consumer.accept(createFinalEvent("fine"), agentCard); + }); + + var unused = agent.runAsync(context).toList().blockingGet(); + + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Message.class); + verify(mockClient) + .sendMessage(messageCaptor.capture(), any(List.class), any(Consumer.class), any()); + Message message = messageCaptor.getValue(); + assertThat(message.getRole()).isEqualTo(Message.Role.USER); + assertThat(message.getParts()).hasSize(3); + assertThat(((TextPart) message.getParts().get(0)).getText()).isEqualTo("hello"); + assertThat(((TextPart) message.getParts().get(1)).getText()).isEqualTo("hi"); + assertThat(((TextPart) message.getParts().get(2)).getText()).isEqualTo("how are you?"); + } + + @Test + @SuppressWarnings("unchecked") // cast for Mockito + public void runAsync_constructsRequestWithFunctionResponse() { + RemoteA2AAgent agent = createAgent(); + Session session = + Session.builder("session-3") + .appName("demo") + .userId("user") + .events( + ImmutableList.of( + Event.builder() + .id("e1") + .author("user") + .content( + Content.builder() + .role("user") + .parts( + ImmutableList.of( + Part.builder() + .functionResponse( + FunctionResponse.builder() + .name("fn") + .id("call-1") + .response(ImmutableMap.of("status", "ok")) + .build()) + .build())) + .build()) + .build())) + .build(); + InvocationContext context = + InvocationContext.builder() + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .pluginManager(new PluginManager()) + .invocationId("invocation-3") + .agent(new TestAgent()) + .session(session) + .runConfig(RunConfig.builder().build()) + .build(); + mockStreamResponse( + consumer -> { + consumer.accept(createFinalEvent("ok"), agentCard); + }); + + var unused = agent.runAsync(context).toList().blockingGet(); + + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Message.class); + verify(mockClient) + .sendMessage(messageCaptor.capture(), any(List.class), any(Consumer.class), any()); + Message message = messageCaptor.getValue(); + + assertThat(message.getParts()).hasSize(1); + io.a2a.spec.Part part = message.getParts().get(0); + assertThat(part).isInstanceOf(DataPart.class); + DataPart dataPart = (DataPart) part; + assertThat(dataPart.getData().get("name")).isEqualTo("fn"); + assertThat(dataPart.getData().get("id")).isEqualTo("call-1"); + assertThat(dataPart.getMetadata().get("adk_type")).isEqualTo("function_response"); + } + + @Test + public void runAsync_invokesBeforeAndAfterCallbacks() { + AtomicBoolean beforeCalled = new AtomicBoolean(false); + AtomicBoolean afterCalled = new AtomicBoolean(false); + RemoteA2AAgent agent = + getAgentBuilder() + .beforeAgentCallback( + ImmutableList.of( + (CallbackContext unused) -> { + beforeCalled.set(true); + return Maybe.empty(); + })) + .afterAgentCallback( + ImmutableList.of( + (CallbackContext unused) -> { + afterCalled.set(true); + return Maybe.empty(); + })) + .build(); + mockStreamResponse( + consumer -> { + consumer.accept(createFinalEvent("done"), agentCard); + }); + + var unused = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(beforeCalled.get()).isTrue(); + assertThat(afterCalled.get()).isTrue(); + } + + @Test + public void runAsync_aggregatesCodeExecution() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + consumer.accept(createPartialCodeEvent("print('hello')", "java"), agentCard); + consumer.accept(createPartialCodeResultEvent("hello\n", "ok"), agentCard); + consumer.accept(createFinalEvent("Done"), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(3); + Part codePart = events.get(0).content().get().parts().get().get(0); + assertThat(codePart.executableCode()).isPresent(); + assertThat(codePart.executableCode().get().code()).hasValue("print('hello')"); + assertThat(codePart.executableCode().get().language().get().toString()).isEqualTo("java"); + Part resultPart = events.get(1).content().get().parts().get().get(0); + assertThat(resultPart.codeExecutionResult()).isPresent(); + assertThat(resultPart.codeExecutionResult().get().output()).hasValue("hello\n"); + assertText(events.get(2), "Done"); + assertRequestMetadata(events.get(2)); + assertResponseMetadata(events.get(2)); + } + + @Test + public void runAsync_aggregatesCodeExecution_defaultsToEmptyLanguage() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + Map data = new HashMap<>(); + data.put("code", "print('hello')"); + Map metadata = new HashMap<>(); + metadata.put("adk_type", "executable_code"); + consumer.accept( + createTestEvent(new DataPart(data, metadata), TaskState.WORKING, true, false), + agentCard); + consumer.accept(createFinalEvent("Done"), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(2); + Part codePart = events.get(0).content().get().parts().get().get(0); + assertThat(codePart.executableCode()).isPresent(); + assertThat(codePart.executableCode().get().code()).hasValue("print('hello')"); + assertThat(codePart.executableCode().get().language().get().toString()) + .isEqualTo("LANGUAGE_UNSPECIFIED"); + } + + @Test + public void runAsync_aggregatesCodeExecutionResult_withOnlyMetadata() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + Map data = new HashMap<>(); + Map metadata = new HashMap<>(); + metadata.put("adk_type", "code_execution_result"); + consumer.accept( + createTestEvent(new DataPart(data, metadata), TaskState.WORKING, true, false), + agentCard); + consumer.accept(createFinalEvent("Done"), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(2); + Part resultPart = events.get(0).content().get().parts().get().get(0); + assertThat(resultPart.codeExecutionResult()).isPresent(); + assertThat(resultPart.codeExecutionResult().get().outcome().get().toString()) + .isEqualTo("OUTCOME_OK"); + assertThat(resultPart.codeExecutionResult().get().output()).hasValue(""); + } + + @Test + public void runAsync_aggregatesPartialEvents_emptyFinalEvent() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + consumer.accept(createPartialEvent("Hello ", true, false), agentCard); + consumer.accept(createPartialEvent("World!", true, false), agentCard); + Task task = + new Task.Builder() + .id("task-1") + .contextId("context-1") + .status(new TaskStatus(TaskState.COMPLETED)) + .build(); + consumer.accept(new TaskEvent(task), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(3); + assertText(events.get(0), "Hello "); + assertNotAggregated(events.get(0)); + assertText(events.get(1), "World!"); + assertNotAggregated(events.get(1)); + Event finalEvent = events.get(2); + assertText(finalEvent, "Hello World!"); + assertAggregated(finalEvent); + } + + @Test + public void runAsync_aggregatesPartialButNotNonPartialEvents() { + RemoteA2AAgent agent = createAgent(); + mockStreamResponse( + consumer -> { + consumer.accept(createPartialEvent("1", true, false), agentCard); + consumer.accept(createPartialEvent("2", true, false), agentCard); + consumer.accept(createPartialEvent("3", false, false), agentCard); + consumer.accept(createPartialEvent("4", true, false), agentCard); + consumer.accept(createFinalEvent("5"), agentCard); + }); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(6); + assertText(events.get(0), "1"); + assertRequestMetadata(events.get(0)); + assertResponseMetadata(events.get(0)); + assertNotAggregated(events.get(0)); + assertText(events.get(1), "2"); + assertRequestMetadata(events.get(1)); + assertResponseMetadata(events.get(1)); + assertNotAggregated(events.get(1)); + assertText(events.get(2), "3"); + assertRequestMetadata(events.get(2)); + assertResponseMetadata(events.get(2)); + assertNotAggregated(events.get(2)); + assertText(events.get(3), "4"); + assertRequestMetadata(events.get(3)); + assertResponseMetadata(events.get(3)); + assertNotAggregated(events.get(3)); + assertText(events.get(4), "34"); + assertRequestMetadata(events.get(4)); + // Aggregated events do not carry response metadata + assertAggregated(events.get(4)); + assertText(events.get(5), "5"); + assertRequestMetadata(events.get(5)); + assertResponseMetadata(events.get(5)); + assertNotAggregated(events.get(5)); + } + + @Test + public void runAsync_beforeCallbackCanShortCircuit() { + Content shortCircuitContent = + Content.builder() + .role("model") + .parts(ImmutableList.of(Part.builder().text("short circuit").build())) + .build(); + RemoteA2AAgent agent = + getAgentBuilder() + .beforeAgentCallback( + ImmutableList.of( + (CallbackContext unused) -> Maybe.just(shortCircuitContent))) + .build(); + + List events = agent.runAsync(invocationContext).toList().blockingGet(); + + assertThat(events).hasSize(1); + assertText(events.get(0), "short circuit"); + verifyNoInteractions(mockClient); + } + + @Test + public void runAsync_handlesClientError() { + RemoteA2AAgent agent = createAgent(); + mockStreamError(new RuntimeException("Connection failed")); + + agent + .runAsync(invocationContext) + .test() + .awaitDone(5, SECONDS) + .assertError(RuntimeException.class) + .assertError( + e -> e.getCause() != null && e.getCause().getMessage().contains("Connection failed")); + } + + private ClientEvent createPartialEvent(String text, boolean append, boolean lastChunk) { + return createTestEvent(new TextPart(text), TaskState.WORKING, append, lastChunk); + } + + private ClientEvent createPartialFunctionCallEvent(String name, String id) { + Map data = new HashMap<>(); + data.put("name", name); + data.put("id", id); + data.put("args", new HashMap<>()); + Map metadata = new HashMap<>(); + metadata.put("adk_type", "function_call"); + + return createTestEvent(new DataPart(data, metadata), TaskState.WORKING, true, false); + } + + private ClientEvent createPartialCodeEvent(String code, String language) { + Map data = new HashMap<>(); + data.put("code", code); + data.put("language", language); + Map metadata = new HashMap<>(); + metadata.put("adk_type", "executable_code"); + + return createTestEvent(new DataPart(data, metadata), TaskState.WORKING, true, false); + } + + private ClientEvent createPartialCodeResultEvent(String output, String outcome) { + Map data = new HashMap<>(); + data.put("output", output); + data.put("outcome", outcome); + Map metadata = new HashMap<>(); + metadata.put("adk_type", "code_execution_result"); + + return createTestEvent(new DataPart(data, metadata), TaskState.WORKING, true, false); + } + + private ClientEvent createPartialFileEvent(String uri, String mimeType) { + return createTestEvent( + new FilePart(new FileWithUri(mimeType, "file", uri)), TaskState.WORKING, true, false); + } + + private ClientEvent createFinalEvent(String text) { + return createTestEvent(new TextPart(text), TaskState.COMPLETED, false, false); + } + + private ClientEvent createTestEvent( + io.a2a.spec.Part part, TaskState state, boolean append, boolean lastChunk) { + Artifact artifact = + new Artifact.Builder().artifactId("artifact-1").parts(ImmutableList.of(part)).build(); + Task task = + new Task.Builder() + .id("task-1") + .contextId("context-1") + .status(new TaskStatus(state)) + .artifacts(ImmutableList.of(artifact)) + .build(); + + if (state == TaskState.COMPLETED && !append && !lastChunk) { + return new TaskEvent(task); + } + + TaskArtifactUpdateEvent updateEvent = + new TaskArtifactUpdateEvent.Builder() + .lastChunk(lastChunk) + .append(append) + .contextId("context-1") + .artifact(artifact) + .taskId("task-id-1") + .build(); + return new TaskUpdateEvent(task, updateEvent); + } + + private RemoteA2AAgent.Builder getAgentBuilder() { + return RemoteA2AAgent.builder().name("remote-agent").a2aClient(mockClient).agentCard(agentCard); + } + + private RemoteA2AAgent createAgent() { + return getAgentBuilder().build(); + } + + @SuppressWarnings("unchecked") // cast for Mockito + private void mockStreamResponse(Consumer> responseProducer) { + doAnswer( + invocation -> { + List> consumers = invocation.getArgument(1); + BiConsumer consumer = consumers.get(0); + responseProducer.accept(consumer); + return null; + }) + .when(mockClient) + .sendMessage(any(Message.class), any(List.class), any(Consumer.class), any()); + } + + @SuppressWarnings("unchecked") // cast for Mockito + private void mockStreamError(Throwable error) { + doAnswer( + invocation -> { + Consumer errorConsumer = invocation.getArgument(2); + errorConsumer.accept(error); + return null; + }) + .when(mockClient) + .sendMessage(any(Message.class), any(List.class), any(Consumer.class), any()); + } + + private void assertText(Event event, String expectedText) { + assertText(event, 0, expectedText); + } + + private void assertText(Event event, int partIndex, String expectedText) { + assertThat(event.content().get().parts().get().get(partIndex).text().orElse("")) + .isEqualTo(expectedText); + } + + private void assertThought(Event event, boolean expected) { + assertThat(event.content().get().parts().get().get(0).thought().orElse(false)) + .isEqualTo(expected); + } + + private void assertAggregated(Event event) { + assertThat(event.customMetadata()).isPresent(); + List metadata = event.customMetadata().get(); + + boolean hasAggregated = + metadata.stream() + .anyMatch( + m -> + A2AMetadata.Key.AGGREGATED.getValue().equals(m.key().orElse("")) + && Objects.equals(m.stringValue().orElse(""), "true")); + boolean hasRequest = + metadata.stream() + .anyMatch(m -> A2AMetadata.Key.REQUEST.getValue().equals(m.key().orElse(""))); + + assertThat(hasAggregated).isTrue(); + assertThat(hasRequest).isTrue(); + } + + private void assertNotAggregated(Event event) { + if (event.customMetadata().isEmpty()) { + return; + } + List metadata = event.customMetadata().get(); + boolean hasAggregated = + metadata.stream() + .anyMatch( + m -> + A2AMetadata.Key.AGGREGATED.getValue().equals(m.key().orElse("")) + && Objects.equals(m.stringValue().orElse(""), "true")); + assertThat(hasAggregated).isFalse(); + } + + private void assertRequestMetadata(Event event) { + assertThat(event.customMetadata()).isPresent(); + List metadata = event.customMetadata().get(); + boolean hasRequest = + metadata.stream() + .anyMatch(m -> A2AMetadata.Key.REQUEST.getValue().equals(m.key().orElse(""))); + assertThat(hasRequest).isTrue(); + } + + private void assertResponseMetadata(Event event) { + assertThat(event.customMetadata()).isPresent(); + List metadata = event.customMetadata().get(); + boolean hasResponse = + metadata.stream() + .anyMatch(m -> A2AMetadata.Key.RESPONSE.getValue().equals(m.key().orElse(""))); + assertThat(hasResponse).isTrue(); + } + + private static final class TestAgent extends BaseAgent { + TestAgent() { + super("test_agent", "test", ImmutableList.of(), null, null); + } + + @Override + protected Flowable runAsyncImpl(InvocationContext invocationContext) { + return Flowable.empty(); + } + + @Override + protected Flowable runLiveImpl(InvocationContext invocationContext) { + return Flowable.empty(); + } + } +} diff --git a/a2a/src/test/java/com/google/adk/a2a/converters/EventConverterTest.java b/a2a/src/test/java/com/google/adk/a2a/converters/EventConverterTest.java index f9d34bf3f..8d460c457 100644 --- a/a2a/src/test/java/com/google/adk/a2a/converters/EventConverterTest.java +++ b/a2a/src/test/java/com/google/adk/a2a/converters/EventConverterTest.java @@ -92,6 +92,8 @@ public void convertEventsToA2AMessage_preservesFunctionCallAndResponseParts() { .invocationId("invocation-1") .agent(new TestAgent()) .session(session) + .userContent( + Content.builder().role("user").parts(ImmutableList.of(userTextPart)).build()) .endInvocation(false) .build(); @@ -101,24 +103,28 @@ public void convertEventsToA2AMessage_preservesFunctionCallAndResponseParts() { // Assert assertThat(maybeMessage).isPresent(); Message message = maybeMessage.get(); - assertThat(message.getParts()).hasSize(3); + assertThat(message.getParts()).hasSize(4); assertThat(message.getParts().get(0)).isInstanceOf(TextPart.class); assertThat(message.getParts().get(1)).isInstanceOf(DataPart.class); assertThat(message.getParts().get(2)).isInstanceOf(DataPart.class); + assertThat(message.getParts().get(3)).isInstanceOf(TextPart.class); DataPart callDataPart = (DataPart) message.getParts().get(1); assertThat(callDataPart.getMetadata().get(PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY)) - .isEqualTo(PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL); + .isEqualTo(A2ADataPartMetadataType.FUNCTION_CALL.getType()); assertThat(callDataPart.getData()).containsEntry("name", "roll_die"); assertThat(callDataPart.getData()).containsEntry("id", "adk-call-1"); assertThat(callDataPart.getData()).containsEntry("args", ImmutableMap.of("sides", 6)); DataPart responseDataPart = (DataPart) message.getParts().get(2); assertThat(responseDataPart.getMetadata().get(PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY)) - .isEqualTo(PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE); + .isEqualTo(A2ADataPartMetadataType.FUNCTION_RESPONSE.getType()); assertThat(responseDataPart.getData()).containsEntry("name", "roll_die"); assertThat(responseDataPart.getData()).containsEntry("id", "adk-call-1"); assertThat(responseDataPart.getData()).containsEntry("response", ImmutableMap.of("result", 3)); + + TextPart lastTextPart = (TextPart) message.getParts().get(3); + assertThat(lastTextPart.getText()).isEqualTo("Roll a die"); } private static final class TestAgent extends BaseAgent { diff --git a/a2a/src/test/java/com/google/adk/a2a/converters/PartConverterTest.java b/a2a/src/test/java/com/google/adk/a2a/converters/PartConverterTest.java index 6ccfd9566..8e8982ffa 100644 --- a/a2a/src/test/java/com/google/adk/a2a/converters/PartConverterTest.java +++ b/a2a/src/test/java/com/google/adk/a2a/converters/PartConverterTest.java @@ -2,7 +2,9 @@ import static com.google.common.truth.Truth.assertThat; import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.Assert.assertThrows; +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; @@ -89,7 +91,7 @@ public void toGenaiPart_withDataPartFunctionCall_returnsGenaiFunctionCallPart() data, ImmutableMap.of( PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, - PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL)); + A2ADataPartMetadataType.FUNCTION_CALL.getType())); Optional result = PartConverter.toGenaiPart(dataPart); @@ -126,7 +128,7 @@ public void toGenaiPart_withDataPartFunctionResponse_returnsGenaiFunctionRespons data, ImmutableMap.of( PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, - PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE)); + A2ADataPartMetadataType.FUNCTION_RESPONSE.getType())); Optional result = PartConverter.toGenaiPart(dataPart); @@ -181,78 +183,21 @@ public void toGenaiParts_convertsAllSupportedParts() { } @Test - public void convertGenaiPartToA2aPart_withNullPart_returnsEmpty() { - assertThat(PartConverter.convertGenaiPartToA2aPart(null)).isEmpty(); - } - - @Test - public void convertGenaiPartToA2aPart_withTextPart_returnsEmpty() { - Part part = Part.builder().text("text").build(); - assertThat(PartConverter.convertGenaiPartToA2aPart(part)).isEmpty(); - } - - @Test - public void convertGenaiPartToA2aPart_withFunctionCallPart_returnsDataPart() { - Part part = - Part.builder() - .functionCall( - FunctionCall.builder() - .name("func") - .id("1") - .args(ImmutableMap.of("param", "value")) - .build()) - .build(); - - Optional result = PartConverter.convertGenaiPartToA2aPart(part); - - assertThat(result).isPresent(); - DataPart dataPart = result.get(); - assertThat(dataPart.getData()) - .containsExactly("name", "func", "id", "1", "args", ImmutableMap.of("param", "value")); - assertThat(dataPart.getMetadata()) - .containsEntry( - PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, - PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL); - } - - @Test - public void convertGenaiPartToA2aPart_withFunctionResponsePart_returnsDataPart() { - Part part = - Part.builder() - .functionResponse( - FunctionResponse.builder() - .name("func") - .id("1") - .response(ImmutableMap.of("result", "value")) - .build()) - .build(); - - Optional result = PartConverter.convertGenaiPartToA2aPart(part); - - assertThat(result).isPresent(); - DataPart dataPart = result.get(); - assertThat(dataPart.getData()) - .containsExactly("name", "func", "id", "1", "response", ImmutableMap.of("result", "value")); - assertThat(dataPart.getMetadata()) - .containsEntry( - PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, - PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE); - } - - @Test - public void fromGenaiPart_withNullPart_returnsEmpty() { - assertThat(PartConverter.fromGenaiPart(null)).isEmpty(); + public void fromGenaiPart_withNullPart_throwsException() { + assertThrows(GenAiFieldMissingException.class, () -> PartConverter.fromGenaiPart(null, false)); } @Test public void fromGenaiPart_withTextPart_returnsTextPart() { - Part part = Part.builder().text("text").build(); + Part part = Part.builder().text("text").thought(true).build(); - Optional> result = PartConverter.fromGenaiPart(part); + io.a2a.spec.Part result = PartConverter.fromGenaiPart(part, true); - assertThat(result).isPresent(); - assertThat(result.get()).isInstanceOf(TextPart.class); - assertThat(((TextPart) result.get()).getText()).isEqualTo("text"); + assertThat(result).isInstanceOf(TextPart.class); + assertThat(((TextPart) result).getText()).isEqualTo("text"); + assertThat(((TextPart) result).getMetadata()).containsEntry("thought", true); + assertThat(((TextPart) result).getMetadata()) + .containsEntry(PartConverter.A2A_DATA_PART_METADATA_IS_PARTIAL_KEY, true); } @Test @@ -262,11 +207,10 @@ public void fromGenaiPart_withFileDataPart_returnsFilePartWithUri() { .fileData(FileData.builder().mimeType("text/plain").fileUri("http://file.txt").build()) .build(); - Optional> result = PartConverter.fromGenaiPart(part); + io.a2a.spec.Part result = PartConverter.fromGenaiPart(part, false); - assertThat(result).isPresent(); - assertThat(result.get()).isInstanceOf(FilePart.class); - FilePart filePart = (FilePart) result.get(); + assertThat(result).isInstanceOf(FilePart.class); + FilePart filePart = (FilePart) result; assertThat(filePart.getFile()).isInstanceOf(FileWithUri.class); FileWithUri fileWithUri = (FileWithUri) filePart.getFile(); assertThat(fileWithUri.mimeType()).isEqualTo("text/plain"); @@ -281,11 +225,10 @@ public void fromGenaiPart_withInlineDataPart_returnsFilePartWithBytes() { .inlineData(Blob.builder().mimeType("text/plain").data(bytes).build()) .build(); - Optional> result = PartConverter.fromGenaiPart(part); + io.a2a.spec.Part result = PartConverter.fromGenaiPart(part, false); - assertThat(result).isPresent(); - assertThat(result.get()).isInstanceOf(FilePart.class); - FilePart filePart = (FilePart) result.get(); + assertThat(result).isInstanceOf(FilePart.class); + FilePart filePart = (FilePart) result; assertThat(filePart.getFile()).isInstanceOf(FileWithBytes.class); FileWithBytes fileWithBytes = (FileWithBytes) filePart.getFile(); assertThat(fileWithBytes.mimeType()).isEqualTo("text/plain"); @@ -297,20 +240,32 @@ public void fromGenaiPart_withFunctionCallPart_returnsDataPart() { Part part = Part.builder() .functionCall( - FunctionCall.builder().name("func").id("1").args(ImmutableMap.of()).build()) + FunctionCall.builder() + .name("func") + .id("1") + .willContinue(true) + .args(ImmutableMap.of()) + .build()) .build(); - Optional> result = PartConverter.fromGenaiPart(part); + io.a2a.spec.Part result = PartConverter.fromGenaiPart(part, false); - assertThat(result).isPresent(); - assertThat(result.get()).isInstanceOf(DataPart.class); - DataPart dataPart = (DataPart) result.get(); + assertThat(result).isInstanceOf(DataPart.class); + DataPart dataPart = (DataPart) result; assertThat(dataPart.getData()) - .containsExactly("name", "func", "id", "1", "args", ImmutableMap.of()); + .containsExactly( + "name", + "func", + "id", + "1", + "args", + ImmutableMap.of(), + PartConverter.WILL_CONTINUE_KEY, + true); assertThat(dataPart.getMetadata()) .containsEntry( PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, - PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_CALL); + A2ADataPartMetadataType.FUNCTION_CALL.getType()); } @Test @@ -321,17 +276,16 @@ public void fromGenaiPart_withFunctionResponsePart_returnsDataPart() { FunctionResponse.builder().name("func").id("1").response(ImmutableMap.of()).build()) .build(); - Optional> result = PartConverter.fromGenaiPart(part); + io.a2a.spec.Part result = PartConverter.fromGenaiPart(part, false); - assertThat(result).isPresent(); - assertThat(result.get()).isInstanceOf(DataPart.class); - DataPart dataPart = (DataPart) result.get(); + assertThat(result).isInstanceOf(DataPart.class); + DataPart dataPart = (DataPart) result; assertThat(dataPart.getData()) .containsExactly("name", "func", "id", "1", "response", ImmutableMap.of()); assertThat(dataPart.getMetadata()) .containsEntry( PartConverter.A2A_DATA_PART_METADATA_TYPE_KEY, - PartConverter.A2A_DATA_PART_METADATA_TYPE_FUNCTION_RESPONSE); + A2ADataPartMetadataType.FUNCTION_RESPONSE.getType()); } @Test diff --git a/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java b/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java index 1a4873a85..5378bdd7b 100644 --- a/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java +++ b/a2a/src/test/java/com/google/adk/a2a/converters/ResponseConverterTest.java @@ -12,7 +12,6 @@ import com.google.adk.sessions.Session; import com.google.common.collect.ImmutableList; import com.google.genai.types.Content; -import com.google.genai.types.Part; import io.a2a.client.MessageEvent; import io.a2a.client.TaskUpdateEvent; import io.a2a.spec.Artifact; @@ -25,7 +24,6 @@ import io.a2a.spec.TextPart; import io.reactivex.rxjava3.core.Flowable; import java.util.Optional; -import java.util.UUID; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -62,122 +60,8 @@ private Task.Builder testTask() { return new Task.Builder().id("task-1").contextId("context-1"); } - @Test - public void eventsToMessage_withNullEvents_returnsEmptyAgentMessage() { - Message message = ResponseConverter.eventsToMessage(null, "context-1", "task-1"); - assertThat(message.getContextId()).isEqualTo("context-1"); - assertThat(message.getRole()).isEqualTo(Message.Role.AGENT); - assertThat(message.getParts()).hasSize(1); - assertThat(((TextPart) message.getParts().get(0)).getText()).isEmpty(); - } - - @Test - public void eventsToMessage_withEmptyEvents_returnsEmptyAgentMessage() { - Message message = ResponseConverter.eventsToMessage(ImmutableList.of(), "context-1", "task-1"); - assertThat(message.getContextId()).isEqualTo("context-1"); - assertThat(message.getRole()).isEqualTo(Message.Role.AGENT); - assertThat(message.getParts()).hasSize(1); - assertThat(((TextPart) message.getParts().get(0)).getText()).isEmpty(); - } - - @Test - public void eventsToMessage_withSingleEvent_returnsMessage() { - Event event = - Event.builder() - .id(UUID.randomUUID().toString()) - .author("user") - .content( - Content.builder() - .role("user") - .parts(ImmutableList.of(Part.builder().text("Hello").build())) - .build()) - .build(); - - Message message = - ResponseConverter.eventsToMessage(ImmutableList.of(event), "context-1", "task-1"); - - assertThat(message.getContextId()).isEqualTo("context-1"); - assertThat(message.getRole()).isEqualTo(Message.Role.USER); - assertThat(message.getParts()).hasSize(1); - assertThat(((TextPart) message.getParts().get(0)).getText()).isEqualTo("Hello"); - } - - @Test - public void eventsToMessage_withMultipleEvents_returnsAggregatedMessage() { - Event event1 = - Event.builder() - .id(UUID.randomUUID().toString()) - .author("agent") - .content( - Content.builder() - .role("model") - .parts(ImmutableList.of(Part.builder().text("Hello ").build())) - .build()) - .build(); - Event event2 = - Event.builder() - .id(UUID.randomUUID().toString()) - .author("agent") - .content( - Content.builder() - .role("model") - .parts(ImmutableList.of(Part.builder().text("World").build())) - .build()) - .build(); - - Message message = - ResponseConverter.eventsToMessage(ImmutableList.of(event1, event2), "context-1", "task-1"); - - assertThat(message.getMessageId()).isEqualTo("task-1"); - assertThat(message.getContextId()).isEqualTo("context-1"); - assertThat(message.getRole()).isEqualTo(Message.Role.AGENT); - assertThat(message.getParts()).hasSize(2); - assertThat(((TextPart) message.getParts().get(0)).getText()).isEqualTo("Hello "); - assertThat(((TextPart) message.getParts().get(1)).getText()).isEqualTo("World"); - } - - @Test - public void eventToMessage_convertsUserEvent() { - Event event = - Event.builder() - .id("event-1") - .author("user") - .content( - Content.builder() - .role("user") - .parts(ImmutableList.of(Part.builder().text("Test").build())) - .build()) - .build(); - - Message message = ResponseConverter.eventToMessage(event, "context-1"); - - assertThat(message.getMessageId()).isEqualTo("event-1"); - assertThat(message.getContextId()).isEqualTo("context-1"); - assertThat(message.getRole()).isEqualTo(Message.Role.USER); - assertThat(message.getParts()).hasSize(1); - assertThat(((TextPart) message.getParts().get(0)).getText()).isEqualTo("Test"); - } - - @Test - public void eventToMessage_convertsAgentEvent() { - Event event = - Event.builder() - .id("event-1") - .author("agent") - .content( - Content.builder() - .role("model") - .parts(ImmutableList.of(Part.builder().text("Test").build())) - .build()) - .build(); - - Message message = ResponseConverter.eventToMessage(event, "context-1"); - - assertThat(message.getMessageId()).isEqualTo("event-1"); - assertThat(message.getContextId()).isEqualTo("context-1"); - assertThat(message.getRole()).isEqualTo(Message.Role.AGENT); - assertThat(message.getParts()).hasSize(1); - assertThat(((TextPart) message.getParts().get(0)).getText()).isEqualTo("Test"); + private static TaskStatusUpdateEvent.Builder testTaskStatusUpdateEvent() { + return new TaskStatusUpdateEvent.Builder().taskId("task-1").contextId("context-1"); } @Test @@ -308,7 +192,8 @@ public void clientEventToEvent_withTaskArtifactUpdateEvent_withLastChunkTrue_ret } @Test - public void clientEventToEvent_withTaskArtifactUpdateEvent_withLastChunkFalse_returnsNull() { + public void + clientEventToEvent_withTaskArtifactUpdateEvent_withLastChunkFalse_returnsHandlingPartialEvent() { io.a2a.spec.Part a2aPart = new TextPart("Artifact content"); Artifact artifact = new Artifact.Builder().artifactId("artifact-1").parts(ImmutableList.of(a2aPart)).build(); @@ -320,16 +205,85 @@ public void clientEventToEvent_withTaskArtifactUpdateEvent_withLastChunkFalse_re TaskArtifactUpdateEvent updateEvent = new TaskArtifactUpdateEvent.Builder() .lastChunk(false) + .append(false) .contextId("context-1") .artifact(artifact) .taskId("task-id-1") .build(); TaskUpdateEvent event = new TaskUpdateEvent(task, updateEvent); + Optional optionalEvent = ResponseConverter.clientEventToEvent(event, invocationContext); + assertThat(optionalEvent).isPresent(); + Event resultEvent = optionalEvent.get(); + assertThat(resultEvent.partial().orElse(false)).isTrue(); + } + + @Test + public void clientEventToEvent_withFinalTaskStatusUpdateEvent_withMessage_returnsEvent() { + Message statusMessage = + new Message.Builder() + .role(Message.Role.AGENT) + .parts(ImmutableList.of(new TextPart("Final status message"))) + .build(); + TaskStatus status = new TaskStatus(TaskState.COMPLETED, statusMessage, null); + TaskStatusUpdateEvent updateEvent = + testTaskStatusUpdateEvent().isFinal(true).status(status).build(); + + TaskUpdateEvent event = new TaskUpdateEvent(testTask().status(status).build(), updateEvent); + + Optional optionalEvent = ResponseConverter.clientEventToEvent(event, invocationContext); + assertThat(optionalEvent).isPresent(); + Event resultEvent = optionalEvent.get(); + assertThat(resultEvent.content().get().parts().get().get(0).text()) + .hasValue("Final status message"); + assertThat(resultEvent.content().get().parts().get().get(0).thought()).hasValue(false); + assertThat(resultEvent.partial().orElse(false)).isFalse(); + assertThat(resultEvent.turnComplete()).hasValue(true); + } + + @Test + public void clientEventToEvent_withFinalTaskStatusUpdateEvent_withoutMessage_returnsEvent() { + TaskStatus status = new TaskStatus(TaskState.COMPLETED, null, null); + TaskStatusUpdateEvent updateEvent = + new TaskStatusUpdateEvent("task-id-1", status, "context-1", true, null); + TaskUpdateEvent event = new TaskUpdateEvent(testTask().status(status).build(), updateEvent); + + Optional optionalEvent = ResponseConverter.clientEventToEvent(event, invocationContext); + assertThat(optionalEvent).isPresent(); + Event resultEvent = optionalEvent.get(); + assertThat(resultEvent.turnComplete()).hasValue(true); + } + + @Test + public void clientEventToEvent_withNonFinalTaskStatusUpdateEvent_withoutMessage_returnsEmpty() { + TaskStatus status = new TaskStatus(TaskState.WORKING, null, null); + TaskStatusUpdateEvent updateEvent = + new TaskStatusUpdateEvent("task-id-1", status, "context-1", false, null); + TaskUpdateEvent event = new TaskUpdateEvent(testTask().status(status).build(), updateEvent); + Optional optionalEvent = ResponseConverter.clientEventToEvent(event, invocationContext); assertThat(optionalEvent).isEmpty(); } + @Test + public void clientEventToEvent_withFailedTaskStatusUpdateEvent_returnsErrorEvent() { + Message statusMessage = + new Message.Builder() + .role(Message.Role.AGENT) + .parts(ImmutableList.of(new TextPart("Task failed"))) + .build(); + TaskStatus status = new TaskStatus(TaskState.FAILED, statusMessage, null); + TaskStatusUpdateEvent updateEvent = + new TaskStatusUpdateEvent("task-id-1", status, "context-1", true, null); + TaskUpdateEvent event = new TaskUpdateEvent(testTask().status(status).build(), updateEvent); + + Optional optionalEvent = ResponseConverter.clientEventToEvent(event, invocationContext); + assertThat(optionalEvent).isPresent(); + Event resultEvent = optionalEvent.get(); + assertThat(resultEvent.errorMessage()).hasValue("Task failed"); + assertThat(resultEvent.turnComplete()).hasValue(true); + } + private static final class TestAgent extends BaseAgent { TestAgent() { super("test_agent", "test", ImmutableList.of(), null, null); diff --git a/a2a/src/test/java/com/google/adk/a2a/executor/AgentExecutorTest.java b/a2a/src/test/java/com/google/adk/a2a/executor/AgentExecutorTest.java new file mode 100644 index 000000000..647aaf21f --- /dev/null +++ b/a2a/src/test/java/com/google/adk/a2a/executor/AgentExecutorTest.java @@ -0,0 +1,469 @@ +package com.google.adk.a2a.executor; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.adk.agents.BaseAgent; +import com.google.adk.agents.InvocationContext; +import com.google.adk.apps.App; +import com.google.adk.artifacts.InMemoryArtifactService; +import com.google.adk.events.Event; +import com.google.adk.sessions.InMemorySessionService; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import com.google.genai.types.Content; +import com.google.genai.types.Part; +import io.a2a.server.agentexecution.RequestContext; +import io.a2a.server.events.EventQueue; +import io.a2a.spec.Message; +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.Flowable; +import io.reactivex.rxjava3.core.Maybe; +import io.reactivex.rxjava3.core.Single; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.ArgumentCaptor; + +@RunWith(JUnit4.class) +public final class AgentExecutorTest { + + private EventQueue eventQueue; + private List enqueuedEvents; + private TestAgent testAgent; + + @Before + public void setUp() { + enqueuedEvents = new ArrayList<>(); + eventQueue = mock(EventQueue.class); + doAnswer( + invocation -> { + enqueuedEvents.add(invocation.getArgument(0)); + return null; + }) + .when(eventQueue) + .enqueueEvent(any()); + testAgent = new TestAgent(); + } + + @Test + public void createAgentExecutor_noAgent_succeeds() { + var unused = + new AgentExecutor.Builder() + .app(App.builder().name("test_app").rootAgent(testAgent).build()) + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .agentExecutorConfig(AgentExecutorConfig.builder().build()) + .build(); + } + + @Test + public void createAgentExecutor_withAgentAndApp_throwsException() { + assertThrows( + IllegalStateException.class, + () -> { + new AgentExecutor.Builder() + .agent(testAgent) + .app(App.builder().name("test_app").rootAgent(testAgent).build()) + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .agentExecutorConfig(AgentExecutorConfig.builder().build()) + .build(); + }); + } + + @Test + public void createAgentExecutor_withEmptyAgentAndApp_throwsException() { + assertThrows( + IllegalStateException.class, + () -> { + new AgentExecutor.Builder() + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .agentExecutorConfig(AgentExecutorConfig.builder().build()) + .build(); + }); + } + + @Test + public void createAgentExecutor_noAgentExecutorConfig_throwsException() { + assertThrows( + NullPointerException.class, + () -> { + new AgentExecutor.Builder() + .app(App.builder().name("test_app").rootAgent(testAgent).build()) + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .build(); + }); + } + + @Test + public void execute_withBeforeExecuteCallback_cancelsExecutionOnError() { + // If callback returns error, execution should stop/fail. + Callbacks.BeforeExecuteCallback callback = + ctx -> Single.error(new RuntimeException("Cancelled")); + + AgentExecutorConfig config = + AgentExecutorConfig.builder().beforeExecuteCallback(callback).build(); + + AgentExecutor executor = + new AgentExecutor.Builder() + .agentExecutorConfig(config) + .app(App.builder().name("test_app").rootAgent(testAgent).build()) + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .build(); + + RequestContext ctx = createRequestContext(); + executor.execute(ctx, eventQueue); + + // Verify error handling triggered cleanup and fail event + // The executor catches the error and emits failed event. + assertThat(enqueuedEvents).isNotEmpty(); + Object lastEvent = Iterables.getLast(enqueuedEvents); + assertThat(lastEvent).isInstanceOf(TaskStatusUpdateEvent.class); + TaskStatusUpdateEvent statusEvent = (TaskStatusUpdateEvent) lastEvent; + assertThat(statusEvent.getStatus().state().toString()).isEqualTo("FAILED"); + assertThat(statusEvent.getStatus().message().getParts().get(0)).isInstanceOf(TextPart.class); + TextPart textPart = (TextPart) statusEvent.getStatus().message().getParts().get(0); + assertThat(textPart.getText()).contains("Cancelled"); + } + + @Test + public void execute_withBeforeExecuteCallback_skipsExecutionIfTrue() { + Callbacks.BeforeExecuteCallback callback = ctx -> Single.just(true); + + AgentExecutorConfig config = + AgentExecutorConfig.builder().beforeExecuteCallback(callback).build(); + + AgentExecutor executor = + new AgentExecutor.Builder() + .agentExecutorConfig(config) + .app(App.builder().name("test_app").rootAgent(testAgent).build()) + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .build(); + + RequestContext ctx = createRequestContext(); + executor.execute(ctx, eventQueue); + + // Filter for artifact events + Optional artifactEvent = + enqueuedEvents.stream() + .filter(e -> e instanceof TaskArtifactUpdateEvent) + .map(e -> (TaskArtifactUpdateEvent) e) + .findFirst(); + + assertThat(artifactEvent).isEmpty(); + } + + @Test + public void execute_withAfterEventCallback_modifiesEvent() { + // Agent emits an event. Callback intercepts and modifies it. + Part textPart = Part.builder().text("Hello world").build(); + Event agentEvent = + Event.builder() + .id("event-1") + .author("agent") + .content(Content.builder().role("model").parts(ImmutableList.of(textPart)).build()) + .build(); + testAgent.setEventsToEmit(Flowable.just(agentEvent)); + + Callbacks.AfterEventCallback callback = + (ctx, event, sourceEvent) -> { + // Modify event by adding metadata + return Maybe.just( + new TaskArtifactUpdateEvent.Builder(event) + .metadata(ImmutableMap.of("modified", true)) + .build()); + }; + + AgentExecutorConfig config = AgentExecutorConfig.builder().afterEventCallback(callback).build(); + + AgentExecutor executor = + new AgentExecutor.Builder() + .agentExecutorConfig(config) + .app(App.builder().name("test_app").rootAgent(testAgent).build()) + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .build(); + + RequestContext ctx = createRequestContext(); + executor.execute(ctx, eventQueue); + + // Filter for artifact events + Optional artifactEvent = + enqueuedEvents.stream() + .filter(e -> e instanceof TaskArtifactUpdateEvent) + .map(e -> (TaskArtifactUpdateEvent) e) + .findFirst(); + + assertThat(artifactEvent).isPresent(); + assertThat(artifactEvent.get().getMetadata()).containsEntry("modified", true); + } + + @Test + public void execute_withAfterExecuteCallback_modifiesStatus() { + testAgent.setEventsToEmit(Flowable.empty()); // Just complete + + Callbacks.AfterExecuteCallback callback = + (ctx, event) -> { + // Modify status to have different message + Message newMessage = + new Message.Builder() + .messageId(UUID.randomUUID().toString()) + .role(Message.Role.AGENT) + .parts(ImmutableList.of(new TextPart("Modified completion"))) + .build(); + + return Maybe.just( + new TaskStatusUpdateEvent.Builder(event) + .status(new TaskStatus(event.getStatus().state(), newMessage, null)) + .build()); + }; + + AgentExecutorConfig config = + AgentExecutorConfig.builder().afterExecuteCallback(callback).build(); + + AgentExecutor executor = + new AgentExecutor.Builder() + .agentExecutorConfig(config) + .app(App.builder().name("test_app").rootAgent(testAgent).build()) + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .build(); + + RequestContext ctx = createRequestContext(); + executor.execute(ctx, eventQueue); + + // Verify status event + Optional statusEvent = + enqueuedEvents.stream() + .filter(e -> e instanceof TaskStatusUpdateEvent) + .map(e -> (TaskStatusUpdateEvent) e) + .filter(TaskStatusUpdateEvent::isFinal) + .findFirst(); + + assertThat(statusEvent).isPresent(); + assertThat(statusEvent.get().getStatus().message().getParts().get(0)) + .isInstanceOf(TextPart.class); + TextPart textPart = (TextPart) statusEvent.get().getStatus().message().getParts().get(0); + assertThat(textPart.getText()).isEqualTo("Modified completion"); + } + + @Test + public void execute_runnerFails_registersFailedEvent() { + testAgent.setEventsToEmit(Flowable.error(new RuntimeException("Runner error"))); + AgentExecutor executor = + new AgentExecutor.Builder() + .agentExecutorConfig(AgentExecutorConfig.builder().build()) + .app(App.builder().name("test_app").rootAgent(testAgent).build()) + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .build(); + + RequestContext ctx = createRequestContext(); + executor.execute(ctx, eventQueue); + + ImmutableList finalEvents = + enqueuedEvents.stream() + .filter(e -> e instanceof TaskStatusUpdateEvent) + .map(e -> (TaskStatusUpdateEvent) e) + // final events could be COMPLETED, FAILED, CANCELED, REJECTED or UNKNOWN + // as per io.a2a.spec.TaskState + .filter(TaskStatusUpdateEvent::isFinal) + .collect(toImmutableList()); + + assertThat(finalEvents).hasSize(1); + + TaskStatusUpdateEvent statusEvent = finalEvents.get(0); + assertThat(statusEvent.getStatus().state()).isEqualTo(TaskState.FAILED); + assertThat(statusEvent.getStatus().message().getParts().get(0)).isInstanceOf(TextPart.class); + TextPart textPart = (TextPart) statusEvent.getStatus().message().getParts().get(0); + assertThat(textPart.getText()).isEqualTo("Runner error"); + } + + @Test + public void execute_runnerSucceeds_registerCompletedTaskFails_noFailedTaskRegistered() { + testAgent.setEventsToEmit(Flowable.empty()); + + // Configure eventQueue to throw exception when TaskStatusUpdateEvent is enqueued + doAnswer( + invocation -> { + Object event = invocation.getArgument(0); + if (event instanceof TaskStatusUpdateEvent statusUpdate) { + if (statusUpdate.getStatus().state() == TaskState.COMPLETED) { + throw new RuntimeException("Enqueue failed"); + } + } + return null; + }) + .when(eventQueue) + .enqueueEvent(any()); + + AgentExecutor executor = + new AgentExecutor.Builder() + .agentExecutorConfig(AgentExecutorConfig.builder().build()) + .app(App.builder().name("test_app").rootAgent(testAgent).build()) + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .build(); + + RequestContext ctx = createRequestContext(); + executor.execute(ctx, eventQueue); + + // Verify status events in the tracked enqueuedEvents + ImmutableList statusEvents = + enqueuedEvents.stream() + .filter(e -> e instanceof TaskStatusUpdateEvent) + .map(e -> (TaskStatusUpdateEvent) e) + .filter(TaskStatusUpdateEvent::isFinal) + .collect(toImmutableList()); + + // There should be no final status events. + assertThat(statusEvents).isEmpty(); + } + + private RequestContext createRequestContext() { + Message message = + new Message.Builder() + .messageId("msg-1") + .role(Message.Role.USER) + .parts(ImmutableList.of(new TextPart("trigger"))) + .build(); + + RequestContext ctx = mock(RequestContext.class); + when(ctx.getMessage()).thenReturn(message); + when(ctx.getTaskId()).thenReturn("task-" + UUID.randomUUID()); + when(ctx.getContextId()).thenReturn("ctx-" + UUID.randomUUID()); + return ctx; + } + + @Test + public void process_statefulAggregation_tracksArtifactIdAndAppendForAuthor() { + Event partial1 = + Event.builder() + .partial(true) + .author("agent_author") + .content( + Content.builder() + .parts(ImmutableList.of(Part.builder().text("chunk1").build())) + .build()) + .build(); + Event partial2 = + Event.builder() + .partial(true) + .author("agent_author") + .content( + Content.builder() + .parts(ImmutableList.of(Part.builder().text("chunk2").build())) + .build()) + .build(); + Event finalEvent = + Event.builder() + .partial(false) + .author("agent_author") + .content( + Content.builder() + .parts(ImmutableList.of(Part.builder().text("chunk1chunk2").build())) + .build()) + .build(); + TestAgent agent = new TestAgent(Flowable.just(partial1, partial2, finalEvent)); + AgentExecutor executor = + new AgentExecutor.Builder() + .app(App.builder().name("test_app").rootAgent(agent).build()) + .sessionService(new InMemorySessionService()) + .artifactService(new InMemoryArtifactService()) + .agentExecutorConfig( + AgentExecutorConfig.builder() + .outputMode(AgentExecutorConfig.OutputMode.ARTIFACT_PER_EVENT) + .build()) + .build(); + RequestContext requestContext = mock(RequestContext.class); + Message message = + new Message.Builder() + .messageId("msg-id") + .taskId("task-id") + .contextId("context-id") + .role(Message.Role.USER) + .parts(ImmutableList.of(new TextPart("test"))) + .build(); + when(requestContext.getMessage()).thenReturn(message); + when(requestContext.getTaskId()).thenReturn("task-id"); + when(requestContext.getContextId()).thenReturn("context-id"); + EventQueue eventQueue = mock(EventQueue.class); + + executor.execute(requestContext, eventQueue); + + ArgumentCaptor eventCaptor = + ArgumentCaptor.forClass(io.a2a.spec.Event.class); + verify(eventQueue, atLeastOnce()).enqueueEvent(eventCaptor.capture()); + ImmutableList artifactEvents = + eventCaptor.getAllValues().stream() + .filter(e -> e instanceof TaskArtifactUpdateEvent) + .map(e -> (TaskArtifactUpdateEvent) e) + .collect(toImmutableList()); + TaskArtifactUpdateEvent ev1 = artifactEvents.get(0); + TaskArtifactUpdateEvent ev2 = artifactEvents.get(1); + TaskArtifactUpdateEvent ev3 = artifactEvents.get(2); + String firstArtifactId = ev1.getArtifact().artifactId(); + // Event 1 (Partial) + assertThat(artifactEvents).hasSize(3); + assertThat(ev1.isAppend()).isTrue(); + assertThat(ev1.isLastChunk()).isFalse(); + // Event 2 (Partial) + assertThat(ev2.isAppend()).isTrue(); + assertThat(ev2.isLastChunk()).isFalse(); + assertThat(ev2.getArtifact().artifactId()).isEqualTo(firstArtifactId); + // Event 3 (Non-partial, final) + assertThat(ev3.isAppend()).isFalse(); + assertThat(ev3.isLastChunk()).isTrue(); + assertThat(ev3.getArtifact().artifactId()).isEqualTo(firstArtifactId); + } + + private static final class TestAgent extends BaseAgent { + private Flowable eventsToEmit; + + TestAgent() { + this(Flowable.empty()); + } + + TestAgent(Flowable eventsToEmit) { + // BaseAgent constructor: name, description, examples, tools, model + super("test_agent", "test", ImmutableList.of(), null, null); + this.eventsToEmit = eventsToEmit; + } + + void setEventsToEmit(Flowable events) { + this.eventsToEmit = events; + } + + @Override + protected Flowable runAsyncImpl(InvocationContext invocationContext) { + return eventsToEmit; + } + + @Override + protected Flowable runLiveImpl(InvocationContext invocationContext) { + return eventsToEmit; + } + } +} diff --git a/a2a/src/test/java/com/google/adk/a2a/grpc/MediaSupportTest.java b/a2a/src/test/java/com/google/adk/a2a/grpc/MediaSupportTest.java index b9fa148d3..a8019be30 100644 --- a/a2a/src/test/java/com/google/adk/a2a/grpc/MediaSupportTest.java +++ b/a2a/src/test/java/com/google/adk/a2a/grpc/MediaSupportTest.java @@ -44,11 +44,11 @@ void testTextPart_conversion() { // GenAI Part to A2A TextPart Part genaiTextPart = Part.builder().text("Hello, world!").build(); - Optional> a2aPart = PartConverter.fromGenaiPart(genaiTextPart); + io.a2a.spec.Part a2aPart = PartConverter.fromGenaiPart(genaiTextPart, false); - assertThat(a2aPart).isPresent(); - assertThat(a2aPart.get()).isInstanceOf(TextPart.class); - assertThat(((TextPart) a2aPart.get()).getText()).isEqualTo("Hello, world!"); + assertThat(a2aPart).isNotNull(); + assertThat(a2aPart).isInstanceOf(TextPart.class); + assertThat(((TextPart) a2aPart).getText()).isEqualTo("Hello, world!"); } @Test @@ -158,11 +158,11 @@ void testGenAIImagePart_toA2A() { .build()) .build(); - Optional> a2aPart = PartConverter.fromGenaiPart(genaiImagePart); + io.a2a.spec.Part a2aPart = PartConverter.fromGenaiPart(genaiImagePart, false); - assertThat(a2aPart).isPresent(); - assertThat(a2aPart.get()).isInstanceOf(FilePart.class); - FilePart filePart = (FilePart) a2aPart.get(); + assertThat(a2aPart).isNotNull(); + assertThat(a2aPart).isInstanceOf(FilePart.class); + FilePart filePart = (FilePart) a2aPart; assertThat(filePart.getFile()).isInstanceOf(FileWithUri.class); FileWithUri fileWithUri = (FileWithUri) filePart.getFile(); assertThat(fileWithUri.uri()).isEqualTo("https://example.com/image.jpg"); @@ -183,11 +183,11 @@ void testGenAIAudioPart_toA2A() { .build()) .build(); - Optional> a2aPart = PartConverter.fromGenaiPart(genaiAudioPart); + io.a2a.spec.Part a2aPart = PartConverter.fromGenaiPart(genaiAudioPart, false); - assertThat(a2aPart).isPresent(); - assertThat(a2aPart.get()).isInstanceOf(FilePart.class); - FilePart filePart = (FilePart) a2aPart.get(); + assertThat(a2aPart).isNotNull(); + assertThat(a2aPart).isInstanceOf(FilePart.class); + FilePart filePart = (FilePart) a2aPart; assertThat(filePart.getFile()).isInstanceOf(FileWithBytes.class); FileWithBytes fileWithBytes = (FileWithBytes) filePart.getFile(); assertThat(fileWithBytes.mimeType()).isEqualTo("audio/wav"); @@ -207,11 +207,11 @@ void testGenAIVideoPart_toA2A() { .build()) .build(); - Optional> a2aPart = PartConverter.fromGenaiPart(genaiVideoPart); + io.a2a.spec.Part a2aPart = PartConverter.fromGenaiPart(genaiVideoPart, false); - assertThat(a2aPart).isPresent(); - assertThat(a2aPart.get()).isInstanceOf(FilePart.class); - FilePart filePart = (FilePart) a2aPart.get(); + assertThat(a2aPart).isNotNull(); + assertThat(a2aPart).isInstanceOf(FilePart.class); + FilePart filePart = (FilePart) a2aPart; assertThat(filePart.getFile()).isInstanceOf(FileWithUri.class); FileWithUri fileWithUri = (FileWithUri) filePart.getFile(); assertThat(fileWithUri.mimeType()).isEqualTo("video/mp4"); diff --git a/contrib/firestore-session-service/pom.xml b/contrib/firestore-session-service/pom.xml index 592555a0f..a62bff5b6 100644 --- a/contrib/firestore-session-service/pom.xml +++ b/contrib/firestore-session-service/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 1.2.0 + 0.8.1-SNAPSHOT ../../pom.xml diff --git a/contrib/firestore-session-service/src/main/java/com/google/adk/sessions/FirestoreSessionService.java b/contrib/firestore-session-service/src/main/java/com/google/adk/sessions/FirestoreSessionService.java index d3295a6ef..db236fc88 100644 --- a/contrib/firestore-session-service/src/main/java/com/google/adk/sessions/FirestoreSessionService.java +++ b/contrib/firestore-session-service/src/main/java/com/google/adk/sessions/FirestoreSessionService.java @@ -50,6 +50,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.regex.Matcher; +import javax.annotation.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -88,7 +89,20 @@ private CollectionReference getSessionsCollection(String userId) { /** Creates a new session in Firestore. */ @Override public Single createSession( - String appName, String userId, ConcurrentMap state, String sessionId) { + String appName, + String userId, + @Nullable ConcurrentMap state, + @Nullable String sessionId) { + return createSession(appName, userId, (Map) state, sessionId); + } + + /** Creates a new session in Firestore. */ + @Override + public Single createSession( + String appName, + String userId, + @Nullable Map state, + @Nullable String sessionId) { return Single.fromCallable( () -> { Objects.requireNonNull(appName, "appName cannot be null"); diff --git a/contrib/langchain4j/pom.xml b/contrib/langchain4j/pom.xml index e5ec78842..e2ba4a7fb 100644 --- a/contrib/langchain4j/pom.xml +++ b/contrib/langchain4j/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 1.2.0 + 0.8.1-SNAPSHOT ../../pom.xml diff --git a/contrib/samples/a2a_basic/pom.xml b/contrib/samples/a2a_basic/pom.xml index e12ca09a1..82b11b96f 100644 --- a/contrib/samples/a2a_basic/pom.xml +++ b/contrib/samples/a2a_basic/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-samples - 1.2.0 + 0.8.1-SNAPSHOT .. diff --git a/contrib/samples/a2a_server/README.md b/contrib/samples/a2a_server/README.md new file mode 100644 index 000000000..5351e32c0 --- /dev/null +++ b/contrib/samples/a2a_server/README.md @@ -0,0 +1,70 @@ +# Google ADK A2A Agent Server Sample + +This sample demonstrates how to expose a Google ADK (Agent Development Kit) +agent via the A2A (Agent-to-Agent) protocol using A2A SDK and Quarkus service. + +## Overview + +The application implements a simple conversational agent that checks whether +given numbers are prime numbers. It uses the `LlmAgent` from the Google ADK and +exposes it via an A2A server. + +### Key Components + +* **`Agent.java`**: Defines the `LlmAgent` instance (`check_prime_agent`) and + the `checkPrime` tool function it uses to verify numbers. +* **`AgentCardProducer.java`**: Loads and provides the `AgentCard` metadata + (from `agent.json`) which defines the agent's identity and capabilities in + the A2A network. +* **`AgentExecutorProducer.java`**: Configures and provides the A2A + `AgentExecutor`, implemented by the ADK library to wire ADK-owned agents + automatically. +* **`StartupConfig.java`**: Contains initialization logic, such as registering + JSON modules for the Vert.x/Quarkus runtime. +* **`application.properties`**: Contains a configuration for the Quarkus + service and A2A, such as port where application will be exposed, application + name and event processing timeouts. + +## Building the Project + +You can build the project using Maven: + +```shell +mvn clean install +``` + +The Java server can be started using `mvn` as follow (don't forget to set your +GOOGLE_API_KEY before running the service): + +```bash +export GOOGLE_API_KEY= + +cd contrib/samples/a2a_server +mvn quarkus:dev +``` + +## Sample request + +```bash +curl -X POST http://localhost:9090 \ + -H 'Content-Type: application/json' \ + -d '{ + "jsonrpc": "2.0", + "id": "cli-check-2", + "method": "message/stream", + "params": { + "message": { + "kind": "message", + "contextId": "cli-demo-context", + "messageId": "cli-check-2", + "role": "user", + "parts": [ + { + "kind": "text", + "text": "Is 2 prime?" + } + ] + } + } + }' +``` diff --git a/contrib/samples/a2a_server/pom.xml b/contrib/samples/a2a_server/pom.xml new file mode 100644 index 000000000..84023e260 --- /dev/null +++ b/contrib/samples/a2a_server/pom.xml @@ -0,0 +1,140 @@ + + + 4.0.0 + + + com.google.adk + google-adk-samples + 0.8.1-SNAPSHOT + .. + + + google-adk-sample-a2a-agent + jar + + Google ADK - Sample - A2A Agent Server + Demonstrates exposing ADK agent via A2A. + + + UTF-8 + 17 + ${project.version} + ${project.version} + 0.3.0.Beta1 + 3.30.6 + 0.8 + + + + + + io.quarkus + quarkus-bom + ${quarkus.platform.version} + pom + import + + + + + + + io.github.a2asdk + a2a-java-sdk-reference-jsonrpc + ${a2a.sdk.version} + + + io.quarkus + quarkus-resteasy-jackson + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + com.google.adk + google-adk + ${google-adk.version} + + + com.google.adk + google-adk-a2a + ${google-adk-a2a.version} + + + + io.github.a2asdk + a2a-java-sdk-spec + ${a2a.sdk.version} + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-reactive-routes + + + io.quarkus + quarkus-jackson + + + + io.github.a2asdk + a2a-java-sdk-client + ${a2a.sdk.version} + + + + com.google.flogger + flogger + ${flogger.version} + + + + com.google.flogger + google-extensions + ${flogger.version} + + + + com.google.flogger + flogger-system-backend + ${flogger.version} + + + + + + + + src/main/java + + **/*.json + + + + src/main/resources + + + + + io.quarkus + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + + + + + + + diff --git a/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentCardProducer.java b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentCardProducer.java new file mode 100644 index 000000000..0937e2512 --- /dev/null +++ b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentCardProducer.java @@ -0,0 +1,33 @@ +package com.google.adk.samples.a2aagent; + +import io.a2a.server.PublicAgentCard; +import io.a2a.spec.AgentCard; +import io.a2a.util.Utils; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +/** Produces the {@link AgentCard} from the bundled JSON resources. */ +@ApplicationScoped +public class AgentCardProducer { + + @Produces + @PublicAgentCard + public AgentCard agentCard() { + try (InputStream is = getClass().getResourceAsStream("/agent/agent.json")) { + if (is == null) { + throw new RuntimeException("agent.json not found in resources"); + } + + // Read the JSON file content + String json = new String(is.readAllBytes(), StandardCharsets.UTF_8); + + // Use the SDK's built-in mapper to convert JSON string to AgentCard record + return Utils.OBJECT_MAPPER.readValue(json, AgentCard.class); + + } catch (Exception e) { + throw new RuntimeException("Failed to load AgentCard from JSON", e); + } + } +} diff --git a/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentExecutorProducer.java b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentExecutorProducer.java new file mode 100644 index 000000000..4ecd2517d --- /dev/null +++ b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/AgentExecutorProducer.java @@ -0,0 +1,28 @@ +package com.google.adk.samples.a2aagent; + +import com.google.adk.a2a.executor.AgentExecutorConfig; +import com.google.adk.samples.a2aagent.agent.Agent; +import com.google.adk.sessions.InMemorySessionService; +import io.a2a.server.agentexecution.AgentExecutor; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** Produces the {@link AgentExecutor} instance that handles agent interactions. */ +@ApplicationScoped +public class AgentExecutorProducer { + + @ConfigProperty(name = "my.adk.app.name", defaultValue = "default-app") + String appName; + + @Produces + public AgentExecutor agentExecutor() { + InMemorySessionService sessionService = new InMemorySessionService(); + return new com.google.adk.a2a.executor.AgentExecutor.Builder() + .agent(Agent.ROOT_AGENT) + .appName(appName) + .sessionService(sessionService) + .agentExecutorConfig(AgentExecutorConfig.builder().build()) + .build(); + } +} diff --git a/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/StartupConfig.java b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/StartupConfig.java new file mode 100644 index 000000000..0da70b086 --- /dev/null +++ b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/StartupConfig.java @@ -0,0 +1,17 @@ +package com.google.adk.samples.a2aagent; + +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import io.quarkus.runtime.StartupEvent; +import io.vertx.core.json.jackson.DatabindCodec; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.event.Observes; + +/** Configuration applied on startup, such as Jackson module registrations. */ +@ApplicationScoped +public class StartupConfig { + + void onStart(@Observes StartupEvent ev) { + // Register globally for Vert.x's internal JSON handling + DatabindCodec.mapper().registerModule(new JavaTimeModule()); + } +} diff --git a/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/agent/Agent.java b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/agent/Agent.java new file mode 100644 index 000000000..a415f618f --- /dev/null +++ b/contrib/samples/a2a_server/src/main/java/com/google/adk/samples/a2aagent/agent/Agent.java @@ -0,0 +1,107 @@ +package com.google.adk.samples.a2aagent.agent; + +import static java.util.stream.Collectors.joining; + +import com.google.adk.agents.LlmAgent; +import com.google.adk.tools.FunctionTool; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.flogger.GoogleLogger; +import io.reactivex.rxjava3.core.Maybe; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** Agent that can check whether numbers are prime. */ +public final class Agent { + + private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); + + /** + * Checks if a list of numbers are prime. + * + * @param nums The list of numbers to check + * @return A map containing the result message + */ + public static ImmutableMap checkPrime(List nums) { + logger.atInfo().log("checkPrime called with nums=%s", nums); + Set primes = new HashSet<>(); + for (int num : nums) { + if (num <= 1) { + continue; + } + boolean isPrime = true; + for (int i = 2; i <= Math.sqrt(num); i++) { + if (num % i == 0) { + isPrime = false; + break; + } + } + if (isPrime) { + primes.add(num); + } + } + String result; + if (primes.isEmpty()) { + result = "No prime numbers found."; + } else if (primes.size() == 1) { + int only = primes.iterator().next(); + // Per request: singular phrasing without article + result = only + " is prime number."; + } else { + result = primes.stream().map(String::valueOf).collect(joining(", ")) + " are prime numbers."; + } + logger.atInfo().log("checkPrime result=%s", result); + return ImmutableMap.of("result", result); + } + + public static final LlmAgent ROOT_AGENT = + LlmAgent.builder() + .model("gemini-2.5-pro") + .name("check_prime_agent") + .description("check prime agent that can check whether numbers are prime.") + .instruction( + """ + You check whether numbers are prime. + + If the last user message contains numbers, call checkPrime exactly once with exactly + those integers as a list (e.g., [2]). Never add other numbers. Do not ask for + clarification. Return only the tool's result. + + Always pass a list of integers to the tool (use a single-element list for one + number). Never pass strings. + """) + // Log the exact contents passed to the LLM request for verification + .beforeModelCallback( + (callbackContext, llmRequest) -> { + try { + logger.atInfo().log( + "Invocation events (count=%d): %s", + callbackContext.events().size(), callbackContext.events()); + } catch (Throwable t) { + logger.atWarning().withCause(t).log("BeforeModel logging error"); + } + return Maybe.empty(); + }) + .afterModelCallback( + (callbackContext, llmResponse) -> { + try { + String content = + llmResponse.content().map(Object::toString).orElse(""); + logger.atInfo().log("AfterModel content=%s", content); + llmResponse + .errorMessage() + .ifPresent( + error -> + logger.atInfo().log( + "AfterModel errorMessage=%s", error.replace("\n", "\\n"))); + } catch (Throwable t) { + logger.atWarning().withCause(t).log("AfterModel logging error"); + } + return Maybe.empty(); + }) + .tools(ImmutableList.of(FunctionTool.create(Agent.class, "checkPrime"))) + .build(); + + private Agent() {} +} diff --git a/contrib/samples/a2a_server/src/main/resources/agent/agent.json b/contrib/samples/a2a_server/src/main/resources/agent/agent.json new file mode 100644 index 000000000..4a0848282 --- /dev/null +++ b/contrib/samples/a2a_server/src/main/resources/agent/agent.json @@ -0,0 +1,18 @@ +{ + "capabilities": {"streaming": true}, + "defaultInputModes": ["text/plain"], + "defaultOutputModes": ["application/json"], + "description": "An agent specialized in checking whether numbers are prime. It can efficiently determine the primality of individual numbers or lists of numbers.", + "name": "check_prime_agent", + "skills": [ + { + "id": "prime_checking", + "name": "Prime Number Checking", + "description": "Check if numbers in a list are prime using efficient mathematical algorithms", + "tags": ["mathematical", "computation", "prime", "numbers"] + } + ], + "preferredTransport": "JSONRPC", + "url": "http://localhost:9090", + "version": "1.0.0" +} diff --git a/contrib/samples/a2a_server/src/main/resources/application.properties b/contrib/samples/a2a_server/src/main/resources/application.properties new file mode 100644 index 000000000..ba7a5f2b0 --- /dev/null +++ b/contrib/samples/a2a_server/src/main/resources/application.properties @@ -0,0 +1,10 @@ +# Timeout for the agent to complete execution (default 30s) +a2a.blocking.agent.timeout.seconds=30 + +# Timeout for final event processing (default 5s) +a2a.blocking.consumption.timeout.seconds=5 + +# Custom application name for the ADK Runner +my.adk.app.name=My-JSONRPC-Agent + +quarkus.http.port=9090 \ No newline at end of file diff --git a/contrib/samples/configagent/pom.xml b/contrib/samples/configagent/pom.xml index db7bde0c5..6f7bfff83 100644 --- a/contrib/samples/configagent/pom.xml +++ b/contrib/samples/configagent/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-samples - 1.2.0 + 0.8.1-SNAPSHOT .. diff --git a/contrib/samples/helloworld/pom.xml b/contrib/samples/helloworld/pom.xml index d7d273b0b..36d12eaf0 100644 --- a/contrib/samples/helloworld/pom.xml +++ b/contrib/samples/helloworld/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-samples - 1.2.0 + 0.8.1-SNAPSHOT .. @@ -36,7 +36,7 @@ UTF-8 17 - 1.11.0 + 1.11.1 com.example.helloworld.HelloWorldRun ${project.version} diff --git a/contrib/samples/mcpfilesystem/pom.xml b/contrib/samples/mcpfilesystem/pom.xml index 656258ebf..935aa6531 100644 --- a/contrib/samples/mcpfilesystem/pom.xml +++ b/contrib/samples/mcpfilesystem/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 1.2.0 + 0.8.1-SNAPSHOT ../../.. @@ -36,7 +36,7 @@ UTF-8 17 - 1.11.0 + 1.11.1 com.example.mcpfilesystem.McpFilesystemRun ${project.parent.version} diff --git a/contrib/samples/pom.xml b/contrib/samples/pom.xml index 043e6a104..905f8e711 100644 --- a/contrib/samples/pom.xml +++ b/contrib/samples/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 1.2.0 + 0.8.1-SNAPSHOT ../.. @@ -17,7 +17,7 @@ a2a_basic - a2a_remote + a2a_server configagent helloworld mcpfilesystem diff --git a/contrib/spring-ai/pom.xml b/contrib/spring-ai/pom.xml index 01cf66b01..f49c3faae 100644 --- a/contrib/spring-ai/pom.xml +++ b/contrib/spring-ai/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 1.2.0 + 0.8.1-SNAPSHOT ../../pom.xml @@ -29,7 +29,7 @@ Spring AI integration for the Agent Development Kit. - 1.1.0 + 2.0.0-M2 1.21.3 diff --git a/core/pom.xml b/core/pom.xml index 06330da65..1fa1b3689 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -20,23 +20,20 @@ com.google.adk google-adk-parent - 1.2.0 + 0.8.1-SNAPSHOT google-adk Agent Development Kit Agent Development Kit: an open-source, code-first toolkit designed to simplify building, evaluating, and deploying advanced AI agents anywhere. + + com.anthropic anthropic-java - - com.zaxxer - HikariCP - 5.1.0 - com.anthropic anthropic-java-vertex @@ -77,6 +74,10 @@ com.squareup.okhttp3 okhttp + + com.squareup.okhttp3 + okhttp-jvm + com.google.auto.value auto-value-annotations @@ -158,6 +159,11 @@ io.reactivex.rxjava3 rxjava + + org.json + json + 20240303 + io.projectreactor reactor-core @@ -167,12 +173,6 @@ wiremock-jre8 test - - com.squareup.okhttp3 - mockwebserver - 4.12.0 - test - io.opentelemetry opentelemetry-api @@ -198,6 +198,11 @@ opentelemetry-sdk-testing test + + com.zaxxer + HikariCP + 5.1.0 + org.postgresql postgresql @@ -208,30 +213,21 @@ mongodb-driver-sync 4.10.2 - - org.json - json - 20240303 - - org.mapdb mapdb 3.0.8 - com.datastax.oss java-driver-core 4.17.0 - com.fasterxml.jackson.datatype jackson-datatype-guava ${jackson.version} - com.anthropic anthropic-java-bedrock @@ -264,22 +260,27 @@ ini4j 0.5.4 - redis.clients jedis 6.0.0 - org.apache.kafka - kafka-clients - 3.3.1 - + org.apache.kafka + kafka-clients + 3.3.1 + net.javacrumbs.future-converter future-converter-java8-guava 1.2.0 + + com.squareup.okhttp3 + mockwebserver + 4.12.0 + test + @@ -290,22 +291,35 @@ - org.apache.maven.plugins - maven-surefire-plugin - - - **/*IT.java - - + maven-compiler-plugin maven-surefire-plugin + basic test + + + false + + + + + vertex-ai-rag-retrieval + + test + + + + true + + + VertexAiRagRetrievalTest#processLlmRequest_gemini2Model_addVertexRagStoreToConfig, VertexAiRagRetrievalTest#processLlmRequest_otherModel_doNotAddVertexRagStoreToConfig + apigee-llm @@ -353,31 +367,4 @@ - - - integration-tests - - - - org.apache.maven.plugins - maven-failsafe-plugin - 3.2.5 - - - - integration-test - verify - - - - - - **/*IT.java - - - - - - - - \ No newline at end of file + diff --git a/core/src/main/java/com/google/adk/Version.java b/core/src/main/java/com/google/adk/Version.java index 2816d6763..1dc0282c3 100644 --- a/core/src/main/java/com/google/adk/Version.java +++ b/core/src/main/java/com/google/adk/Version.java @@ -22,7 +22,7 @@ */ public final class Version { // Don't touch this, release-please should keep it up to date. - public static final String JAVA_ADK_VERSION = "1.2.0"; // x-release-please-released-version + public static final String JAVA_ADK_VERSION = "0.8.0"; // x-release-please-released-version private Version() {} } diff --git a/core/src/main/java/com/google/adk/agents/BaseAgent.java b/core/src/main/java/com/google/adk/agents/BaseAgent.java index 226e61abe..d74ba9ca5 100644 --- a/core/src/main/java/com/google/adk/agents/BaseAgent.java +++ b/core/src/main/java/com/google/adk/agents/BaseAgent.java @@ -409,7 +409,7 @@ private Single> callCallback( .id(Event.generateEventId()) .invocationId(invocationContext.invocationId()) .author(name()) - .branch(invocationContext.branch()) + .branch(invocationContext.branch().orElse(null)) .actions(callbackContext.eventActions()) .content(content) .build()); @@ -426,7 +426,7 @@ private Single> callCallback( .id(Event.generateEventId()) .invocationId(invocationContext.invocationId()) .author(name()) - .branch(invocationContext.branch()) + .branch(invocationContext.branch().orElse(null)) .actions(callbackContext.eventActions()); return Single.just(Optional.of(eventBuilder.build())); diff --git a/core/src/main/java/com/google/adk/agents/InvocationContext.java b/core/src/main/java/com/google/adk/agents/InvocationContext.java index 6457a8ca4..7602ca9f2 100644 --- a/core/src/main/java/com/google/adk/agents/InvocationContext.java +++ b/core/src/main/java/com/google/adk/agents/InvocationContext.java @@ -43,18 +43,18 @@ public class InvocationContext { private final BaseArtifactService artifactService; private final BaseMemoryService memoryService; private final Plugin pluginManager; - private final Optional liveRequestQueue; + @Nullable private final LiveRequestQueue liveRequestQueue; private final Map activeStreamingTools; private final String invocationId; private final Session session; - private final Optional userContent; + @Nullable private final Content userContent; private final RunConfig runConfig; @Nullable private final EventsCompactionConfig eventsCompactionConfig; @Nullable private final ContextCacheConfig contextCacheConfig; private final InvocationCostManager invocationCostManager; private final Map callbackContextData; - private Optional branch; + @Nullable private String branch; private BaseAgent agent; private boolean endInvocation; @@ -78,70 +78,6 @@ protected InvocationContext(Builder builder) { this.callbackContextData = new ConcurrentHashMap<>(builder.callbackContextData); } - /** - * @deprecated Use {@link #builder()} instead. - */ - @Deprecated(forRemoval = true) - public InvocationContext( - BaseSessionService sessionService, - BaseArtifactService artifactService, - BaseMemoryService memoryService, - Plugin pluginManager, - Optional liveRequestQueue, - Optional branch, - String invocationId, - BaseAgent agent, - Session session, - Optional userContent, - RunConfig runConfig, - boolean endInvocation) { - this( - builder() - .sessionService(sessionService) - .artifactService(artifactService) - .memoryService(memoryService) - .pluginManager(pluginManager) - .liveRequestQueue(liveRequestQueue) - .branch(branch) - .invocationId(invocationId) - .agent(agent) - .session(session) - .userContent(userContent) - .runConfig(runConfig) - .endInvocation(endInvocation)); - } - - /** - * @deprecated Use {@link #builder()} instead. - */ - @Deprecated(forRemoval = true) - public InvocationContext( - BaseSessionService sessionService, - BaseArtifactService artifactService, - BaseMemoryService memoryService, - Optional liveRequestQueue, - Optional branch, - String invocationId, - BaseAgent agent, - Session session, - Optional userContent, - RunConfig runConfig, - boolean endInvocation) { - this( - builder() - .sessionService(sessionService) - .artifactService(artifactService) - .memoryService(memoryService) - .liveRequestQueue(liveRequestQueue) - .branch(branch) - .invocationId(invocationId) - .agent(agent) - .session(session) - .userContent(userContent) - .runConfig(runConfig) - .endInvocation(endInvocation)); - } - /** * @deprecated Use {@link #builder()} instead. */ @@ -153,10 +89,10 @@ public InvocationContext( + ".invocationId(invocationId)" + ".agent(agent)" + ".session(session)" - + ".userContent(Optional.ofNullable(userContent))" + + ".userContent(userContent)" + ".runConfig(runConfig)" + ".build()", - imports = {"com.google.adk.agents.InvocationContext", "java.util.Optional"}) + imports = {"com.google.adk.agents.InvocationContext"}) @Deprecated(forRemoval = true) public static InvocationContext create( BaseSessionService sessionService, @@ -172,7 +108,7 @@ public static InvocationContext create( .invocationId(invocationId) .agent(agent) .session(session) - .userContent(Optional.ofNullable(userContent)) + .userContent(userContent) .runConfig(runConfig) .build(); } @@ -245,7 +181,7 @@ public Map activeStreamingTools() { /** Returns the queue for managing live requests, if available for this invocation. */ public Optional liveRequestQueue() { - return liveRequestQueue; + return Optional.ofNullable(liveRequestQueue); } /** Returns the unique ID for this invocation. */ @@ -258,7 +194,7 @@ public String invocationId() { * history. */ public void branch(@Nullable String branch) { - this.branch = Optional.ofNullable(branch); + this.branch = branch; } /** @@ -266,7 +202,7 @@ public void branch(@Nullable String branch) { * the conversation history. */ public Optional branch() { - return branch; + return Optional.ofNullable(branch); } /** Returns the agent being invoked. */ @@ -291,7 +227,7 @@ public Session session() { /** Returns the user content that triggered this invocation, if any. */ public Optional userContent() { - return userContent; + return Optional.ofNullable(userContent); } /** Returns the configuration for the current agent run. */ @@ -416,13 +352,13 @@ private Builder(InvocationContext context) { private BaseArtifactService artifactService; private BaseMemoryService memoryService; private Plugin pluginManager = new PluginManager(); - private Optional liveRequestQueue = Optional.empty(); + @Nullable private LiveRequestQueue liveRequestQueue = null; private Map activeStreamingTools = new ConcurrentHashMap<>(); - private Optional branch = Optional.empty(); + @Nullable private String branch = null; private String invocationId = newInvocationContextId(); private BaseAgent agent; private Session session; - private Optional userContent = Optional.empty(); + @Nullable private Content userContent = null; private RunConfig runConfig = RunConfig.builder().build(); private boolean endInvocation = false; @Nullable private EventsCompactionConfig eventsCompactionConfig; @@ -478,21 +414,6 @@ public Builder pluginManager(Plugin pluginManager) { return this; } - /** - * Sets the queue for managing live requests. - * - * @param liveRequestQueue the queue for managing live requests. - * @return this builder instance for chaining. - * @deprecated Use {@link #liveRequestQueue(LiveRequestQueue)} instead. - */ - // TODO: b/462140921 - Builders should not accept Optional parameters. - @Deprecated(forRemoval = true) - @CanIgnoreReturnValue - public Builder liveRequestQueue(Optional liveRequestQueue) { - this.liveRequestQueue = liveRequestQueue; - return this; - } - /** * Sets the queue for managing live requests. * @@ -501,7 +422,7 @@ public Builder liveRequestQueue(Optional liveRequestQueue) { */ @CanIgnoreReturnValue public Builder liveRequestQueue(@Nullable LiveRequestQueue liveRequestQueue) { - this.liveRequestQueue = Optional.ofNullable(liveRequestQueue); + this.liveRequestQueue = liveRequestQueue; return this; } @@ -510,28 +431,13 @@ public Builder liveRequestQueue(@Nullable LiveRequestQueue liveRequestQueue) { * * @param branch the branch ID for the invocation. * @return this builder instance for chaining. - * @deprecated Use {@link #branch(String)} instead. */ - // TODO: b/462140921 - Builders should not accept Optional parameters. - @Deprecated(forRemoval = true) @CanIgnoreReturnValue - public Builder branch(Optional branch) { + public Builder branch(@Nullable String branch) { this.branch = branch; return this; } - /** - * Sets the branch ID for the invocation. - * - * @param branch the branch ID for the invocation. - * @return this builder instance for chaining. - */ - @CanIgnoreReturnValue - public Builder branch(String branch) { - this.branch = Optional.of(branch); - return this; - } - /** * Sets the unique ID for the invocation. * @@ -575,23 +481,11 @@ public Builder session(Session session) { * @return this builder instance for chaining. */ @CanIgnoreReturnValue - public Builder userContent(Optional userContent) { + public Builder userContent(@Nullable Content userContent) { this.userContent = userContent; return this; } - /** - * Sets the user content that triggered this invocation. - * - * @param userContent the user content that triggered this invocation. - * @return this builder instance for chaining. - */ - @CanIgnoreReturnValue - public Builder userContent(Content userContent) { - this.userContent = Optional.of(userContent); - return this; - } - /** * Sets the configuration for the current agent run. * diff --git a/core/src/main/java/com/google/adk/agents/LiveRequest.java b/core/src/main/java/com/google/adk/agents/LiveRequest.java index 9c66f038b..df1ada6a5 100644 --- a/core/src/main/java/com/google/adk/agents/LiveRequest.java +++ b/core/src/main/java/com/google/adk/agents/LiveRequest.java @@ -77,18 +77,12 @@ public abstract static class Builder { @JsonProperty("content") public abstract Builder content(@Nullable Content content); - public abstract Builder content(Optional content); - @JsonProperty("blob") public abstract Builder blob(@Nullable Blob blob); - public abstract Builder blob(Optional blob); - @JsonProperty("close") public abstract Builder close(@Nullable Boolean close); - public abstract Builder close(Optional close); - abstract LiveRequest autoBuild(); public final LiveRequest build() { diff --git a/core/src/main/java/com/google/adk/agents/LlmAgent.java b/core/src/main/java/com/google/adk/agents/LlmAgent.java index 218ac7358..88b96cbd4 100644 --- a/core/src/main/java/com/google/adk/agents/LlmAgent.java +++ b/core/src/main/java/com/google/adk/agents/LlmAgent.java @@ -319,7 +319,7 @@ public Builder beforeModelCallback(BeforeModelCallback beforeModelCallback) { @CanIgnoreReturnValue public Builder beforeModelCallback( - @Nullable List beforeModelCallbacks) { + @Nullable List beforeModelCallbacks) { this.beforeModelCallback = convertCallbacks( beforeModelCallbacks, @@ -356,7 +356,8 @@ public Builder afterModelCallback(AfterModelCallback afterModelCallback) { } @CanIgnoreReturnValue - public Builder afterModelCallback(@Nullable List afterModelCallbacks) { + public Builder afterModelCallback( + @Nullable List afterModelCallbacks) { this.afterModelCallback = convertCallbacks( afterModelCallbacks, @@ -393,7 +394,7 @@ public Builder onModelErrorCallback(OnModelErrorCallback onModelErrorCallback) { @CanIgnoreReturnValue public Builder onModelErrorCallback( - @Nullable List onModelErrorCallbacks) { + @Nullable List onModelErrorCallbacks) { this.onModelErrorCallback = convertCallbacks( onModelErrorCallbacks, @@ -489,7 +490,8 @@ public Builder afterToolCallback(AfterToolCallback afterToolCallback) { } @CanIgnoreReturnValue - public Builder afterToolCallback(@Nullable List afterToolCallbacks) { + public Builder afterToolCallback( + @Nullable List afterToolCallbacks) { this.afterToolCallback = convertCallbacks( afterToolCallbacks, @@ -529,7 +531,7 @@ public Builder onToolErrorCallback(OnToolErrorCallback onToolErrorCallback) { @CanIgnoreReturnValue public Builder onToolErrorCallback( - @Nullable List onToolErrorCallbacks) { + @Nullable List onToolErrorCallbacks) { this.onToolErrorCallback = convertCallbacks( onToolErrorCallbacks, @@ -1124,7 +1126,10 @@ private Model resolveModelInternal() { Model currentModel = this.model.get(); if (currentModel.model().isPresent()) { - return currentModel; + String modelName = currentModel.model().get().model(); + BaseLlm resolvedLlm = currentModel.model().get(); + + return Model.builder().modelName(modelName).model(resolvedLlm).build(); } if (currentModel.modelName().isPresent()) { diff --git a/core/src/main/java/com/google/adk/apps/App.java b/core/src/main/java/com/google/adk/apps/App.java index 18e8753c7..897e24490 100644 --- a/core/src/main/java/com/google/adk/apps/App.java +++ b/core/src/main/java/com/google/adk/apps/App.java @@ -104,6 +104,12 @@ public Builder plugins(List plugins) { return this; } + @CanIgnoreReturnValue + public Builder plugins(Plugin... plugins) { + this.plugins = ImmutableList.copyOf(plugins); + return this; + } + @CanIgnoreReturnValue public Builder eventsCompactionConfig(EventsCompactionConfig eventsCompactionConfig) { this.eventsCompactionConfig = eventsCompactionConfig; diff --git a/core/src/main/java/com/google/adk/artifacts/BaseArtifactService.java b/core/src/main/java/com/google/adk/artifacts/BaseArtifactService.java index 32ef9ff4d..a9bb6ba4d 100644 --- a/core/src/main/java/com/google/adk/artifacts/BaseArtifactService.java +++ b/core/src/main/java/com/google/adk/artifacts/BaseArtifactService.java @@ -16,6 +16,7 @@ package com.google.adk.artifacts; +import com.google.adk.sessions.SessionKey; import com.google.common.collect.ImmutableList; import com.google.genai.types.Part; import io.reactivex.rxjava3.core.Completable; @@ -39,6 +40,12 @@ public interface BaseArtifactService { Single saveArtifact( String appName, String userId, String sessionId, String filename, Part artifact); + /** Saves an artifact. */ + default Single saveArtifact(SessionKey sessionKey, String filename, Part artifact) { + return saveArtifact( + sessionKey.appName(), sessionKey.userId(), sessionKey.id(), filename, artifact); + } + /** * Saves an artifact and returns it with fileData if available. * @@ -58,18 +65,35 @@ default Single saveAndReloadArtifact( .flatMap(version -> loadArtifact(appName, userId, sessionId, filename, version).toSingle()); } + /** Saves an artifact and returns it with fileData if available. */ + default Single saveAndReloadArtifact( + SessionKey sessionKey, String filename, Part artifact) { + return saveAndReloadArtifact( + sessionKey.appName(), sessionKey.userId(), sessionKey.id(), filename, artifact); + } + /** Loads the latest version of an artifact from the service. */ default Maybe loadArtifact( String appName, String userId, String sessionId, String filename) { return loadArtifact(appName, userId, sessionId, filename, Optional.empty()); } + /** Loads the latest version of an artifact from the service. */ + default Maybe loadArtifact(SessionKey sessionKey, String filename) { + return loadArtifact(sessionKey.appName(), sessionKey.userId(), sessionKey.id(), filename); + } + /** Loads a specific version of an artifact from the service. */ default Maybe loadArtifact( String appName, String userId, String sessionId, String filename, int version) { return loadArtifact(appName, userId, sessionId, filename, Optional.of(version)); } + default Maybe loadArtifact(SessionKey sessionKey, String filename, int version) { + return loadArtifact( + sessionKey.appName(), sessionKey.userId(), sessionKey.id(), filename, version); + } + /** * @deprecated Use {@link #loadArtifact(String, String, String, String)} or {@link * #loadArtifact(String, String, String, String, int)} instead. @@ -88,6 +112,10 @@ Maybe loadArtifact( */ Single listArtifactKeys(String appName, String userId, String sessionId); + default Single listArtifactKeys(SessionKey sessionKey) { + return listArtifactKeys(sessionKey.appName(), sessionKey.userId(), sessionKey.id()); + } + /** * Deletes an artifact. * @@ -98,6 +126,10 @@ Maybe loadArtifact( */ Completable deleteArtifact(String appName, String userId, String sessionId, String filename); + default Completable deleteArtifact(SessionKey sessionKey, String filename) { + return deleteArtifact(sessionKey.appName(), sessionKey.userId(), sessionKey.id(), filename); + } + /** * Lists all the versions (as revision IDs) of an artifact. * @@ -109,4 +141,8 @@ Maybe loadArtifact( */ Single> listVersions( String appName, String userId, String sessionId, String filename); + + default Single> listVersions(SessionKey sessionKey, String filename) { + return listVersions(sessionKey.appName(), sessionKey.userId(), sessionKey.id(), filename); + } } diff --git a/core/src/main/java/com/google/adk/events/Event.java b/core/src/main/java/com/google/adk/events/Event.java index d968efa53..2677b635d 100644 --- a/core/src/main/java/com/google/adk/events/Event.java +++ b/core/src/main/java/com/google/adk/events/Event.java @@ -27,6 +27,7 @@ import com.google.common.collect.Iterables; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.genai.types.Content; +import com.google.genai.types.CustomMetadata; import com.google.genai.types.FinishReason; import com.google.genai.types.FunctionCall; import com.google.genai.types.FunctionResponse; @@ -48,20 +49,21 @@ public class Event extends JsonBaseModel { private String id; private String invocationId; private String author; - private Optional content = Optional.empty(); + private @Nullable Content content; private EventActions actions; - private Optional> longRunningToolIds = Optional.empty(); - private Optional partial = Optional.empty(); - private Optional turnComplete = Optional.empty(); - private Optional errorCode = Optional.empty(); - private Optional errorMessage = Optional.empty(); - private Optional finishReason = Optional.empty(); - private Optional usageMetadata = Optional.empty(); - private Optional avgLogprobs = Optional.empty(); - private Optional interrupted = Optional.empty(); - private Optional branch = Optional.empty(); - private Optional groundingMetadata = Optional.empty(); - private Optional modelVersion = Optional.empty(); + private @Nullable Set longRunningToolIds; + private @Nullable Boolean partial; + private @Nullable Boolean turnComplete; + private @Nullable FinishReason errorCode; + private @Nullable String errorMessage; + private @Nullable FinishReason finishReason; + private @Nullable GenerateContentResponseUsageMetadata usageMetadata; + private @Nullable Double avgLogprobs; + private @Nullable Boolean interrupted; + private @Nullable String branch; + private @Nullable GroundingMetadata groundingMetadata; + private @Nullable List customMetadata; + private @Nullable String modelVersion; private long timestamp; private Event() {} @@ -102,10 +104,10 @@ public void setAuthor(String author) { @JsonProperty("content") public Optional content() { - return content; + return Optional.ofNullable(content); } - public void setContent(Optional content) { + public void setContent(@Nullable Content content) { this.content = content; } @@ -124,10 +126,10 @@ public void setActions(EventActions actions) { */ @JsonProperty("longRunningToolIds") public Optional> longRunningToolIds() { - return longRunningToolIds; + return Optional.ofNullable(longRunningToolIds); } - public void setLongRunningToolIds(Optional> longRunningToolIds) { + public void setLongRunningToolIds(@Nullable Set longRunningToolIds) { this.longRunningToolIds = longRunningToolIds; } @@ -137,73 +139,79 @@ public void setLongRunningToolIds(Optional> longRunningToolIds) { */ @JsonProperty("partial") public Optional partial() { - return partial; + return Optional.ofNullable(partial); } - public void setPartial(Optional partial) { + public void setPartial(@Nullable Boolean partial) { this.partial = partial; } @JsonProperty("turnComplete") public Optional turnComplete() { - return turnComplete; + return Optional.ofNullable(turnComplete); } - public void setTurnComplete(Optional turnComplete) { + public void setTurnComplete(@Nullable Boolean turnComplete) { this.turnComplete = turnComplete; } @JsonProperty("errorCode") public Optional errorCode() { - return errorCode; + return Optional.ofNullable(errorCode); } @JsonProperty("finishReason") public Optional finishReason() { - return finishReason; + return Optional.ofNullable(finishReason); } - public void setErrorCode(Optional errorCode) { + public void setErrorCode(@Nullable FinishReason errorCode) { this.errorCode = errorCode; } + @Deprecated + @SuppressWarnings("checkstyle:IllegalType") public void setFinishReason(Optional finishReason) { + this.finishReason = finishReason.orElse(null); + } + + public void setFinishReason(@Nullable FinishReason finishReason) { this.finishReason = finishReason; } @JsonProperty("errorMessage") public Optional errorMessage() { - return errorMessage; + return Optional.ofNullable(errorMessage); } - public void setErrorMessage(Optional errorMessage) { + public void setErrorMessage(@Nullable String errorMessage) { this.errorMessage = errorMessage; } @JsonProperty("usageMetadata") public Optional usageMetadata() { - return usageMetadata; + return Optional.ofNullable(usageMetadata); } - public void setUsageMetadata(Optional usageMetadata) { + public void setUsageMetadata(@Nullable GenerateContentResponseUsageMetadata usageMetadata) { this.usageMetadata = usageMetadata; } @JsonProperty("avgLogprobs") public Optional avgLogprobs() { - return avgLogprobs; + return Optional.ofNullable(avgLogprobs); } - public void setAvgLogprobs(Optional avgLogprobs) { + public void setAvgLogprobs(@Nullable Double avgLogprobs) { this.avgLogprobs = avgLogprobs; } @JsonProperty("interrupted") public Optional interrupted() { - return interrupted; + return Optional.ofNullable(interrupted); } - public void setInterrupted(Optional interrupted) { + public void setInterrupted(@Nullable Boolean interrupted) { this.interrupted = interrupted; } @@ -214,7 +222,7 @@ public void setInterrupted(Optional interrupted) { */ @JsonProperty("branch") public Optional branch() { - return branch; + return Optional.ofNullable(branch); } /** @@ -225,30 +233,36 @@ public Optional branch() { * @param branch Branch identifier. */ public void branch(@Nullable String branch) { - this.branch = Optional.ofNullable(branch); - } - - public void branch(Optional branch) { this.branch = branch; } /** The grounding metadata of the event. */ @JsonProperty("groundingMetadata") public Optional groundingMetadata() { - return groundingMetadata; + return Optional.ofNullable(groundingMetadata); } - public void setGroundingMetadata(Optional groundingMetadata) { + public void setGroundingMetadata(@Nullable GroundingMetadata groundingMetadata) { this.groundingMetadata = groundingMetadata; } + /** The custom metadata of the event. */ + @JsonProperty("customMetadata") + public Optional> customMetadata() { + return Optional.ofNullable(customMetadata); + } + + public void setCustomMetadata(@Nullable List customMetadata) { + this.customMetadata = customMetadata; + } + /** The model version used to generate the response. */ @JsonProperty("modelVersion") public Optional modelVersion() { - return modelVersion; + return Optional.ofNullable(modelVersion); } - public void setModelVersion(Optional modelVersion) { + public void setModelVersion(@Nullable String modelVersion) { this.modelVersion = modelVersion; } @@ -333,21 +347,22 @@ public static class Builder { private String id; private String invocationId; private String author; - private Optional content = Optional.empty(); + private @Nullable Content content; private EventActions actions; - private Optional> longRunningToolIds = Optional.empty(); - private Optional partial = Optional.empty(); - private Optional turnComplete = Optional.empty(); - private Optional errorCode = Optional.empty(); - private Optional errorMessage = Optional.empty(); - private Optional finishReason = Optional.empty(); - private Optional usageMetadata = Optional.empty(); - private Optional avgLogprobs = Optional.empty(); - private Optional interrupted = Optional.empty(); - private Optional branch = Optional.empty(); - private Optional groundingMetadata = Optional.empty(); - private Optional modelVersion = Optional.empty(); - private Optional timestamp = Optional.empty(); + private @Nullable Set longRunningToolIds; + private @Nullable Boolean partial; + private @Nullable Boolean turnComplete; + private @Nullable FinishReason errorCode; + private @Nullable String errorMessage; + private @Nullable FinishReason finishReason; + private @Nullable GenerateContentResponseUsageMetadata usageMetadata; + private @Nullable Double avgLogprobs; + private @Nullable Boolean interrupted; + private @Nullable String branch; + private @Nullable GroundingMetadata groundingMetadata; + private @Nullable List customMetadata; + private @Nullable String modelVersion; + private @Nullable Long timestamp; @JsonCreator private static Builder create() { @@ -378,12 +393,6 @@ public Builder author(String value) { @CanIgnoreReturnValue @JsonProperty("content") public Builder content(@Nullable Content value) { - this.content = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder content(Optional value) { this.content = value; return this; } @@ -402,12 +411,6 @@ Optional actions() { @CanIgnoreReturnValue @JsonProperty("longRunningToolIds") public Builder longRunningToolIds(@Nullable Set value) { - this.longRunningToolIds = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder longRunningToolIds(Optional> value) { this.longRunningToolIds = value; return this; } @@ -415,12 +418,6 @@ public Builder longRunningToolIds(Optional> value) { @CanIgnoreReturnValue @JsonProperty("partial") public Builder partial(@Nullable Boolean value) { - this.partial = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder partial(Optional value) { this.partial = value; return this; } @@ -428,12 +425,6 @@ public Builder partial(Optional value) { @CanIgnoreReturnValue @JsonProperty("turnComplete") public Builder turnComplete(@Nullable Boolean value) { - this.turnComplete = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder turnComplete(Optional value) { this.turnComplete = value; return this; } @@ -441,12 +432,6 @@ public Builder turnComplete(Optional value) { @CanIgnoreReturnValue @JsonProperty("errorCode") public Builder errorCode(@Nullable FinishReason value) { - this.errorCode = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder errorCode(Optional value) { this.errorCode = value; return this; } @@ -454,12 +439,6 @@ public Builder errorCode(Optional value) { @CanIgnoreReturnValue @JsonProperty("errorMessage") public Builder errorMessage(@Nullable String value) { - this.errorMessage = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder errorMessage(Optional value) { this.errorMessage = value; return this; } @@ -467,12 +446,6 @@ public Builder errorMessage(Optional value) { @CanIgnoreReturnValue @JsonProperty("finishReason") public Builder finishReason(@Nullable FinishReason value) { - this.finishReason = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder finishReason(Optional value) { this.finishReason = value; return this; } @@ -480,12 +453,6 @@ public Builder finishReason(Optional value) { @CanIgnoreReturnValue @JsonProperty("usageMetadata") public Builder usageMetadata(@Nullable GenerateContentResponseUsageMetadata value) { - this.usageMetadata = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder usageMetadata(Optional value) { this.usageMetadata = value; return this; } @@ -493,12 +460,6 @@ public Builder usageMetadata(Optional valu @CanIgnoreReturnValue @JsonProperty("avgLogprobs") public Builder avgLogprobs(@Nullable Double value) { - this.avgLogprobs = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder avgLogprobs(Optional value) { this.avgLogprobs = value; return this; } @@ -506,12 +467,6 @@ public Builder avgLogprobs(Optional value) { @CanIgnoreReturnValue @JsonProperty("interrupted") public Builder interrupted(@Nullable Boolean value) { - this.interrupted = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder interrupted(Optional value) { this.interrupted = value; return this; } @@ -519,73 +474,52 @@ public Builder interrupted(Optional value) { @CanIgnoreReturnValue @JsonProperty("timestamp") public Builder timestamp(long value) { - this.timestamp = Optional.of(value); - return this; - } - - @CanIgnoreReturnValue - public Builder timestamp(Optional value) { this.timestamp = value; return this; } // Getter for builder's timestamp, used in build() Optional timestamp() { - return timestamp; + return Optional.ofNullable(timestamp); } @CanIgnoreReturnValue @JsonProperty("branch") public Builder branch(@Nullable String value) { - this.branch = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder branch(Optional value) { this.branch = value; return this; } // Getter for builder's branch, used in build() Optional branch() { - return branch; + return Optional.ofNullable(branch); } @CanIgnoreReturnValue @JsonProperty("groundingMetadata") public Builder groundingMetadata(@Nullable GroundingMetadata value) { - this.groundingMetadata = Optional.ofNullable(value); - return this; - } - - @CanIgnoreReturnValue - public Builder groundingMetadata(Optional value) { this.groundingMetadata = value; return this; } Optional groundingMetadata() { - return groundingMetadata; + return Optional.ofNullable(groundingMetadata); } @CanIgnoreReturnValue - @JsonProperty("modelVersion") - public Builder modelVersion(@Nullable String value) { - this.modelVersion = Optional.ofNullable(value); + @JsonProperty("customMetadata") + public Builder customMetadata(@Nullable List value) { + this.customMetadata = value; return this; } @CanIgnoreReturnValue - public Builder modelVersion(Optional value) { + @JsonProperty("modelVersion") + public Builder modelVersion(@Nullable String value) { this.modelVersion = value; return this; } - Optional modelVersion() { - return modelVersion; - } - public Event build() { Event event = new Event(); event.setId(id); @@ -603,6 +537,7 @@ public Event build() { event.setInterrupted(interrupted); event.branch(branch); event.setGroundingMetadata(groundingMetadata); + event.setCustomMetadata(customMetadata); event.setModelVersion(modelVersion); event.setActions(actions().orElseGet(() -> EventActions.builder().build())); event.setTimestamp(timestamp().orElseGet(() -> Instant.now().toEpochMilli())); @@ -639,6 +574,7 @@ public Builder toBuilder() { .interrupted(this.interrupted) .branch(this.branch) .groundingMetadata(this.groundingMetadata) + .customMetadata(this.customMetadata) .modelVersion(this.modelVersion); if (this.timestamp != 0) { builder.timestamp(this.timestamp); @@ -671,6 +607,7 @@ public boolean equals(Object obj) { && Objects.equals(interrupted, other.interrupted) && Objects.equals(branch, other.branch) && Objects.equals(groundingMetadata, other.groundingMetadata) + && Objects.equals(customMetadata, other.customMetadata) && Objects.equals(modelVersion, other.modelVersion); } @@ -698,6 +635,7 @@ public int hashCode() { interrupted, branch, groundingMetadata, + customMetadata, modelVersion, timestamp); } diff --git a/core/src/main/java/com/google/adk/flows/llmflows/AgentTransfer.java b/core/src/main/java/com/google/adk/flows/llmflows/AgentTransfer.java index 402f71c9d..0a0da8761 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/AgentTransfer.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/AgentTransfer.java @@ -55,22 +55,17 @@ public Single processRequest( .appendInstructions( ImmutableList.of(buildTargetAgentsInstructions(agent, transferTargets))); - // Note: this tool is not exposed to the LLM in GenerateContent request. It is there only to - // serve as a backwards-compatible instance for users who depend on the exact name of - // "transferToAgent". - builder.appendTools(ImmutableList.of(createTransferToAgentTool("legacyTransferToAgent"))); - - FunctionTool agentTransferTool = createTransferToAgentTool("transferToAgent"); + FunctionTool agentTransferTool = createTransferToAgentTool(); agentTransferTool.processLlmRequest(builder, ToolContext.builder(context).build()); return Single.just( RequestProcessor.RequestProcessingResult.create(builder.build(), ImmutableList.of())); } - private FunctionTool createTransferToAgentTool(String methodName) { + private FunctionTool createTransferToAgentTool() { Method transferToAgentMethod; try { transferToAgentMethod = - AgentTransfer.class.getMethod(methodName, String.class, ToolContext.class); + AgentTransfer.class.getMethod("transferToAgent", String.class, ToolContext.class); } catch (NoSuchMethodException e) { throw new IllegalStateException(e); } @@ -169,18 +164,4 @@ public static void transferToAgent( EventActions eventActions = toolContext.eventActions(); toolContext.setActions(eventActions.toBuilder().transferToAgent(agentName).build()); } - - /** - * Backwards compatible transferToAgent that uses camel-case naming instead of the ADK's - * snake_case convention. - * - *

It exists only to support users who already use literal "transferToAgent" function call to - * instruct ADK to transfer the question to another agent. - */ - @Schema(name = "transferToAgent") - public static void legacyTransferToAgent( - @Schema(name = "agentName") String agentName, - @Schema(optional = true) ToolContext toolContext) { - transferToAgent(agentName, toolContext); - } } diff --git a/core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java b/core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java index 549652e86..6ed9ccaa3 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/BaseLlmFlow.java @@ -351,7 +351,7 @@ private Flowable runOneStep(InvocationContext context) { .id(Event.generateEventId()) .invocationId(context.invocationId()) .author(context.agent().name()) - .branch(context.branch()) + .branch(context.branch().orElse(null)) .build(); mutableEventTemplate.setTimestamp(0L); @@ -535,7 +535,7 @@ public void onError(Throwable e) { Event.builder() .invocationId(invocationContext.invocationId()) .author(invocationContext.agent().name()) - .branch(invocationContext.branch()); + .branch(invocationContext.branch().orElse(null)); Flowable receiveFlow = connection @@ -577,13 +577,7 @@ public void onError(Throwable e) { .get() .content(event.content().get()); } - if (functionResponses.stream() - .anyMatch( - functionResponse -> - functionResponse - .name() - .orElse("") - .equals("transferToAgent")) + if (event.actions().transferToAgent().isPresent() || event.actions().endInvocation().orElse(false)) { sendTask.dispose(); connection.close(); @@ -645,17 +639,17 @@ private Event buildModelResponseEvent( Event baseEventForLlmResponse, LlmRequest llmRequest, LlmResponse llmResponse) { Event.Builder eventBuilder = baseEventForLlmResponse.toBuilder() - .content(llmResponse.content()) - .partial(llmResponse.partial()) - .errorCode(llmResponse.errorCode()) - .errorMessage(llmResponse.errorMessage()) - .interrupted(llmResponse.interrupted()) - .turnComplete(llmResponse.turnComplete()) - .groundingMetadata(llmResponse.groundingMetadata()) - .avgLogprobs(llmResponse.avgLogprobs()) - .finishReason(llmResponse.finishReason()) - .usageMetadata(llmResponse.usageMetadata()) - .modelVersion(llmResponse.modelVersion()); + .content(llmResponse.content().orElse(null)) + .partial(llmResponse.partial().orElse(null)) + .errorCode(llmResponse.errorCode().orElse(null)) + .errorMessage(llmResponse.errorMessage().orElse(null)) + .interrupted(llmResponse.interrupted().orElse(null)) + .turnComplete(llmResponse.turnComplete().orElse(null)) + .groundingMetadata(llmResponse.groundingMetadata().orElse(null)) + .avgLogprobs(llmResponse.avgLogprobs().orElse(null)) + .finishReason(llmResponse.finishReason().orElse(null)) + .usageMetadata(llmResponse.usageMetadata().orElse(null)) + .modelVersion(llmResponse.modelVersion().orElse(null)); Event event = eventBuilder.build(); @@ -667,7 +661,7 @@ private Event buildModelResponseEvent( Functions.getLongRunningFunctionCalls(event.functionCalls(), llmRequest.tools()); logger.debug("longRunningToolIds: {}", longRunningToolIds); if (!longRunningToolIds.isEmpty()) { - event.setLongRunningToolIds(Optional.of(longRunningToolIds)); + event.setLongRunningToolIds(longRunningToolIds); } } return event; diff --git a/core/src/main/java/com/google/adk/flows/llmflows/CodeExecution.java b/core/src/main/java/com/google/adk/flows/llmflows/CodeExecution.java index 1f99cf4a2..f7c3c51ef 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/CodeExecution.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/CodeExecution.java @@ -229,7 +229,7 @@ private static Flowable runPreProcessor( Event.builder() .invocationId(invocationContext.invocationId()) .author(llmAgent.name()) - .content(Optional.of(codeContent)) + .content(codeContent) .build(); return Flowable.defer( @@ -303,13 +303,13 @@ private static Flowable runPostProcessor( } String codeStr = codeStrOptional.get(); responseContent = responseContentBuilder.build(); - llmResponseBuilder.content(Optional.empty()); + llmResponseBuilder.content((Content) null); Event codeEvent = Event.builder() .invocationId(invocationContext.invocationId()) .author(llmAgent.name()) - .content(Optional.of(responseContent)) + .content(responseContent) .actions(EventActions.builder().build()) .build(); @@ -456,7 +456,7 @@ private static Single postProcessCodeExecutionResult( return Event.builder() .invocationId(invocationContext.invocationId()) .author(invocationContext.agent().name()) - .content(Optional.of(resultContent)) + .content(resultContent) .actions(eventActionsBuilder.build()) .build(); }); diff --git a/core/src/main/java/com/google/adk/flows/llmflows/Contents.java b/core/src/main/java/com/google/adk/flows/llmflows/Contents.java index f45461626..ca8e0a051 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/Contents.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/Contents.java @@ -169,7 +169,36 @@ private boolean isEmptyContent(Event event) { || content.role().get().isEmpty() || content.parts().isEmpty() || content.parts().get().isEmpty() - || content.parts().get().get(0).text().map(String::isEmpty).orElse(false)); + || content.parts().get().stream().allMatch(this::isPartInvisible)); + } + + /** + * Returns whether a part is invisible for LLM context. + * + *

A part is invisible if: + * + *

    + *
  • It has no meaningful content (text, inline_data, file_data, function_call, + * function_response, executable_code, or code_execution_result), OR + *
  • It is marked as a thought AND does not contain function_call or function_response + *
+ * + *

Function calls and responses are never invisible, even if marked as thought, because they + * represent actions that need to be executed or results that need to be processed. + * + * @param part the part to check. + * @return {@code true} if the part is invisible, {@code false} otherwise. + */ + private boolean isPartInvisible(Part part) { + if (part.functionCall().isPresent() || part.functionResponse().isPresent()) { + return false; + } + return part.thought().orElse(false) + || !(part.text().isPresent() + || part.inlineData().isPresent() + || part.fileData().isPresent() + || part.codeExecutionResult().isPresent() + || part.executableCode().isPresent()); } /** @@ -406,6 +435,11 @@ private static boolean isEventBelongsToBranch(Optional invocationBranchO * @return A new list of events with the appropriate rearrangement. */ private static List rearrangeEventsForLatestFunctionResponse(List events) { + if (events.size() < 2) { + // No need to process, since there is no function_call. + return events; + } + // TODO: b/412663475 - Handle parallel function calls within the same event. Currently, this // throws an error. if (events.isEmpty() || Iterables.getLast(events).functionResponses().isEmpty()) { @@ -559,7 +593,7 @@ private static List rearrangeEventsForAsyncFunctionResponsesInHistory( // Gemini 3 requires function calls to be grouped first and only then function responses: // FC1 FC2 FR1 FR2 - boolean shouldBufferResponseEvents = modelName.startsWith("gemini-3-"); + boolean shouldBufferResponseEvents = modelName.contains("gemini-3"); for (int i = 0; i < events.size(); i++) { Event event = events.get(i); @@ -726,9 +760,7 @@ private static Event mergeFunctionResponseEvents(List functionResponseEve } return baseEvent.toBuilder() - .content( - Optional.of( - Content.builder().role(baseContent.role().get()).parts(partsInMergedEvent).build())) + .content(Content.builder().role(baseContent.role().get()).parts(partsInMergedEvent).build()) .build(); } diff --git a/core/src/main/java/com/google/adk/flows/llmflows/Functions.java b/core/src/main/java/com/google/adk/flows/llmflows/Functions.java index 82813defa..ecc2bb412 100644 --- a/core/src/main/java/com/google/adk/flows/llmflows/Functions.java +++ b/core/src/main/java/com/google/adk/flows/llmflows/Functions.java @@ -122,7 +122,7 @@ public static void populateClientFunctionCallId(Event modelResponseEvent) { new IllegalStateException( "Content role is missing in event: " + modelResponseEvent.id())); Content newContent = Content.builder().role(role).parts(newParts).build(); - modelResponseEvent.setContent(Optional.of(newContent)); + modelResponseEvent.setContent(newContent); } } @@ -253,7 +253,8 @@ private static Function> getFunctionCallMapper( functionCall.id().map(toolConfirmations::get).orElse(null)) .build(); - Map functionArgs = functionCall.args().orElse(new HashMap<>()); + Map functionArgs = + functionCall.args().map(HashMap::new).orElse(new HashMap<>()); Maybe> maybeFunctionResult = maybeInvokeBeforeToolCall(invocationContext, tool, functionArgs, toolContext) @@ -467,8 +468,8 @@ private static Optional mergeParallelFunctionResponseEvents( .id(Event.generateEventId()) .invocationId(baseEvent.invocationId()) .author(baseEvent.author()) - .branch(baseEvent.branch()) - .content(Optional.of(Content.builder().role("user").parts(mergedParts).build())) + .branch(baseEvent.branch().orElse(null)) + .content(Content.builder().role("user").parts(mergedParts).build()) .actions(mergedActionsBuilder.build()) .timestamp(baseEvent.timestamp()) .build()); @@ -482,12 +483,8 @@ private static Maybe> maybeInvokeBeforeToolCall( if (invocationContext.agent() instanceof LlmAgent) { LlmAgent agent = (LlmAgent) invocationContext.agent(); - HashMap mutableFunctionArgs = new HashMap<>(functionArgs); - Maybe> pluginResult = - invocationContext - .pluginManager() - .beforeToolCallback(tool, mutableFunctionArgs, toolContext); + invocationContext.pluginManager().beforeToolCallback(tool, functionArgs, toolContext); List callbacks = agent.canonicalBeforeToolCallbacks(); if (callbacks.isEmpty()) { @@ -500,8 +497,7 @@ private static Maybe> maybeInvokeBeforeToolCall( Flowable.fromIterable(callbacks) .concatMapMaybe( callback -> - callback.call( - invocationContext, tool, mutableFunctionArgs, toolContext)) + callback.call(invocationContext, tool, functionArgs, toolContext)) .firstElement()); return pluginResult.switchIfEmpty(callbackResult); @@ -628,7 +624,7 @@ private static Event buildResponseEvent( .id(Event.generateEventId()) .invocationId(invocationContext.invocationId()) .author(invocationContext.agent().name()) - .branch(invocationContext.branch()) + .branch(invocationContext.branch().orElse(null)) .content(Content.builder().role("user").parts(partFunctionResponse).build()) .actions(toolContext.eventActions()) .build(); @@ -688,7 +684,7 @@ public static Optional generateRequestConfirmationEvent( Event.builder() .invocationId(invocationContext.invocationId()) .author(invocationContext.agent().name()) - .branch(invocationContext.branch()) + .branch(invocationContext.branch().orElse(null)) .content(contentBuilder.build()) .longRunningToolIds(longRunningToolIds) .build()); diff --git a/core/src/main/java/com/google/adk/models/GeminiLlmConnection.java b/core/src/main/java/com/google/adk/models/GeminiLlmConnection.java index 45d81b420..023bd85e7 100644 --- a/core/src/main/java/com/google/adk/models/GeminiLlmConnection.java +++ b/core/src/main/java/com/google/adk/models/GeminiLlmConnection.java @@ -203,7 +203,7 @@ private static LlmResponse createServerContentResponse(LiveServerContent serverC return builder .partial(serverContent.turnComplete().map(completed -> !completed).orElse(false)) .turnComplete(serverContent.turnComplete().orElse(false)) - .interrupted(serverContent.interrupted()) + .interrupted(serverContent.interrupted().orElse(null)) .build(); } diff --git a/core/src/main/java/com/google/adk/models/LlmResponse.java b/core/src/main/java/com/google/adk/models/LlmResponse.java index 6f8f3d785..1ca381e23 100644 --- a/core/src/main/java/com/google/adk/models/LlmResponse.java +++ b/core/src/main/java/com/google/adk/models/LlmResponse.java @@ -25,6 +25,7 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.genai.types.Candidate; import com.google.genai.types.Content; +import com.google.genai.types.CustomMetadata; import com.google.genai.types.FinishReason; import com.google.genai.types.GenerateContentResponse; import com.google.genai.types.GenerateContentResponsePromptFeedback; @@ -59,6 +60,14 @@ public abstract class LlmResponse extends JsonBaseModel { @JsonProperty("groundingMetadata") public abstract Optional groundingMetadata(); + /** + * Returns the custom metadata of the response, if available. + * + * @return An {@link Optional} containing a list of {@link CustomMetadata} or empty. + */ + @JsonProperty("customMetadata") + public abstract Optional> customMetadata(); + /** * Indicates whether the text content is part of a unfinished text stream. * @@ -119,71 +128,51 @@ static LlmResponse.Builder jacksonBuilder() { } @JsonProperty("content") - public abstract Builder content(Content content); - - public abstract Builder content(Optional content); + public abstract Builder content(@Nullable Content content); @JsonProperty("interrupted") public abstract Builder interrupted(@Nullable Boolean interrupted); - public abstract Builder interrupted(Optional interrupted); - @JsonProperty("groundingMetadata") public abstract Builder groundingMetadata(@Nullable GroundingMetadata groundingMetadata); - public abstract Builder groundingMetadata(Optional groundingMetadata); + @JsonProperty("customMetadata") + public abstract Builder customMetadata(@Nullable List customMetadata); @JsonProperty("partial") public abstract Builder partial(@Nullable Boolean partial); - public abstract Builder partial(Optional partial); - @JsonProperty("turnComplete") public abstract Builder turnComplete(@Nullable Boolean turnComplete); - public abstract Builder turnComplete(Optional turnComplete); - @JsonProperty("errorCode") public abstract Builder errorCode(@Nullable FinishReason errorCode); - public abstract Builder errorCode(Optional errorCode); - @JsonProperty("finishReason") public abstract Builder finishReason(@Nullable FinishReason finishReason); - public abstract Builder finishReason(Optional finishReason); - @JsonProperty("avgLogprobs") public abstract Builder avgLogprobs(@Nullable Double avgLogprobs); - public abstract Builder avgLogprobs(Optional avgLogprobs); - @JsonProperty("errorMessage") public abstract Builder errorMessage(@Nullable String errorMessage); - public abstract Builder errorMessage(Optional errorMessage); - @JsonProperty("usageMetadata") public abstract Builder usageMetadata( @Nullable GenerateContentResponseUsageMetadata usageMetadata); - public abstract Builder usageMetadata( - Optional usageMetadata); - @JsonProperty("modelVersion") public abstract Builder modelVersion(@Nullable String modelVersion); - public abstract Builder modelVersion(Optional modelVersion); - @CanIgnoreReturnValue public final Builder response(GenerateContentResponse response) { Optional> candidatesOpt = response.candidates(); if (candidatesOpt.isPresent() && !candidatesOpt.get().isEmpty()) { Candidate candidate = candidatesOpt.get().get(0); - this.finishReason(candidate.finishReason()); + this.finishReason(candidate.finishReason().orElse(null)); if (candidate.content().isPresent()) { this.content(candidate.content().get()); - this.groundingMetadata(candidate.groundingMetadata()); + this.groundingMetadata(candidate.groundingMetadata().orElse(null)); } else { candidate.finishReason().ifPresent(this::errorCode); candidate.finishMessage().ifPresent(this::errorMessage); @@ -202,8 +191,8 @@ public final Builder response(GenerateContentResponse response) { this.errorMessage("Unknown error."); } } - this.usageMetadata(response.usageMetadata()); - this.modelVersion(response.modelVersion()); + this.usageMetadata(response.usageMetadata().orElse(null)); + this.modelVersion(response.modelVersion().orElse(null)); return this; } diff --git a/core/src/main/java/com/google/adk/models/VertexCredentials.java b/core/src/main/java/com/google/adk/models/VertexCredentials.java index 5e36cbe95..93dc05ec1 100644 --- a/core/src/main/java/com/google/adk/models/VertexCredentials.java +++ b/core/src/main/java/com/google/adk/models/VertexCredentials.java @@ -38,16 +38,11 @@ public static Builder builder() { /** Builder for {@link VertexCredentials}. */ @AutoValue.Builder public abstract static class Builder { - public abstract Builder setProject(Optional value); public abstract Builder setProject(@Nullable String value); - public abstract Builder setLocation(Optional value); - public abstract Builder setLocation(@Nullable String value); - public abstract Builder setCredentials(Optional value); - public abstract Builder setCredentials(@Nullable GoogleCredentials value); public abstract VertexCredentials build(); diff --git a/core/src/main/java/com/google/adk/plugins/PluginManager.java b/core/src/main/java/com/google/adk/plugins/PluginManager.java index a63d9a402..56dea936a 100644 --- a/core/src/main/java/com/google/adk/plugins/PluginManager.java +++ b/core/src/main/java/com/google/adk/plugins/PluginManager.java @@ -23,6 +23,8 @@ import com.google.adk.models.LlmResponse; import com.google.adk.tools.BaseTool; import com.google.adk.tools.ToolContext; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; import com.google.genai.types.Content; import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Flowable; @@ -81,6 +83,21 @@ public Optional getPlugin(String pluginName) { return plugins.stream().filter(p -> p.getName().equals(pluginName)).findFirst(); } + /** + * Returns the list of registered plugins. + * + *

This method is intended for testing purposes only. + * + *

Note that it returns a copy of the plugins list to prevent modification of the original + * list. + * + * @return The list of registered plugins. + */ + @VisibleForTesting + public List getPlugins() { + return ImmutableList.copyOf(plugins); + } + // --- Callback Runners --- public Maybe runOnUserMessageCallback( diff --git a/core/src/main/java/com/google/adk/runner/Runner.java b/core/src/main/java/com/google/adk/runner/Runner.java index 0ddfdaea1..4371300fb 100644 --- a/core/src/main/java/com/google/adk/runner/Runner.java +++ b/core/src/main/java/com/google/adk/runner/Runner.java @@ -35,6 +35,7 @@ import com.google.adk.sessions.BaseSessionService; import com.google.adk.sessions.InMemorySessionService; import com.google.adk.sessions.Session; +import com.google.adk.sessions.SessionKey; import com.google.adk.summarizer.EventsCompactionConfig; import com.google.adk.summarizer.LlmEventSummarizer; import com.google.adk.summarizer.SlidingWindowEventCompactor; @@ -131,6 +132,13 @@ public Builder plugins(List plugins) { return this; } + @CanIgnoreReturnValue + public Builder plugins(Plugin... plugins) { + Preconditions.checkState(this.app == null, "plugins() cannot be called when app is set."); + this.plugins = ImmutableList.copyOf(plugins); + return this; + } + public Runner build() { BaseAgent buildAgent; String buildAppName; @@ -332,7 +340,7 @@ private Single appendNewMessageToSession( .id(Event.generateEventId()) .invocationId(invocationContext.invocationId()) .author("user") - .content(Optional.of(newMessage)); + .content(newMessage); // Add state delta if provided if (stateDelta != null && !stateDelta.isEmpty()) { @@ -383,6 +391,25 @@ public Flowable runAsync( .flatMapPublisher(session -> this.runAsyncImpl(session, newMessage, runConfig, stateDelta)); } + /** See {@link #runAsync(String, String, Content, RunConfig, Map)}. */ + public Flowable runAsync( + SessionKey sessionKey, + Content newMessage, + RunConfig runConfig, + @Nullable Map stateDelta) { + return runAsync(sessionKey.userId(), sessionKey.id(), newMessage, runConfig, stateDelta); + } + + /** See {@link #runAsync(String, String, Content, RunConfig, Map)}. */ + public Flowable runAsync(SessionKey sessionKey, Content newMessage, RunConfig runConfig) { + return runAsync(sessionKey, newMessage, runConfig, /* stateDelta= */ null); + } + + /** See {@link #runAsync(String, String, Content, RunConfig, Map)}. */ + public Flowable runAsync(SessionKey sessionKey, Content newMessage) { + return runAsync(sessionKey, newMessage, RunConfig.builder().build()); + } + /** See {@link #runAsync(String, String, Content, RunConfig, Map)}. */ public Flowable runAsync(String userId, String sessionId, Content newMessage) { return runAsync(userId, sessionId, newMessage, RunConfig.builder().build()); @@ -513,7 +540,7 @@ private Flowable runAgentWithFreshSession( .id(Event.generateEventId()) .invocationId(contextWithUpdatedSession.invocationId()) .author("model") - .content(Optional.of(content)) + .content(content) .build()); // Agent execution @@ -564,9 +591,9 @@ private void copySessionStates(Session source, Session target) { * @return invocation context configured for a live run. */ private InvocationContext newInvocationContextForLive( - Session session, Optional liveRequestQueue, RunConfig runConfig) { + Session session, @Nullable LiveRequestQueue liveRequestQueue, RunConfig runConfig) { RunConfig.Builder runConfigBuilder = RunConfig.builder(runConfig); - if (liveRequestQueue.isPresent()) { + if (liveRequestQueue != null) { // Default to AUDIO modality if not specified. if (CollectionUtils.isNullOrEmpty(runConfig.responseModalities())) { runConfigBuilder.setResponseModalities( @@ -587,8 +614,9 @@ private InvocationContext newInvocationContextForLive( InvocationContext.Builder builder = newInvocationContextBuilder(session) .runConfig(runConfigBuilder.build()) - .userContent(Content.fromParts()); - liveRequestQueue.ifPresent(builder::liveRequestQueue); + .userContent(Content.fromParts()) + .liveRequestQueue(liveRequestQueue); + return builder.build(); } @@ -616,7 +644,7 @@ public Flowable runLive( return Flowable.defer( () -> { InvocationContext invocationContext = - newInvocationContextForLive(session, Optional.of(liveRequestQueue), runConfig); + newInvocationContextForLive(session, liveRequestQueue, runConfig); Single invocationContextSingle; if (invocationContext.agent() instanceof LlmAgent agent) { @@ -671,6 +699,17 @@ public Flowable runLive( .flatMapPublisher(session -> this.runLive(session, liveRequestQueue, runConfig)); } + /** + * Retrieves the session and runs the agent in live mode. + * + * @return stream of events from the agent. + * @throws IllegalArgumentException if the session is not found. + */ + public Flowable runLive( + SessionKey sessionKey, LiveRequestQueue liveRequestQueue, RunConfig runConfig) { + return runLive(sessionKey.userId(), sessionKey.id(), liveRequestQueue, runConfig); + } + /** * Runs the agent asynchronously with a default user ID. * diff --git a/core/src/main/java/com/google/adk/sessions/BaseSessionService.java b/core/src/main/java/com/google/adk/sessions/BaseSessionService.java index 540153460..7a0885544 100644 --- a/core/src/main/java/com/google/adk/sessions/BaseSessionService.java +++ b/core/src/main/java/com/google/adk/sessions/BaseSessionService.java @@ -23,8 +23,10 @@ import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Single; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.annotation.Nullable; @@ -47,13 +49,47 @@ public interface BaseSessionService { * service should generate a unique ID. * @return The newly created {@link Session} instance. * @throws SessionException if creation fails. + * @deprecated Use {@link #createSession(String, String, Map, String)} instead. */ + @Deprecated Single createSession( String appName, String userId, @Nullable ConcurrentMap state, @Nullable String sessionId); + /** + * Creates a new session with the specified parameters. + * + * @param appName The name of the application associated with the session. + * @param userId The identifier for the user associated with the session. + * @param state An optional map representing the initial state of the session. Can be null or + * empty. + * @param sessionId An optional client-provided identifier for the session. If empty or null, the + * service should generate a unique ID. + * @return The newly created {@link Session} instance. + * @throws SessionException if creation fails. + */ + default Single createSession( + String appName, + String userId, + @Nullable Map state, + @Nullable String sessionId) { + return createSession(appName, userId, ensureConcurrentMap(state), sessionId); + } + + /** + * Creates a new session with the specified parameters. + * + * @param sessionKey The session key containing appName, userId and sessionId. + * @param state An optional map representing the initial state of the session. Can be null or + * empty. + */ + default Single createSession( + SessionKey sessionKey, @Nullable Map state) { + return createSession(sessionKey.appName(), sessionKey.userId(), state, sessionKey.id()); + } + /** * Creates a new session with the specified application name and user ID, using a default state * (null) and allowing the service to generate a unique session ID. @@ -70,6 +106,14 @@ default Single createSession(String appName, String userId) { return createSession(appName, userId, null, null); } + /** + * Creates a new session with the specified application name and user ID, using a default state + * (null) and allowing the service to generate a unique session ID. + */ + default Single createSession(SessionKey sessionKey) { + return createSession(sessionKey.appName(), sessionKey.userId(), null, sessionKey.id()); + } + /** * Retrieves a specific session, optionally filtering the events included. * @@ -86,6 +130,12 @@ default Single createSession(String appName, String userId) { Maybe getSession( String appName, String userId, String sessionId, Optional config); + /** Retrieves a specific session, optionally filtering the events included. */ + default Maybe getSession(SessionKey sessionKey, @Nullable GetSessionConfig config) { + return getSession( + sessionKey.appName(), sessionKey.userId(), sessionKey.id(), Optional.ofNullable(config)); + } + /** * Lists sessions associated with a specific application and user. * @@ -99,6 +149,11 @@ Maybe getSession( */ Single listSessions(String appName, String userId); + /** Lists sessions associated with a specific application and user. */ + default Single listSessions(SessionKey sessionKey) { + return listSessions(sessionKey.appName(), sessionKey.userId()); + } + /** * Deletes a specific session. * @@ -110,6 +165,11 @@ Maybe getSession( */ Completable deleteSession(String appName, String userId, String sessionId); + /** Deletes a specific session. */ + default Completable deleteSession(SessionKey sessionKey) { + return deleteSession(sessionKey.appName(), sessionKey.userId(), sessionKey.id()); + } + /** * Lists the events within a specific session. Supports pagination via the response object. * @@ -123,6 +183,11 @@ Maybe getSession( */ Single listEvents(String appName, String userId, String sessionId); + /** Lists the events within a specific session. */ + default Single listEvents(SessionKey sessionKey) { + return listEvents(sessionKey.appName(), sessionKey.userId(), sessionKey.id()); + } + /** * Closes a session. This is currently a placeholder and may involve finalizing session state or * performing cleanup actions in future implementations. The default implementation does nothing. @@ -165,21 +230,19 @@ default Single appendEvent(Session session, Event event) { EventActions actions = event.actions(); if (actions != null) { - ConcurrentMap stateDelta = actions.stateDelta(); - if (stateDelta != null && !stateDelta.isEmpty()) { - ConcurrentMap sessionState = session.state(); - if (sessionState != null) { - stateDelta.forEach( - (key, value) -> { - if (!key.startsWith(State.TEMP_PREFIX)) { - if (value == State.REMOVED) { - sessionState.remove(key); - } else { - sessionState.put(key, value); - } + Map stateDelta = actions.stateDelta(); + Map sessionState = session.state(); + if (stateDelta != null && !stateDelta.isEmpty() && sessionState != null) { + stateDelta.forEach( + (key, value) -> { + if (!key.startsWith(State.TEMP_PREFIX)) { + if (value == State.REMOVED) { + sessionState.remove(key); + } else { + sessionState.put(key, value); } - }); - } + } + }); } } @@ -190,4 +253,21 @@ default Single appendEvent(Session session, Event event) { return Single.just(event); } + + /** + * Ensures the given {@link Map} is a {@link ConcurrentMap}. If the input is null, returns null. + * If the input is already a {@link ConcurrentMap}, it is cast and returned. Otherwise, a new + * {@link ConcurrentHashMap} is created from the input map. + */ + @Nullable + private static ConcurrentMap ensureConcurrentMap( + @Nullable Map state) { + if (state == null) { + return null; + } + if (state instanceof ConcurrentMap concurrentMap) { + return concurrentMap; + } + return new ConcurrentHashMap<>(state); + } } diff --git a/core/src/main/java/com/google/adk/sessions/InMemorySessionService.java b/core/src/main/java/com/google/adk/sessions/InMemorySessionService.java index 060fcaf60..b2a584b11 100644 --- a/core/src/main/java/com/google/adk/sessions/InMemorySessionService.java +++ b/core/src/main/java/com/google/adk/sessions/InMemorySessionService.java @@ -71,6 +71,15 @@ public Single createSession( String userId, @Nullable ConcurrentMap state, @Nullable String sessionId) { + return createSession(appName, userId, (Map) state, sessionId); + } + + @Override + public Single createSession( + String appName, + String userId, + @Nullable Map state, + @Nullable String sessionId) { Objects.requireNonNull(appName, "appName cannot be null"); Objects.requireNonNull(userId, "userId cannot be null"); @@ -83,7 +92,6 @@ public Single createSession( // Ensure state map and events list are mutable for the new session ConcurrentMap initialState = (state == null) ? new ConcurrentHashMap<>() : new ConcurrentHashMap<>(state); - List initialEvents = new ArrayList<>(); // Assuming Session constructor or setters allow setting these mutable collections Session newSession = @@ -91,7 +99,6 @@ public Single createSession( .appName(appName) .userId(userId) .state(initialState) - .events(initialEvents) .lastUpdateTime(Instant.now()) .build(); diff --git a/core/src/main/java/com/google/adk/sessions/Session.java b/core/src/main/java/com/google/adk/sessions/Session.java index 3bf27b55e..f8376589a 100644 --- a/core/src/main/java/com/google/adk/sessions/Session.java +++ b/core/src/main/java/com/google/adk/sessions/Session.java @@ -25,6 +25,7 @@ import java.time.Duration; import java.time.Instant; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -48,19 +49,31 @@ public static Builder builder(String id) { return new Builder(id); } + /** Creates a new {@link Builder} with the given session key. */ + public static Builder builder(SessionKey sessionKey) { + return new Builder(sessionKey); + } + /** Builder for {@link Session}. */ public static final class Builder { private String id; private String appName; private String userId; private State state = new State(new ConcurrentHashMap<>()); - private List events = new ArrayList<>(); + private List events = Collections.synchronizedList(new ArrayList<>()); private Instant lastUpdateTime = Instant.EPOCH; public Builder(String id) { this.id = id; } + /** Creates a new {@link Builder} with the given session key. */ + public Builder(SessionKey sessionKey) { + this.id = sessionKey.id(); + this.appName = sessionKey.appName(); + this.userId = sessionKey.userId(); + } + @JsonCreator private Builder() {} @@ -71,6 +84,15 @@ public Builder id(String id) { return this; } + /** Sets the session key. */ + @CanIgnoreReturnValue + public Builder sessionKey(SessionKey sessionKey) { + this.id = sessionKey.id(); + this.appName = sessionKey.appName(); + this.userId = sessionKey.userId(); + return this; + } + @CanIgnoreReturnValue public Builder state(State state) { this.state = state; @@ -101,7 +123,7 @@ public Builder userId(String userId) { @CanIgnoreReturnValue @JsonProperty("events") public Builder events(List events) { - this.events = events; + this.events = Collections.synchronizedList(events); return this; } @@ -129,6 +151,11 @@ public Session build() { } } + /** Returns the session key. */ + public SessionKey sessionKey() { + return new SessionKey(appName, userId, id); + } + @JsonProperty("id") public String id() { return id; diff --git a/core/src/main/java/com/google/adk/sessions/SessionJsonConverter.java b/core/src/main/java/com/google/adk/sessions/SessionJsonConverter.java index 71b072695..97cc0f56d 100644 --- a/core/src/main/java/com/google/adk/sessions/SessionJsonConverter.java +++ b/core/src/main/java/com/google/adk/sessions/SessionJsonConverter.java @@ -208,9 +208,12 @@ static Event fromApiEvent(Map apiEvent) { .timestamp(convertToInstant(apiEvent.get("timestamp")).toEpochMilli()) .errorCode( Optional.ofNullable(apiEvent.get("errorCode")) - .map(value -> new FinishReason((String) value))) + .map(value -> new FinishReason((String) value)) + .orElse(null)) .errorMessage( - Optional.ofNullable(apiEvent.get("errorMessage")).map(value -> (String) value)) + Optional.ofNullable(apiEvent.get("errorMessage")) + .map(value -> (String) value) + .orElse(null)) .build(); Map eventMetadata = (Map) apiEvent.get("eventMetadata"); if (eventMetadata != null) { @@ -236,7 +239,7 @@ static Event fromApiEvent(Map apiEvent) { Optional.ofNullable((Boolean) eventMetadata.get("turnComplete")).orElse(false)) .interrupted( Optional.ofNullable((Boolean) eventMetadata.get("interrupted")).orElse(false)) - .branch(Optional.ofNullable((String) eventMetadata.get("branch"))) + .branch((String) eventMetadata.get("branch")) .groundingMetadata(groundingMetadata) .usageMetadata(usageMetadata) .longRunningToolIds( diff --git a/core/src/main/java/com/google/adk/sessions/SessionKey.java b/core/src/main/java/com/google/adk/sessions/SessionKey.java new file mode 100644 index 000000000..db26b5a3a --- /dev/null +++ b/core/src/main/java/com/google/adk/sessions/SessionKey.java @@ -0,0 +1,82 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.adk.sessions; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.adk.JsonBaseModel; +import java.util.Objects; + +/** Key for a session, composed of appName, userId and session id. */ +public final class SessionKey extends JsonBaseModel { + private final String appName; + private final String userId; + private final String id; + + @JsonCreator + public SessionKey( + @JsonProperty("appName") String appName, + @JsonProperty("userId") String userId, + @JsonProperty("id") String id) { + this.appName = appName; + this.userId = userId; + this.id = id; + } + + @JsonProperty("appName") + public String appName() { + return appName; + } + + @JsonProperty("userId") + public String userId() { + return userId; + } + + @JsonProperty("id") + public String id() { + return id; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SessionKey that = (SessionKey) o; + return Objects.equals(appName, that.appName) + && Objects.equals(userId, that.userId) + && Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(appName, userId, id); + } + + @Override + public String toString() { + return toJson(); + } + + public static SessionKey fromJson(String json) { + return fromJsonString(json, SessionKey.class); + } +} diff --git a/core/src/main/java/com/google/adk/sessions/VertexAiClient.java b/core/src/main/java/com/google/adk/sessions/VertexAiClient.java index d35bbccae..718738b92 100644 --- a/core/src/main/java/com/google/adk/sessions/VertexAiClient.java +++ b/core/src/main/java/com/google/adk/sessions/VertexAiClient.java @@ -14,10 +14,10 @@ import io.reactivex.rxjava3.core.Single; import java.io.IOException; import java.io.UncheckedIOException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import java.util.concurrent.TimeoutException; import javax.annotation.Nullable; import okhttp3.ResponseBody; @@ -51,8 +51,8 @@ final class VertexAiClient { } Maybe createSession( - String reasoningEngineId, String userId, ConcurrentMap state) { - ConcurrentHashMap sessionJsonMap = new ConcurrentHashMap<>(); + String reasoningEngineId, String userId, Map state) { + Map sessionJsonMap = new HashMap<>(); sessionJsonMap.put("userId", userId); if (state != null) { sessionJsonMap.put("sessionState", state); diff --git a/core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java b/core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java index 7878daf22..2fff7a752 100644 --- a/core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java +++ b/core/src/main/java/com/google/adk/sessions/VertexAiSessionService.java @@ -76,6 +76,15 @@ public Single createSession( String userId, @Nullable ConcurrentMap state, @Nullable String sessionId) { + return createSession(appName, userId, (Map) state, sessionId); + } + + @Override + public Single createSession( + String appName, + String userId, + @Nullable Map state, + @Nullable String sessionId) { String reasoningEngineId = parseReasoningEngineId(appName); return client diff --git a/core/src/main/java/com/google/adk/summarizer/EventsCompactionConfig.java b/core/src/main/java/com/google/adk/summarizer/EventsCompactionConfig.java index b61cd2008..39698c3db 100644 --- a/core/src/main/java/com/google/adk/summarizer/EventsCompactionConfig.java +++ b/core/src/main/java/com/google/adk/summarizer/EventsCompactionConfig.java @@ -16,6 +16,8 @@ package com.google.adk.summarizer; +import com.google.auto.value.AutoBuilder; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import javax.annotation.Nullable; /** @@ -39,6 +41,35 @@ public record EventsCompactionConfig( @Nullable Integer tokenThreshold, @Nullable Integer eventRetentionSize) { + public static Builder builder() { + return new AutoBuilder_EventsCompactionConfig_Builder(); + } + + public Builder toBuilder() { + return new AutoBuilder_EventsCompactionConfig_Builder(this); + } + + /** Builder for {@link EventsCompactionConfig}. */ + @AutoBuilder + public abstract static class Builder { + @CanIgnoreReturnValue + public abstract Builder compactionInterval(@Nullable Integer compactionInterval); + + @CanIgnoreReturnValue + public abstract Builder overlapSize(@Nullable Integer overlapSize); + + @CanIgnoreReturnValue + public abstract Builder summarizer(@Nullable BaseEventSummarizer summarizer); + + @CanIgnoreReturnValue + public abstract Builder tokenThreshold(@Nullable Integer tokenThreshold); + + @CanIgnoreReturnValue + public abstract Builder eventRetentionSize(@Nullable Integer eventRetentionSize); + + public abstract EventsCompactionConfig build(); + } + public EventsCompactionConfig(int compactionInterval, int overlapSize) { this(compactionInterval, overlapSize, null, null, null); } diff --git a/core/src/main/java/com/google/adk/tools/BaseToolset.java b/core/src/main/java/com/google/adk/tools/BaseToolset.java index 4d3482c57..40167aa75 100644 --- a/core/src/main/java/com/google/adk/tools/BaseToolset.java +++ b/core/src/main/java/com/google/adk/tools/BaseToolset.java @@ -19,7 +19,7 @@ import com.google.adk.agents.ReadonlyContext; import io.reactivex.rxjava3.core.Flowable; import java.util.List; -import java.util.Optional; +import javax.annotation.Nullable; /** Base interface for toolsets. */ public interface BaseToolset extends AutoCloseable { @@ -43,28 +43,26 @@ public interface BaseToolset extends AutoCloseable { void close() throws Exception; /** - * Helper method to be used by implementers that returns true if the given tool is in the provided - * list of tools of if testing against the given ToolPredicate returns true (otherwise false). + * Checks if a tool should be selected based on a filter. * * @param tool The tool to check. - * @param toolFilter An Optional containing either a ToolPredicate or a List of tool names. - * @param readonlyContext The current context. - * @return true if the tool is selected. + * @param toolFilter A ToolPredicate, a List of tool names, or null. + * @param readonlyContext The context for checking the tool, or null. */ default boolean isToolSelected( - BaseTool tool, Optional toolFilter, Optional readonlyContext) { - if (toolFilter.isEmpty()) { + BaseTool tool, @Nullable Object toolFilter, @Nullable ReadonlyContext readonlyContext) { + if (toolFilter == null) { return true; } - Object filter = toolFilter.get(); - if (filter instanceof ToolPredicate toolPredicate) { + + if (toolFilter instanceof ToolPredicate toolPredicate) { return toolPredicate.test(tool, readonlyContext); } - if (filter instanceof List) { - @SuppressWarnings("unchecked") - List toolNames = (List) filter; + + if (toolFilter instanceof List toolNames) { return toolNames.contains(tool.name()); } + return false; } } diff --git a/core/src/main/java/com/google/adk/tools/BuiltInCodeExecutionTool.java b/core/src/main/java/com/google/adk/tools/BuiltInCodeExecutionTool.java index 060b3ffb8..ad97b96a6 100644 --- a/core/src/main/java/com/google/adk/tools/BuiltInCodeExecutionTool.java +++ b/core/src/main/java/com/google/adk/tools/BuiltInCodeExecutionTool.java @@ -16,13 +16,19 @@ package com.google.adk.tools; +import com.google.adk.agents.LlmAgent; +import com.google.adk.models.BaseLlm; import com.google.adk.models.LlmRequest; +import com.google.adk.utils.ModelNameUtils; import com.google.common.collect.ImmutableList; import com.google.genai.types.GenerateContentConfig; import com.google.genai.types.Tool; import com.google.genai.types.ToolCodeExecution; import io.reactivex.rxjava3.core.Completable; import java.util.List; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A built-in code execution tool that is automatically invoked by Gemini 2 models. @@ -32,6 +38,7 @@ */ public final class BuiltInCodeExecutionTool extends BaseTool { public static final BuiltInCodeExecutionTool INSTANCE = new BuiltInCodeExecutionTool(); + private static final Logger LOG = LoggerFactory.getLogger(BuiltInCodeExecutionTool.class); public BuiltInCodeExecutionTool() { super("code_execution", "code_execution"); @@ -41,10 +48,28 @@ public BuiltInCodeExecutionTool() { public Completable processLlmRequest( LlmRequest.Builder llmRequestBuilder, ToolContext toolContext) { - String model = llmRequestBuilder.build().model().get(); - if (model.isEmpty() || !model.startsWith("gemini-2")) { - return Completable.error( - new IllegalArgumentException("Code execution tool is not supported for model " + model)); + Optional model = + Optional.ofNullable(toolContext) + .flatMap(tCtx -> Optional.ofNullable(tCtx.invocationContext())) + .flatMap( + iCtx -> { + if (iCtx.agent() instanceof LlmAgent llmAgent) { + return Optional.of(llmAgent); + } else { + return Optional.empty(); + } + }) + .flatMap(llmAgent -> llmAgent.resolvedModel().model()); + + String modelName = llmRequestBuilder.build().model().get(); + if (!ModelNameUtils.isGeminiModel(modelName) + || model.filter(ModelNameUtils::isInstanceOfGemini).isEmpty()) { + // model name is not a gemini model, or the model isn't an instance of Gemini class (eg. + // LangChain case). + LOG.warn( + "Code execution tool is not supported for model: {} ({}).", + modelName, + model.map(Object::getClass).map(Class::toString).orElse("")); } GenerateContentConfig.Builder configBuilder = llmRequestBuilder diff --git a/core/src/main/java/com/google/adk/tools/FunctionCallingUtils.java b/core/src/main/java/com/google/adk/tools/FunctionCallingUtils.java index eec115127..3244b40e9 100644 --- a/core/src/main/java/com/google/adk/tools/FunctionCallingUtils.java +++ b/core/src/main/java/com/google/adk/tools/FunctionCallingUtils.java @@ -45,7 +45,7 @@ public final class FunctionCallingUtils { private static final Logger logger = LoggerFactory.getLogger(FunctionCallingUtils.class); - private static final ObjectMapper objectMapper = JsonBaseModel.getMapper(); + private static final ObjectMapper defaultObjectMapper = JsonBaseModel.getMapper(); /** Holds the state during a single schema generation process to handle caching and recursion. */ private static class SchemaGenerationContext { @@ -162,7 +162,20 @@ private static Schema buildSchemaFromParameter(Parameter param) { * @throws IllegalArgumentException if a type is encountered that cannot be serialized by Jackson. */ public static Schema buildSchemaFromType(Type type) { - return buildSchemaRecursive(objectMapper.constructType(type), new SchemaGenerationContext()); + return buildSchemaFromType(type, defaultObjectMapper); + } + + /** + * Builds a Schema from a Java Type, creating a new context for the generation process. + * + * @param type The Java {@link Type} to convert into a Schema. + * @param objectMapper The {@link ObjectMapper} to use for introspecting types. + * @return The generated {@link Schema}. + * @throws IllegalArgumentException if a type is encountered that cannot be serialized by Jackson. + */ + public static Schema buildSchemaFromType(Type type, ObjectMapper objectMapper) { + return buildSchemaRecursive( + objectMapper.constructType(type), new SchemaGenerationContext(), objectMapper); } /** @@ -173,7 +186,8 @@ public static Schema buildSchemaFromType(Type type) { * @return The generated {@link Schema}. * @throws IllegalArgumentException if a type is encountered that cannot be serialized by Jackson. */ - private static Schema buildSchemaRecursive(JavaType javaType, SchemaGenerationContext context) { + private static Schema buildSchemaRecursive( + JavaType javaType, SchemaGenerationContext context, ObjectMapper objectMapper) { if (context.isProcessing(javaType)) { logger.warn("Type {} is recursive. Omitting from schema.", javaType.toCanonical()); return Schema.builder() @@ -194,7 +208,9 @@ private static Schema buildSchemaRecursive(JavaType javaType, SchemaGenerationCo Class rawClass = javaType.getRawClass(); if (javaType.isCollectionLikeType() && List.class.isAssignableFrom(rawClass)) { - builder.type("ARRAY").items(buildSchemaRecursive(javaType.getContentType(), context)); + builder + .type("ARRAY") + .items(buildSchemaRecursive(javaType.getContentType(), context, objectMapper)); } else if (javaType.isMapLikeType()) { builder.type("OBJECT"); } else if (String.class.equals(rawClass)) { @@ -232,7 +248,8 @@ private static Schema buildSchemaRecursive(JavaType javaType, SchemaGenerationCo for (BeanPropertyDefinition property : beanDescription.findProperties()) { AnnotatedMember member = property.getPrimaryMember(); if (member != null) { - properties.put(property.getName(), buildSchemaRecursive(member.getType(), context)); + properties.put( + property.getName(), buildSchemaRecursive(member.getType(), context, objectMapper)); if (property.isRequired()) { required.add(property.getName()); } diff --git a/core/src/main/java/com/google/adk/tools/FunctionTool.java b/core/src/main/java/com/google/adk/tools/FunctionTool.java index a6167ee46..4323b4569 100644 --- a/core/src/main/java/com/google/adk/tools/FunctionTool.java +++ b/core/src/main/java/com/google/adk/tools/FunctionTool.java @@ -44,12 +44,12 @@ public class FunctionTool extends BaseTool { private static final Logger logger = LoggerFactory.getLogger(FunctionTool.class); - private static final ObjectMapper objectMapper = JsonBaseModel.getMapper(); private final @Nullable Object instance; private final Method func; private final FunctionDeclaration funcDeclaration; private final boolean requireConfirmation; + private final ObjectMapper objectMapper; public static FunctionTool create(Object instance, Method func) { return create(instance, func, /* requireConfirmation= */ false); @@ -166,11 +166,26 @@ private static boolean wasCompiledWithDefaultParameterNames(Method func) { } protected FunctionTool(@Nullable Object instance, Method func, boolean isLongRunning) { - this(instance, func, isLongRunning, /* requireConfirmation= */ false); + this( + instance, func, isLongRunning, /* requireConfirmation= */ false, JsonBaseModel.getMapper()); } protected FunctionTool( @Nullable Object instance, Method func, boolean isLongRunning, boolean requireConfirmation) { + this(instance, func, isLongRunning, requireConfirmation, JsonBaseModel.getMapper()); + } + + protected FunctionTool( + @Nullable Object instance, Method func, boolean isLongRunning, ObjectMapper objectMapper) { + this(instance, func, isLongRunning, /* requireConfirmation= */ false, objectMapper); + } + + protected FunctionTool( + @Nullable Object instance, + Method func, + boolean isLongRunning, + boolean requireConfirmation, + ObjectMapper objectMapper) { super( func.isAnnotationPresent(Annotations.Schema.class) && !func.getAnnotation(Annotations.Schema.class).name().isEmpty() @@ -193,6 +208,7 @@ protected FunctionTool( FunctionCallingUtils.buildFunctionDeclaration( this.func, ImmutableList.of("toolContext", "inputStream")); this.requireConfirmation = requireConfirmation; + this.objectMapper = objectMapper; } @Override @@ -365,7 +381,7 @@ private static Class getTypeClass(Type type, String paramName) { } } - private static List createList(List values, Class type) { + private List createList(List values, Class type) { List list = new ArrayList<>(); // List of parameterized type is not supported. if (type == null) { @@ -387,7 +403,7 @@ private static List createList(List values, Class type) { return list; } - private static Object castValue(Object value, Class type) { + private Object castValue(Object value, Class type) { if (type.equals(Integer.class) || type.equals(int.class)) { if (value instanceof Integer) { return value; diff --git a/core/src/main/java/com/google/adk/tools/GoogleSearchTool.java b/core/src/main/java/com/google/adk/tools/GoogleSearchTool.java index 6f89754cf..b4f298c21 100644 --- a/core/src/main/java/com/google/adk/tools/GoogleSearchTool.java +++ b/core/src/main/java/com/google/adk/tools/GoogleSearchTool.java @@ -20,12 +20,9 @@ import com.google.common.collect.ImmutableList; import com.google.genai.types.GenerateContentConfig; import com.google.genai.types.GoogleSearch; -import com.google.genai.types.GoogleSearchRetrieval; import com.google.genai.types.Tool; import io.reactivex.rxjava3.core.Completable; import java.util.List; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * A built-in tool that is automatically invoked by Gemini 2 and 3 models to retrieve search results @@ -43,7 +40,6 @@ * } */ public final class GoogleSearchTool extends BaseTool { - private static final Logger logger = LoggerFactory.getLogger(GoogleSearchTool.class); public static final GoogleSearchTool INSTANCE = new GoogleSearchTool(); public GoogleSearchTool() { @@ -66,17 +62,7 @@ public Completable processLlmRequest( updatedToolsBuilder.addAll(existingTools); String model = llmRequestBuilder.build().model().get(); - if (model != null && model.startsWith("gemini-1")) { - if (!updatedToolsBuilder.build().isEmpty()) { - logger.error("Tools already present: {}", configBuilder.build().tools().get()); - return Completable.error( - new IllegalArgumentException( - "Google search tool cannot be used with other tools in Gemini 1.x.")); - } - updatedToolsBuilder.add( - Tool.builder().googleSearchRetrieval(GoogleSearchRetrieval.builder().build()).build()); - configBuilder.tools(updatedToolsBuilder.build()); - } else if (model != null && (model.startsWith("gemini-2") || model.startsWith("gemini-3"))) { + if (model != null && (model.startsWith("gemini-2") || model.startsWith("gemini-3"))) { updatedToolsBuilder.add(Tool.builder().googleSearch(GoogleSearch.builder().build()).build()); configBuilder.tools(updatedToolsBuilder.build()); diff --git a/core/src/main/java/com/google/adk/tools/ToolPredicate.java b/core/src/main/java/com/google/adk/tools/ToolPredicate.java index 86d739e70..6adf53c18 100644 --- a/core/src/main/java/com/google/adk/tools/ToolPredicate.java +++ b/core/src/main/java/com/google/adk/tools/ToolPredicate.java @@ -18,6 +18,7 @@ import com.google.adk.agents.ReadonlyContext; import java.util.Optional; +import javax.annotation.Nullable; /** * Functional interface to decide whether a tool should be exposed to the LLM based on the current @@ -31,6 +32,19 @@ public interface ToolPredicate { * @param tool The tool to check. * @param readonlyContext The current context. * @return true if the tool should be selected, false otherwise. + * @deprecated Use {@link #test(BaseTool, ReadonlyContext)} instead. */ + @Deprecated boolean test(BaseTool tool, Optional readonlyContext); + + /** + * Decides if the given tool is selected. + * + * @param tool The tool to check. + * @param readonlyContext The current context. + * @return true if the tool should be selected, false otherwise. + */ + default boolean test(BaseTool tool, @Nullable ReadonlyContext readonlyContext) { + return test(tool, Optional.ofNullable(readonlyContext)); + } } diff --git a/core/src/main/java/com/google/adk/tools/mcp/McpAsyncToolset.java b/core/src/main/java/com/google/adk/tools/mcp/McpAsyncToolset.java index 45a2fe333..73af9cc6a 100644 --- a/core/src/main/java/com/google/adk/tools/mcp/McpAsyncToolset.java +++ b/core/src/main/java/com/google/adk/tools/mcp/McpAsyncToolset.java @@ -170,9 +170,7 @@ public Flowable getTools(ReadonlyContext readonlyContext) { .map( tools -> tools.stream() - .filter( - tool -> - isToolSelected(tool, toolFilter, Optional.ofNullable(readonlyContext))) + .filter(tool -> isToolSelected(tool, toolFilter.orElse(null), readonlyContext)) .toList()) .onErrorResumeNext( err -> { diff --git a/core/src/main/java/com/google/adk/tools/mcp/McpServerLogConsumer.java b/core/src/main/java/com/google/adk/tools/mcp/McpServerLogConsumer.java index 6fe16b923..921197061 100644 --- a/core/src/main/java/com/google/adk/tools/mcp/McpServerLogConsumer.java +++ b/core/src/main/java/com/google/adk/tools/mcp/McpServerLogConsumer.java @@ -24,10 +24,11 @@ class McpServerLogConsumer implements Consumer { + private static final Logger LOG = LoggerFactory.getLogger(McpServerLogConsumer.class); + @Override public void accept(LoggingMessageNotification notif) { - Logger log = LoggerFactory.getLogger(notif.logger()); - log.atLevel(convert(notif.level())).log("{}", notif.data()); + LOG.atLevel(convert(notif.level())).log("{}", notif.data()); } private Level convert(McpSchema.LoggingLevel level) { diff --git a/core/src/main/java/com/google/adk/tools/mcp/McpSessionManager.java b/core/src/main/java/com/google/adk/tools/mcp/McpSessionManager.java index eabed9c6a..65eb59fe9 100644 --- a/core/src/main/java/com/google/adk/tools/mcp/McpSessionManager.java +++ b/core/src/main/java/com/google/adk/tools/mcp/McpSessionManager.java @@ -20,12 +20,15 @@ import io.modelcontextprotocol.client.McpClient; import io.modelcontextprotocol.client.McpSyncClient; import io.modelcontextprotocol.spec.McpClientTransport; +import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; import io.modelcontextprotocol.spec.McpSchema.InitializeResult; import java.time.Duration; import java.util.Optional; +import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; /** * Manages MCP client sessions. @@ -113,6 +116,16 @@ public static McpAsyncClient initializeAsyncSession( initializationTimeout == null ? Duration.ofMinutes(5) : initializationTimeout) .requestTimeout(requestTimeout == null ? Duration.ofMinutes(5) : requestTimeout) .capabilities(ClientCapabilities.builder().build()) + .loggingConsumer(asyncMcpServerLogConsumer()) .build(); } + + private static Function> + asyncMcpServerLogConsumer() { + var syncConsumer = new McpServerLogConsumer(); + return message -> { + syncConsumer.accept(message); + return Mono.empty(); + }; + } } diff --git a/core/src/main/java/com/google/adk/tools/mcp/McpToolset.java b/core/src/main/java/com/google/adk/tools/mcp/McpToolset.java index 3bf8f39d0..207243ceb 100644 --- a/core/src/main/java/com/google/adk/tools/mcp/McpToolset.java +++ b/core/src/main/java/com/google/adk/tools/mcp/McpToolset.java @@ -216,9 +216,7 @@ public Flowable getTools(ReadonlyContext readonlyContext) { new McpTool( tool, this.mcpSession, this.mcpSessionManager, this.objectMapper)) .filter( - tool -> - isToolSelected( - tool, toolFilter, Optional.ofNullable(readonlyContext)))); + tool -> isToolSelected(tool, toolFilter.orElse(null), readonlyContext))); }) .retryWhen( errorObservable -> diff --git a/core/src/main/java/com/google/adk/tools/retrieval/VertexAiRagRetrieval.java b/core/src/main/java/com/google/adk/tools/retrieval/VertexAiRagRetrieval.java index b36a05d10..16f11a1f8 100644 --- a/core/src/main/java/com/google/adk/tools/retrieval/VertexAiRagRetrieval.java +++ b/core/src/main/java/com/google/adk/tools/retrieval/VertexAiRagRetrieval.java @@ -20,6 +20,7 @@ import com.google.adk.models.LlmRequest; import com.google.adk.tools.ToolContext; +import com.google.adk.utils.ModelNameUtils; import com.google.cloud.aiplatform.v1.RagContexts; import com.google.cloud.aiplatform.v1.RagQuery; import com.google.cloud.aiplatform.v1.RetrieveContextsRequest; @@ -105,10 +106,9 @@ public VertexAiRagRetrieval( public Completable processLlmRequest( LlmRequest.Builder llmRequestBuilder, ToolContext toolContext) { LlmRequest llmRequest = llmRequestBuilder.build(); - // Use Gemini built-in Vertex AI RAG tool for Gemini 2 models or when using Vertex AI API Model + // Use Gemini built-in Vertex AI RAG tool for Gemini models when using Vertex AI API Model boolean useVertexAi = Boolean.parseBoolean(System.getenv("GOOGLE_GENAI_USE_VERTEXAI")); - if (useVertexAi - && (llmRequest.model().isPresent() && llmRequest.model().get().startsWith("gemini-2"))) { + if (useVertexAi && llmRequest.model().filter(ModelNameUtils::isGeminiModel).isPresent()) { GenerateContentConfig config = llmRequest.config().orElseGet(() -> GenerateContentConfig.builder().build()); ImmutableList.Builder toolsBuilder = ImmutableList.builder(); diff --git a/core/src/main/java/com/google/adk/utils/ModelNameUtils.java b/core/src/main/java/com/google/adk/utils/ModelNameUtils.java index 9995f18b2..c46f6e3a8 100644 --- a/core/src/main/java/com/google/adk/utils/ModelNameUtils.java +++ b/core/src/main/java/com/google/adk/utils/ModelNameUtils.java @@ -16,16 +16,24 @@ package com.google.adk.utils; +import com.google.common.base.Strings; +import java.util.Objects; import java.util.regex.Matcher; import java.util.regex.Pattern; public final class ModelNameUtils { + private static final String GEMINI_PREFIX = "gemini-"; private static final Pattern GEMINI_2_PATTERN = Pattern.compile("^gemini-2\\..*"); + private static final String GEMINI_CLASS = "com.google.adk.models.Gemini"; private static final Pattern PATH_PATTERN = Pattern.compile("^projects/[^/]+/locations/[^/]+/publishers/[^/]+/models/(.+)$"); private static final Pattern APIGEE_PATTERN = Pattern.compile("^apigee/(?:[^/]+/)?(?:[^/]+/)?(.+)$"); + public static boolean isGeminiModel(String modelString) { + return extractModelName(Strings.nullToEmpty(modelString)).startsWith(GEMINI_PREFIX); + } + public static boolean isGemini2Model(String modelString) { if (modelString == null) { return false; @@ -34,6 +42,29 @@ public static boolean isGemini2Model(String modelString) { return GEMINI_2_PATTERN.matcher(modelName).matches(); } + /** + * Checks whether an object is an instance of {@link com.google.adk.models.Gemini}, by searching + * through its class hierarchy for a class whose name equals the hardcoded String name of Gemini + * class. + * + *

This method can be used where the "real" instanceof check is not possible because the Gemini + * type is not known at compile time. + * + * @param o The object to check. + * @return true if object's class is {@link com.google.adk.models.Gemini}, false otherwise. + */ + public static boolean isInstanceOfGemini(Object o) { + if (o == null) { + return false; + } + for (Class clazz = o.getClass(); clazz != null; clazz = clazz.getSuperclass()) { + if (Objects.equals(clazz.getName(), GEMINI_CLASS)) { + return true; + } + } + return false; + } + /** * Extract the actual model name from either simple or path-based format. * diff --git a/core/src/test/java/com/google/adk/agents/CallbacksTest.java b/core/src/test/java/com/google/adk/agents/CallbacksTest.java index 11087e6d6..8325d346e 100644 --- a/core/src/test/java/com/google/adk/agents/CallbacksTest.java +++ b/core/src/test/java/com/google/adk/agents/CallbacksTest.java @@ -1172,10 +1172,51 @@ public Maybe> beforeToolCallback( event, ImmutableMap.of("echo_tool", new TestUtils.FailingEchoTool())) .blockingGet(); - assertThat(getFunctionResponse(functionResponseEvent)).isEqualTo(responseFromAgentCb); } + @Test + public void handleFunctionCalls_withBeforeToolCallback_modifiesArgs() { + ImmutableMap originalArgs = ImmutableMap.of("arg1", "val1"); + ImmutableMap modifiedArgs = ImmutableMap.of("arg1", "val1", "arg2", "val2"); + + Callbacks.BeforeToolCallbackSync cb1 = + (invocationContext, tool, input, toolContext) -> { + input.put("arg2", "val2"); + return Optional.empty(); + }; + + TestUtils.EchoTool echoTool = new TestUtils.EchoTool(); + + InvocationContext invocationContext = + createInvocationContext( + createTestAgentBuilder(createTestLlm(LlmResponse.builder().build())) + .beforeToolCallbackSync(cb1) + .build()); + + Event event = + createEvent("event").toBuilder() + .content( + Content.fromParts( + Part.fromText("..."), + Part.builder() + .functionCall( + FunctionCall.builder() + .id("fc_id") + .name("echo_tool") + .args(originalArgs) + .build()) + .build())) + .build(); + + Event functionResponseEvent = + Functions.handleFunctionCalls( + invocationContext, event, ImmutableMap.of("echo_tool", echoTool)) + .blockingGet(); + + assertThat(getFunctionResponse(functionResponseEvent)).containsExactly("result", modifiedArgs); + } + @Test public void agentRunAsync_withToolCallbacks_inspectsArgsAndReturnsResponse() { TestUtils.EchoTool echoTool = new TestUtils.EchoTool(); diff --git a/core/src/test/java/com/google/adk/agents/InvocationContextTest.java b/core/src/test/java/com/google/adk/agents/InvocationContextTest.java index 0237261c5..e588a38ca 100644 --- a/core/src/test/java/com/google/adk/agents/InvocationContextTest.java +++ b/core/src/test/java/com/google/adk/agents/InvocationContextTest.java @@ -31,7 +31,6 @@ import com.google.genai.types.Content; import java.util.HashMap; import java.util.Map; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import org.junit.Assert; import org.junit.Before; @@ -607,45 +606,6 @@ public void testBranch() { assertThat(context.branch()).isEmpty(); } - @Test - // Testing deprecated methods. - public void testDeprecatedCreateMethods() { - InvocationContext context1 = - InvocationContext.builder() - .sessionService(mockSessionService) - .artifactService(mockArtifactService) - .invocationId(testInvocationId) - .agent(mockAgent) - .session(session) - .userContent(Optional.ofNullable(userContent)) - .runConfig(runConfig) - .build(); - - assertThat(context1.sessionService()).isEqualTo(mockSessionService); - assertThat(context1.artifactService()).isEqualTo(mockArtifactService); - assertThat(context1.invocationId()).isEqualTo(testInvocationId); - assertThat(context1.agent()).isEqualTo(mockAgent); - assertThat(context1.session()).isEqualTo(session); - assertThat(context1.userContent()).hasValue(userContent); - assertThat(context1.runConfig()).isEqualTo(runConfig); - - InvocationContext context2 = - InvocationContext.create( - mockSessionService, - mockArtifactService, - mockAgent, - session, - liveRequestQueue, - runConfig); - - assertThat(context2.sessionService()).isEqualTo(mockSessionService); - assertThat(context2.artifactService()).isEqualTo(mockArtifactService); - assertThat(context2.agent()).isEqualTo(mockAgent); - assertThat(context2.session()).isEqualTo(session); - assertThat(context2.liveRequestQueue()).hasValue(liveRequestQueue); - assertThat(context2.runConfig()).isEqualTo(runConfig); - } - @Test public void testActiveStreamingTools() { InvocationContext context = @@ -686,9 +646,9 @@ public void testBuilderOptionalParameters() { .artifactService(mockArtifactService) .agent(mockAgent) .session(session) - .liveRequestQueue(Optional.of(liveRequestQueue)) - .branch(Optional.of("test-branch")) - .userContent(Optional.of(userContent)) + .liveRequestQueue(liveRequestQueue) + .branch("test-branch") + .userContent(userContent) .build(); assertThat(context.liveRequestQueue()).hasValue(liveRequestQueue); @@ -696,68 +656,6 @@ public void testBuilderOptionalParameters() { assertThat(context.userContent()).hasValue(userContent); } - @Test - // Testing deprecated methods. - public void testDeprecatedConstructor() { - InvocationContext context = - new InvocationContext( - mockSessionService, - mockArtifactService, - mockMemoryService, - pluginManager, - Optional.of(liveRequestQueue), - Optional.of("test-branch"), - testInvocationId, - mockAgent, - session, - Optional.of(userContent), - runConfig, - true); - - assertThat(context.sessionService()).isEqualTo(mockSessionService); - assertThat(context.artifactService()).isEqualTo(mockArtifactService); - assertThat(context.memoryService()).isEqualTo(mockMemoryService); - assertThat(context.pluginManager()).isEqualTo(pluginManager); - assertThat(context.liveRequestQueue()).hasValue(liveRequestQueue); - assertThat(context.branch()).hasValue("test-branch"); - assertThat(context.invocationId()).isEqualTo(testInvocationId); - assertThat(context.agent()).isEqualTo(mockAgent); - assertThat(context.session()).isEqualTo(session); - assertThat(context.userContent()).hasValue(userContent); - assertThat(context.runConfig()).isEqualTo(runConfig); - assertThat(context.endInvocation()).isTrue(); - } - - @Test - // Testing deprecated methods. - public void testDeprecatedConstructor_11params() { - InvocationContext context = - new InvocationContext( - mockSessionService, - mockArtifactService, - mockMemoryService, - Optional.of(liveRequestQueue), - Optional.of("test-branch"), - testInvocationId, - mockAgent, - session, - Optional.of(userContent), - runConfig, - true); - - assertThat(context.sessionService()).isEqualTo(mockSessionService); - assertThat(context.artifactService()).isEqualTo(mockArtifactService); - assertThat(context.memoryService()).isEqualTo(mockMemoryService); - assertThat(context.liveRequestQueue()).hasValue(liveRequestQueue); - assertThat(context.branch()).hasValue("test-branch"); - assertThat(context.invocationId()).isEqualTo(testInvocationId); - assertThat(context.agent()).isEqualTo(mockAgent); - assertThat(context.session()).isEqualTo(session); - assertThat(context.userContent()).hasValue(userContent); - assertThat(context.runConfig()).isEqualTo(runConfig); - assertThat(context.endInvocation()).isTrue(); - } - @Test public void build_missingInvocationId_null_throwsException() { InvocationContext.Builder builder = diff --git a/core/src/test/java/com/google/adk/agents/LlmAgentTest.java b/core/src/test/java/com/google/adk/agents/LlmAgentTest.java index ce8be8dfb..594e47fd8 100644 --- a/core/src/test/java/com/google/adk/agents/LlmAgentTest.java +++ b/core/src/test/java/com/google/adk/agents/LlmAgentTest.java @@ -376,6 +376,17 @@ public void resolveModel_withModelName_resolvesFromRegistry() { assertThat(resolvedModel.model()).hasValue(testLlm); } + @Test + public void resolveModel_withModel_usesProvidedModel() { + TestLlm testLlm = createTestLlm(LlmResponse.builder().build()); + LlmAgent testAgent = createTestAgent(testLlm); + + Model resolvedModel = testAgent.resolvedModel(); + + assertThat(resolvedModel.model()).hasValue(testLlm); + assertThat(resolvedModel.modelName()).hasValue(testLlm.model()); + } + @Test public void canonicalCallbacks_returnsEmptyListWhenNull() { TestLlm testLlm = createTestLlm(LlmResponse.builder().build()); diff --git a/core/src/test/java/com/google/adk/events/EventTest.java b/core/src/test/java/com/google/adk/events/EventTest.java index cbfb6ef0b..a4feab5c1 100644 --- a/core/src/test/java/com/google/adk/events/EventTest.java +++ b/core/src/test/java/com/google/adk/events/EventTest.java @@ -76,6 +76,7 @@ public final class EventTest { .avgLogprobs(0.5) .interrupted(true) .timestamp(123456789L) + .modelVersion("model_version") .build(); @Test @@ -99,6 +100,7 @@ public void event_builder_works() { assertThat(EVENT.interrupted()).hasValue(true); assertThat(EVENT.timestamp()).isEqualTo(123456789L); assertThat(EVENT.actions()).isEqualTo(EVENT_ACTIONS); + assertThat(EVENT.modelVersion()).hasValue("model_version"); } @Test diff --git a/core/src/test/java/com/google/adk/flows/llmflows/AgentTransferTest.java b/core/src/test/java/com/google/adk/flows/llmflows/AgentTransferTest.java index 6e6e99640..79552520b 100644 --- a/core/src/test/java/com/google/adk/flows/llmflows/AgentTransferTest.java +++ b/core/src/test/java/com/google/adk/flows/llmflows/AgentTransferTest.java @@ -24,12 +24,13 @@ import static com.google.common.truth.Truth.assertThat; import com.google.adk.agents.InvocationContext; +import com.google.adk.agents.LiveRequest; +import com.google.adk.agents.LiveRequestQueue; import com.google.adk.agents.LlmAgent; import com.google.adk.agents.LoopAgent; import com.google.adk.agents.RunConfig; import com.google.adk.agents.SequentialAgent; import com.google.adk.events.Event; -import com.google.adk.models.LlmRequest; import com.google.adk.runner.InMemoryRunner; import com.google.adk.runner.Runner; import com.google.adk.sessions.Session; @@ -44,6 +45,7 @@ import com.google.genai.types.Schema; import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.subscribers.TestSubscriber; import java.util.List; import java.util.Map; import java.util.Optional; @@ -97,6 +99,50 @@ public void exitLoopTool_exitsLoop() { // TODO: b/413488103 - complete when LoopAgent is implemented. } + @Test + public void runLive_transferToAgent_closesConnection() throws Exception { + // Arrange + Content transferCallContent = Content.fromParts(createTransferCallPart("sub_agent_1")); + Content response1 = Content.fromParts(Part.fromText("response1")); + + TestLlm testLlm = + createTestLlm( + Flowable.just(createLlmResponse(transferCallContent)), + Flowable.just(createLlmResponse(response1))); + + LlmAgent subAgent1 = createTestAgentBuilder(testLlm).name("sub_agent_1").build(); + LlmAgent rootAgent = + createTestAgentBuilder(testLlm) + .name("root_agent") + .subAgents(ImmutableList.of(subAgent1)) + .build(); + InvocationContext invocationContext = createInvocationContext(rootAgent); + + Runner runner = getRunnerAndCreateSession(rootAgent, invocationContext.session()); + LiveRequestQueue liveRequestQueue = new LiveRequestQueue(); + + // Act + TestSubscriber testSubscriber = + runner + .runLive(invocationContext.session(), liveRequestQueue, RunConfig.builder().build()) + .test(); + liveRequestQueue.content(Content.fromParts(Part.fromText("hi"))); + testSubscriber.await(); + + // Assert + testSubscriber.assertComplete(); + assertThat(simplifyEvents(testSubscriber.values())) + .containsExactly( + "root_agent: FunctionCall(name=transfer_to_agent, args={agent_name=sub_agent_1})", + "root_agent: FunctionResponse(name=transfer_to_agent, response={})", + "sub_agent_1: response1") + .inOrder(); + + long closedConnectionsCount = + testLlm.getLiveRequestHistory().stream().filter(LiveRequest::shouldClose).count(); + assertThat(closedConnectionsCount).isEqualTo(1); + } + @Test public void testAutoToAuto() { Content transferCallContent = Content.fromParts(createTransferCallPart("sub_agent_1")); @@ -412,85 +458,6 @@ public void testAutoToLoop() { assertThat(simplifyEvents(actualEvents)).containsExactly("root_agent: response5"); } - @Test - public void testLegacyTransferToAgent() { - Content transferCallContent = - Content.fromParts( - Part.fromFunctionCall("transferToAgent", ImmutableMap.of("agentName", "sub_agent_1"))); - Content response1 = Content.fromParts(Part.fromText("response1")); - Content response2 = Content.fromParts(Part.fromText("response2")); - - TestLlm testLlm = - createTestLlm( - Flowable.just(createLlmResponse(transferCallContent)), - Flowable.just(createLlmResponse(response1)), - Flowable.just(createLlmResponse(response2))); - - LlmAgent subAgent1 = createTestAgentBuilder(testLlm).name("sub_agent_1").build(); - LlmAgent rootAgent = - createTestAgentBuilder(testLlm) - .name("root_agent") - .subAgents(ImmutableList.of(subAgent1)) - .build(); - InvocationContext invocationContext = createInvocationContext(rootAgent); - - Runner runner = getRunnerAndCreateSession(rootAgent, invocationContext.session()); - List actualEvents = runRunner(runner, invocationContext); - - assertThat(simplifyEvents(actualEvents)) - .containsExactly( - "root_agent: FunctionCall(name=transferToAgent, args={agentName=sub_agent_1})", - "root_agent: FunctionResponse(name=transferToAgent, response={})", - "sub_agent_1: response1") - .inOrder(); - - actualEvents = runRunner(runner, invocationContext); - - assertThat(simplifyEvents(actualEvents)).containsExactly("sub_agent_1: response2"); - } - - @Test - public void testAgentTransferDoesNotExposeLegacyTransferToAgent() { - Content transferCallContent = - Content.fromParts( - Part.fromFunctionCall("transferToAgent", ImmutableMap.of("agentName", "sub_agent_1"))); - Content response1 = Content.fromParts(Part.fromText("response1")); - Content response2 = Content.fromParts(Part.fromText("response2")); - TestLlm testLlm = - createTestLlm( - Flowable.just(createLlmResponse(transferCallContent)), - Flowable.just(createLlmResponse(response1)), - Flowable.just(createLlmResponse(response2))); - LlmAgent subAgent1 = createTestAgentBuilder(testLlm).name("sub_agent_1").build(); - LlmAgent rootAgent = - createTestAgentBuilder(testLlm) - .name("root_agent") - .subAgents(ImmutableList.of(subAgent1)) - .build(); - InvocationContext invocationContext = createInvocationContext(rootAgent); - AgentTransfer processor = new AgentTransfer(); - LlmRequest request = LlmRequest.builder().build(); - - var processed = processor.processRequest(invocationContext, request); - - assertThat(processed.blockingGet().updatedRequest().config().get().tools()).isPresent(); - assertThat(processed.blockingGet().updatedRequest().config().get().tools().get()).hasSize(1); - assertThat( - processed - .blockingGet() - .updatedRequest() - .config() - .get() - .tools() - .get() - .get(0) - .functionDeclarations() - .get() - .get(0) - .name()) - .hasValue("transfer_to_agent"); - } - private Runner getRunnerAndCreateSession(LlmAgent agent, Session session) { Runner runner = new InMemoryRunner(agent, session.appName()); // Ensure the session exists before running the agent. diff --git a/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java b/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java index b5df658ba..85e78666d 100644 --- a/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java +++ b/core/src/test/java/com/google/adk/flows/llmflows/ContentsTest.java @@ -142,7 +142,9 @@ public void rearrangeLatest_multipleFRsForSameFCAsync_returnsMergedFR() { @Test public void rearrangeLatest_missingFCEvent_throwsException() { Event frEvent = createFunctionResponseEvent("fr1", "tool1", "call1"); - ImmutableList events = ImmutableList.of(createUserEvent("u1", "Query"), frEvent); + Event frEvent2 = createFunctionResponseEvent("fr2", "tool1", "call1"); + ImmutableList events = + ImmutableList.of(createUserEvent("u1", "Query"), frEvent, frEvent2); assertThrows(IllegalStateException.class, () -> runContentsProcessor(events)); } @@ -473,10 +475,12 @@ public void processRequest_includeContentsNone_asyncFRAcrossTurns_throwsExceptio Event fc1 = createFunctionCallEvent("fc1", "tool1", "call1"); Event u2 = createUserEvent("u2", "Query 2"); Event fr1 = createFunctionResponseEvent("fr1", "tool1", "call1"); // FR for fc1 + Event fr2 = createFunctionResponseEvent("fr2", "tool2", "call1"); // FR for fc2 - ImmutableList events = ImmutableList.of(u1, fc1, u2, fr1); + ImmutableList events = ImmutableList.of(u1, fc1, u2, fr1, fr2); - // The current turn starts from u2. fc1 is not in the sublist [u2, fr1], so rearrangement fails. + // The current turn starts from u2. fc1 is not in the sublist [u2, fr1, fr2], so rearrangement + // fails. IllegalStateException e = assertThrows( IllegalStateException.class, @@ -486,6 +490,19 @@ public void processRequest_includeContentsNone_asyncFRAcrossTurns_throwsExceptio .contains("No function call event found for function response IDs: [call1]"); } + @Test + public void processRequest_notEnoughEvents_returnsOriginalList() { + Event fr1 = + createFunctionCallAndResponseEvent( + "fr1", "tool1", "call1", ImmutableMap.of("result", "ok"), "user"); + + ImmutableList events = ImmutableList.of(fr1); + + List result = + runContentsProcessorWithIncludeContents(events, LlmAgent.IncludeContents.NONE, "A2A-agent"); + assertThat(result).isEmpty(); + } + @Test public void processRequest_includeContentsNone_asyncFRWithinTurn() { Event u1 = createUserEvent("u1", "Query 1"); @@ -737,11 +754,37 @@ public void processRequest_slidingWindow_preservesOverlappingCompactions() { .containsExactly("C1", "C2", "E4", "E5"); } + @Test + public void processRequest_notEmptyContent() { + Event e = + Event.builder() + .id("e1") + .author(AGENT) + .content( + Content.builder() + .role("model") + .parts( + ImmutableList.of( + Part.builder().text("").thought(true).build(), + Part.builder() + .functionCall( + FunctionCall.builder() + .name("test-tool") + .id("test-call-id") + .build()) + .thought(false) + .build())) + .build()) + .build(); + List contents = runContentsProcessor(ImmutableList.of(e)); + assertThat(contents).containsExactly(e.content().get()); + } + private static Event createUserEvent(String id, String text) { return Event.builder() .id(id) .author(USER) - .content(Optional.of(Content.fromParts(Part.fromText(text)))) + .content(Content.fromParts(Part.fromText(text))) .invocationId("invocationId") .build(); } @@ -751,7 +794,7 @@ private static Event createUserEvent( return Event.builder() .id(id) .author(USER) - .content(Optional.of(Content.fromParts(Part.fromText(text)))) + .content(Content.fromParts(Part.fromText(text))) .invocationId(invocationId) .timestamp(timestamp) .build(); @@ -883,13 +926,40 @@ private static Event createFunctionResponseEvent( .build(); } + private static Event createFunctionCallAndResponseEvent( + String id, String toolName, String callId, Map response, String author) { + return Event.builder() + .id(id) + .author(author) + .invocationId("invocationId") + .content( + Content.fromParts( + Part.builder() + .functionCall(FunctionCall.builder().name(toolName).id(callId).build()) + .build(), + Part.builder() + .functionResponse( + FunctionResponse.builder() + .name(toolName) + .id(callId) + .response(response) + .build()) + .build())) + .build(); + } + private List runContentsProcessor(List events) { return runContentsProcessorWithIncludeContents(events, LlmAgent.IncludeContents.DEFAULT); } private List runContentsProcessorWithIncludeContents( List events, LlmAgent.IncludeContents includeContents) { - LlmAgent agent = LlmAgent.builder().name(AGENT).includeContents(includeContents).build(); + return runContentsProcessorWithIncludeContents(events, includeContents, AGENT); + } + + private List runContentsProcessorWithIncludeContents( + List events, LlmAgent.IncludeContents includeContents, String agentName) { + LlmAgent agent = LlmAgent.builder().name(agentName).includeContents(includeContents).build(); Session session = sessionService.createSession("test-app", "test-user", null, "test-session").blockingGet(); session.events().addAll(events); diff --git a/core/src/test/java/com/google/adk/flows/llmflows/FunctionsTest.java b/core/src/test/java/com/google/adk/flows/llmflows/FunctionsTest.java index 97092f68c..d5db4d4b3 100644 --- a/core/src/test/java/com/google/adk/flows/llmflows/FunctionsTest.java +++ b/core/src/test/java/com/google/adk/flows/llmflows/FunctionsTest.java @@ -33,7 +33,6 @@ import com.google.genai.types.FunctionCall; import com.google.genai.types.FunctionResponse; import com.google.genai.types.Part; -import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -43,12 +42,7 @@ public final class FunctionsTest { private static final Event EVENT_WITH_NO_CONTENT = - Event.builder() - .id("event1") - .invocationId("invocation1") - .author("agent") - .content(Optional.empty()) - .build(); + Event.builder().id("event1").invocationId("invocation1").author("agent").build(); private static final Event EVENT_WITH_NO_PARTS = Event.builder() diff --git a/core/src/test/java/com/google/adk/models/LlmResponseTest.java b/core/src/test/java/com/google/adk/models/LlmResponseTest.java index 65e00413f..646b89602 100644 --- a/core/src/test/java/com/google/adk/models/LlmResponseTest.java +++ b/core/src/test/java/com/google/adk/models/LlmResponseTest.java @@ -28,7 +28,6 @@ import com.google.genai.types.FunctionCall; import com.google.genai.types.GenerateContentResponseUsageMetadata; import com.google.genai.types.Part; -import java.util.Optional; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -74,8 +73,8 @@ public void testSerializationAndDeserialization_allFieldsPresent() .partial(true) .turnComplete(false) .errorCode(new FinishReason("ERR_123")) - .errorMessage(Optional.of("An error occurred.")) - .interrupted(Optional.of(true)) + .errorMessage("An error occurred.") + .interrupted(true) .usageMetadata(usageMetadata) .build(); @@ -113,16 +112,7 @@ public void testSerializationAndDeserialization_optionalFieldsEmpty() throws JsonProcessingException { Content sampleContent = createSampleFunctionCallContent("tool_abc"); LlmResponse originalResponse = - LlmResponse.builder() - .content(sampleContent) - .groundingMetadata(Optional.empty()) - .partial(Optional.empty()) - .turnComplete(false) - .errorCode(Optional.empty()) - .errorMessage(Optional.empty()) - .interrupted(Optional.empty()) - .usageMetadata(Optional.empty()) - .build(); + LlmResponse.builder().content(sampleContent).turnComplete(false).build(); String json = originalResponse.toJson(); assertThat(json).isNotNull(); diff --git a/core/src/test/java/com/google/adk/plugins/LoggingPluginTest.java b/core/src/test/java/com/google/adk/plugins/LoggingPluginTest.java index 764525ff0..a08599c9a 100644 --- a/core/src/test/java/com/google/adk/plugins/LoggingPluginTest.java +++ b/core/src/test/java/com/google/adk/plugins/LoggingPluginTest.java @@ -65,9 +65,7 @@ public class LoggingPluginTest { Event.builder() .id("event_id") .author("author") - .content(Optional.empty()) .actions(EventActions.builder().build()) - .longRunningToolIds(Optional.empty()) .build(); private final LlmRequest.Builder llmRequestBuilder = LlmRequest.builder().model("default").contents(ImmutableList.of()); diff --git a/core/src/test/java/com/google/adk/runner/InputAudioTranscriptionTest.java b/core/src/test/java/com/google/adk/runner/InputAudioTranscriptionTest.java index 55d41916f..95a016e34 100644 --- a/core/src/test/java/com/google/adk/runner/InputAudioTranscriptionTest.java +++ b/core/src/test/java/com/google/adk/runner/InputAudioTranscriptionTest.java @@ -33,7 +33,6 @@ import com.google.genai.types.Modality; import com.google.genai.types.Part; import java.lang.reflect.Method; -import java.util.Optional; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -50,10 +49,9 @@ private InvocationContext invokeNewInvocationContextForLive( throws Exception { Method method = Runner.class.getDeclaredMethod( - "newInvocationContextForLive", Session.class, Optional.class, RunConfig.class); + "newInvocationContextForLive", Session.class, LiveRequestQueue.class, RunConfig.class); method.setAccessible(true); - return (InvocationContext) - method.invoke(runner, session, Optional.of(liveRequestQueue), runConfig); + return (InvocationContext) method.invoke(runner, session, liveRequestQueue, runConfig); } @Test diff --git a/core/src/test/java/com/google/adk/runner/RunnerTest.java b/core/src/test/java/com/google/adk/runner/RunnerTest.java index 421b79abb..8a0a84b08 100644 --- a/core/src/test/java/com/google/adk/runner/RunnerTest.java +++ b/core/src/test/java/com/google/adk/runner/RunnerTest.java @@ -41,6 +41,7 @@ import com.google.adk.models.LlmResponse; import com.google.adk.plugins.BasePlugin; import com.google.adk.sessions.Session; +import com.google.adk.sessions.SessionKey; import com.google.adk.summarizer.EventsCompactionConfig; import com.google.adk.telemetry.Tracing; import com.google.adk.testing.TestLlm; @@ -578,6 +579,14 @@ public void onEventCallback_success() { verify(plugin).onEventCallback(any(), any()); } + @Test + public void runAsync_withSessionKey_success() { + var events = + runner.runAsync(session.sessionKey(), createContent("from user")).toList().blockingGet(); + + assertThat(simplifyEvents(events)).containsExactly("test agent: from llm"); + } + @Test public void runAsync_withStateDelta_mergesStateIntoSession() { ImmutableMap stateDelta = ImmutableMap.of("key1", "value1", "key2", 42); @@ -605,6 +614,32 @@ public void runAsync_withStateDelta_mergesStateIntoSession() { assertThat(finalSession.state()).containsAtLeastEntriesIn(stateDelta); } + @Test + public void runAsync_withSessionKeyAndStateDelta_mergesStateIntoSession() { + ImmutableMap stateDelta = ImmutableMap.of("key1", "value1", "key2", 42); + + var events = + runner + .runAsync( + session.sessionKey(), + createContent("test message"), + RunConfig.builder().build(), + stateDelta) + .toList() + .blockingGet(); + + // Verify agent runs successfully + assertThat(simplifyEvents(events)).containsExactly("test agent: from llm"); + + // Verify state was merged into session + Session finalSession = + runner + .sessionService() + .getSession("test", "user", session.id(), Optional.empty()) + .blockingGet(); + assertThat(finalSession.state()).containsAtLeastEntriesIn(stateDelta); + } + @Test public void runAsync_withEmptyStateDelta_doesNotModifySession() { ImmutableMap emptyStateDelta = ImmutableMap.of(); @@ -840,6 +875,20 @@ public void runLive_success() throws Exception { assertThat(simplifyEvents(testSubscriber.values())).containsExactly("test agent: from llm"); } + @Test + public void runLive_withSessionKey_success() throws Exception { + LiveRequestQueue liveRequestQueue = new LiveRequestQueue(); + TestSubscriber testSubscriber = + runner.runLive(session.sessionKey(), liveRequestQueue, RunConfig.builder().build()).test(); + + liveRequestQueue.content(createContent("from user")); + liveRequestQueue.close(); + + testSubscriber.await(); + testSubscriber.assertComplete(); + assertThat(simplifyEvents(testSubscriber.values())).containsExactly("test agent: from llm"); + } + @Test public void runLive_withToolExecution() throws Exception { LlmAgent agentWithTool = @@ -948,6 +997,18 @@ public void runAsync_withoutSessionAndAutoCreateSessionTrue_createsSession() { .isNotNull(); } + @Test + public void runAsync_withoutSessionAndAutoCreateSessionTrue_withSessionKey_createsSession() { + RunConfig runConfig = RunConfig.builder().setAutoCreateSession(true).build(); + SessionKey sessionKey = new SessionKey("test", "user", UUID.randomUUID().toString()); + + var events = + runner.runAsync(sessionKey, createContent("from user"), runConfig).toList().blockingGet(); + + assertThat(simplifyEvents(events)).containsExactly("test agent: from llm"); + assertThat(runner.sessionService().getSession(sessionKey, null).blockingGet()).isNotNull(); + } + @Test public void runAsync_withoutSessionAndAutoCreateSessionFalse_throwsException() { RunConfig runConfig = RunConfig.builder().setAutoCreateSession(false).build(); @@ -959,6 +1020,17 @@ public void runAsync_withoutSessionAndAutoCreateSessionFalse_throwsException() { .assertError(IllegalArgumentException.class); } + @Test + public void runAsync_withoutSessionAndAutoCreateSessionFalse_withSessionKey_throwsException() { + RunConfig runConfig = RunConfig.builder().setAutoCreateSession(false).build(); + SessionKey sessionKey = new SessionKey("test", "user", UUID.randomUUID().toString()); + + runner + .runAsync(sessionKey, createContent("from user"), runConfig) + .test() + .assertError(IllegalArgumentException.class); + } + @Test public void runLive_withoutSessionAndAutoCreateSessionTrue_createsSession() throws Exception { RunConfig runConfig = RunConfig.builder().setAutoCreateSession(true).build(); @@ -982,6 +1054,25 @@ public void runLive_withoutSessionAndAutoCreateSessionTrue_createsSession() thro .isNotNull(); } + @Test + public void runLive_withoutSessionAndAutoCreateSessionTrue_withSessionKey_createsSession() + throws Exception { + RunConfig runConfig = RunConfig.builder().setAutoCreateSession(true).build(); + SessionKey sessionKey = new SessionKey("test", "user", UUID.randomUUID().toString()); + LiveRequestQueue liveRequestQueue = new LiveRequestQueue(); + + TestSubscriber testSubscriber = + runner.runLive(sessionKey, liveRequestQueue, runConfig).test(); + + liveRequestQueue.content(createContent("from user")); + liveRequestQueue.close(); + + testSubscriber.await(); + testSubscriber.assertComplete(); + assertThat(simplifyEvents(testSubscriber.values())).containsExactly("test agent: from llm"); + assertThat(runner.sessionService().getSession(sessionKey, null).blockingGet()).isNotNull(); + } + @Test public void runLive_withoutSessionAndAutoCreateSessionFalse_throwsException() { RunConfig runConfig = RunConfig.builder().setAutoCreateSession(false).build(); @@ -994,6 +1085,18 @@ public void runLive_withoutSessionAndAutoCreateSessionFalse_throwsException() { .assertError(IllegalArgumentException.class); } + @Test + public void runLive_withoutSessionAndAutoCreateSessionFalse_withSessionKey_throwsException() { + RunConfig runConfig = RunConfig.builder().setAutoCreateSession(false).build(); + SessionKey sessionKey = new SessionKey("test", "user", UUID.randomUUID().toString()); + LiveRequestQueue liveRequestQueue = new LiveRequestQueue(); + + runner + .runLive(sessionKey, liveRequestQueue, runConfig) + .test() + .assertError(IllegalArgumentException.class); + } + @Test public void runAsync_withToolConfirmation() { TestLlm testLlm = @@ -1085,6 +1188,14 @@ public void close_closesPluginsAndCodeExecutors() { verify(plugin).close(); } + @Test + public void buildRunnerWithPlugins_success() { + BasePlugin plugin1 = mockPlugin("test1"); + BasePlugin plugin2 = mockPlugin("test2"); + Runner runner = Runner.builder().agent(agent).appName("test").plugins(plugin1, plugin2).build(); + assertThat(runner.pluginManager().getPlugins()).containsExactly(plugin1, plugin2); + } + public static class Tools { private Tools() {} diff --git a/core/src/test/java/com/google/adk/sessions/InMemorySessionServiceTest.java b/core/src/test/java/com/google/adk/sessions/InMemorySessionServiceTest.java index 6223dd2f0..41e156ffd 100644 --- a/core/src/test/java/com/google/adk/sessions/InMemorySessionServiceTest.java +++ b/core/src/test/java/com/google/adk/sessions/InMemorySessionServiceTest.java @@ -20,6 +20,7 @@ import com.google.adk.events.Event; import com.google.adk.events.EventActions; import io.reactivex.rxjava3.core.Single; +import java.util.HashMap; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -84,7 +85,7 @@ public void lifecycle_listSessions() { Session session = sessionService - .createSession("app-name", "user-id", new ConcurrentHashMap<>(), "session-1") + .createSession("app-name", "user-id", new HashMap<>(), "session-1") .blockingGet(); ConcurrentMap stateDelta = new ConcurrentHashMap<>(); @@ -130,9 +131,7 @@ public void lifecycle_deleteSession() { public void appendEvent_updatesSessionState() { InMemorySessionService sessionService = new InMemorySessionService(); Session session = - sessionService - .createSession("app", "user", new ConcurrentHashMap<>(), "session1") - .blockingGet(); + sessionService.createSession("app", "user", new HashMap<>(), "session1").blockingGet(); ConcurrentMap stateDelta = new ConcurrentHashMap<>(); stateDelta.put("sessionKey", "sessionValue"); @@ -167,9 +166,7 @@ public void appendEvent_updatesSessionState() { public void appendEvent_removesState() { InMemorySessionService sessionService = new InMemorySessionService(); Session session = - sessionService - .createSession("app", "user", new ConcurrentHashMap<>(), "session1") - .blockingGet(); + sessionService.createSession("app", "user", new HashMap<>(), "session1").blockingGet(); ConcurrentMap stateDeltaAdd = new ConcurrentHashMap<>(); stateDeltaAdd.put("sessionKey", "sessionValue"); @@ -221,9 +218,7 @@ public void appendEvent_removesState() { public void sequentialAgents_shareTempState() { InMemorySessionService sessionService = new InMemorySessionService(); Session session = - sessionService - .createSession("app", "user", new ConcurrentHashMap<>(), "session1") - .blockingGet(); + sessionService.createSession("app", "user", new HashMap<>(), "session1").blockingGet(); // Agent 1 writes to temp state ConcurrentMap stateDelta1 = new ConcurrentHashMap<>(); diff --git a/core/src/test/java/com/google/adk/sessions/SessionJsonConverterTest.java b/core/src/test/java/com/google/adk/sessions/SessionJsonConverterTest.java index f6120cf08..335f7f1d0 100644 --- a/core/src/test/java/com/google/adk/sessions/SessionJsonConverterTest.java +++ b/core/src/test/java/com/google/adk/sessions/SessionJsonConverterTest.java @@ -22,7 +22,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.junit.Test; @@ -49,12 +48,12 @@ public void convertEventToJson_fullEvent_success() throws JsonProcessingExceptio .author("user") .invocationId("inv-123") .timestamp(Instant.parse("2023-01-01T00:00:00Z").toEpochMilli()) - .errorCode(Optional.of(new FinishReason("OTHER"))) - .errorMessage(Optional.of("Something was not found")) + .errorCode(new FinishReason("OTHER")) + .errorMessage("Something was not found") .partial(true) .turnComplete(true) .interrupted(false) - .branch(Optional.of("branch-1")) + .branch("branch-1") .content(Content.fromParts(Part.fromText("Hello"))) .actions(actions) .build(); diff --git a/core/src/test/java/com/google/adk/sessions/VertexAiSessionServiceTest.java b/core/src/test/java/com/google/adk/sessions/VertexAiSessionServiceTest.java index 36eab1d16..def4faf4c 100644 --- a/core/src/test/java/com/google/adk/sessions/VertexAiSessionServiceTest.java +++ b/core/src/test/java/com/google/adk/sessions/VertexAiSessionServiceTest.java @@ -167,8 +167,7 @@ public void setUp() throws Exception { @Test public void createSession_success() throws Exception { - ConcurrentMap sessionStateMap = - new ConcurrentHashMap<>(ImmutableMap.of("new_key", "new_value")); + Map sessionStateMap = new HashMap<>(ImmutableMap.of("new_key", "new_value")); Single sessionSingle = vertexAiSessionService.createSession("123", "test_user", sessionStateMap, null); Session createdSession = sessionSingle.blockingGet(); @@ -190,8 +189,7 @@ public void createSession_success() throws Exception { @Test public void createSession_getSession_success() throws Exception { - ConcurrentMap sessionStateMap = - new ConcurrentHashMap<>(ImmutableMap.of("new_key", "new_value")); + Map sessionStateMap = new HashMap<>(ImmutableMap.of("new_key", "new_value")); Single sessionSingle = vertexAiSessionService.createSession("789", "test_user", sessionStateMap, null); Session createdSession = sessionSingle.blockingGet(); @@ -252,8 +250,7 @@ public void getAndDeleteSession_success() throws Exception { @Test public void createSessionAndGetSession_success() throws Exception { - ConcurrentMap sessionStateMap = - new ConcurrentHashMap<>(ImmutableMap.of("key", "value")); + Map sessionStateMap = new HashMap<>(ImmutableMap.of("key", "value")); Single sessionSingle = vertexAiSessionService.createSession("123", "user", sessionStateMap, null); Session createdSession = sessionSingle.blockingGet(); @@ -341,8 +338,8 @@ public void listEmptySession_success() { @Test public void appendEvent_withStateRemoved_updatesSessionState() { String userId = "userB"; - ConcurrentMap initialState = - new ConcurrentHashMap<>(ImmutableMap.of("key1", "value1", "key2", "value2")); + Map initialState = + new HashMap<>(ImmutableMap.of("key1", "value1", "key2", "value2")); Session session = vertexAiSessionService.createSession("987", userId, initialState, null).blockingGet(); diff --git a/core/src/test/java/com/google/adk/summarizer/EventsCompactionConfigTest.java b/core/src/test/java/com/google/adk/summarizer/EventsCompactionConfigTest.java new file mode 100644 index 000000000..01f59d37a --- /dev/null +++ b/core/src/test/java/com/google/adk/summarizer/EventsCompactionConfigTest.java @@ -0,0 +1,55 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.adk.summarizer; + +import static com.google.common.truth.Truth.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class EventsCompactionConfigTest { + + @Test + public void builder_buildsConfig() { + EventsCompactionConfig config = + EventsCompactionConfig.builder() + .compactionInterval(10) + .overlapSize(2) + .tokenThreshold(100) + .eventRetentionSize(5) + .build(); + + assertThat(config.compactionInterval()).isEqualTo(10); + assertThat(config.overlapSize()).isEqualTo(2); + assertThat(config.tokenThreshold()).isEqualTo(100); + assertThat(config.eventRetentionSize()).isEqualTo(5); + assertThat(config.summarizer()).isNull(); + } + + @Test + public void toBuilder_rebuildsConfig() { + EventsCompactionConfig config = + EventsCompactionConfig.builder().compactionInterval(10).overlapSize(2).build(); + + EventsCompactionConfig rebuilt = config.toBuilder().compactionInterval(20).build(); + + assertThat(rebuilt.compactionInterval()).isEqualTo(20); + assertThat(rebuilt.overlapSize()).isEqualTo(2); + } +} diff --git a/core/src/test/java/com/google/adk/tools/BaseToolTest.java b/core/src/test/java/com/google/adk/tools/BaseToolTest.java index 16418657d..2a07e7a44 100644 --- a/core/src/test/java/com/google/adk/tools/BaseToolTest.java +++ b/core/src/test/java/com/google/adk/tools/BaseToolTest.java @@ -2,13 +2,16 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.adk.agents.InvocationContext; +import com.google.adk.agents.LlmAgent; +import com.google.adk.models.Gemini; import com.google.adk.models.LlmRequest; +import com.google.adk.sessions.InMemorySessionService; import com.google.common.collect.ImmutableList; import com.google.genai.types.FunctionDeclaration; import com.google.genai.types.GenerateContentConfig; import com.google.genai.types.GoogleMaps; import com.google.genai.types.GoogleSearch; -import com.google.genai.types.GoogleSearchRetrieval; import com.google.genai.types.Tool; import com.google.genai.types.ToolCodeExecution; import com.google.genai.types.UrlContext; @@ -143,25 +146,6 @@ public void processLlmRequestWithGoogleSearchToolAddsToolToConfig() { Tool.builder().googleSearch(GoogleSearch.builder().build()).build()); } - @Test - public void processLlmRequestWithGoogleSearchRetrievalToolAddsToolToConfig() { - GoogleSearchTool googleSearchTool = new GoogleSearchTool(); - LlmRequest llmRequest = - LlmRequest.builder() - .config(GenerateContentConfig.builder().build()) - .model("gemini-1") - .build(); - LlmRequest.Builder llmRequestBuilder = llmRequest.toBuilder(); - Completable unused = - googleSearchTool.processLlmRequest(llmRequestBuilder, /* toolContext= */ null); - LlmRequest updatedLlmRequest = llmRequestBuilder.build(); - assertThat(updatedLlmRequest.config()).isPresent(); - assertThat(updatedLlmRequest.config().get().tools()).isPresent(); - assertThat(updatedLlmRequest.config().get().tools().get()) - .containsExactly( - Tool.builder().googleSearchRetrieval(GoogleSearchRetrieval.builder().build()).build()); - } - @Test public void processLlmRequestWithUrlContextToolAddsToolToConfig() { FunctionDeclaration functionDeclaration = @@ -191,13 +175,27 @@ public void processLlmRequestWithUrlContextToolAddsToolToConfig() { Tool.builder().urlContext(UrlContext.builder().build()).build()); } + private static InvocationContext.Builder testInvocationContext() { + InvocationContext.Builder builder = InvocationContext.builder(); + builder.agent(testAgent().build()); + InMemorySessionService inMemorySessionService = new InMemorySessionService(); + builder.sessionService(inMemorySessionService); + builder.session(inMemorySessionService.createSession("test-app", "test-user-id").blockingGet()); + return builder; + } + + private static LlmAgent.Builder testAgent() { + return LlmAgent.builder().name("test-agent"); + } + @Test - public void processLlmRequestWithBuiltInCodeExecutionToolAddsToolToConfig() { + public void + processLlmRequestWithBuiltInCodeExecutionToolAndNonGeminiModelAndNullContextAddsToolToConfig() { BuiltInCodeExecutionTool builtInCodeExecutionTool = new BuiltInCodeExecutionTool(); LlmRequest llmRequest = LlmRequest.builder() .config(GenerateContentConfig.builder().build()) - .model("gemini-2") + .model("text-bison") .build(); LlmRequest.Builder llmRequestBuilder = llmRequest.toBuilder(); Completable unused = @@ -209,6 +207,29 @@ public void processLlmRequestWithBuiltInCodeExecutionToolAddsToolToConfig() { .containsExactly(Tool.builder().codeExecution(ToolCodeExecution.builder().build()).build()); } + @Test + public void processLlmRequestWithBuiltInCodeExecutionToolAndGemini2ModelAddsToolToConfig() { + BuiltInCodeExecutionTool builtInCodeExecutionTool = new BuiltInCodeExecutionTool(); + LlmRequest llmRequest = + LlmRequest.builder() + .config(GenerateContentConfig.builder().build()) + .model("gemini-2") + .build(); + LlmRequest.Builder llmRequestBuilder = llmRequest.toBuilder(); + ToolContext toolContext = + ToolContext.builder( + testInvocationContext() + .agent(testAgent().model(new Gemini("gemini-2", "")).build()) + .build()) + .build(); + Completable unused = builtInCodeExecutionTool.processLlmRequest(llmRequestBuilder, toolContext); + LlmRequest updatedLlmRequest = llmRequestBuilder.build(); + assertThat(updatedLlmRequest.config()).isPresent(); + assertThat(updatedLlmRequest.config().get().tools()).isPresent(); + assertThat(updatedLlmRequest.config().get().tools().get()) + .containsExactly(Tool.builder().codeExecution(ToolCodeExecution.builder().build()).build()); + } + @Test public void processLlmRequestWithGoogleMapsToolAddsToolToConfig() { GoogleMapsTool googleMapsTool = new GoogleMapsTool(); diff --git a/core/src/test/java/com/google/adk/tools/retrieval/VertexAiRagRetrievalTest.java b/core/src/test/java/com/google/adk/tools/retrieval/VertexAiRagRetrievalTest.java index 6f04a7ef8..8246751b9 100644 --- a/core/src/test/java/com/google/adk/tools/retrieval/VertexAiRagRetrievalTest.java +++ b/core/src/test/java/com/google/adk/tools/retrieval/VertexAiRagRetrievalTest.java @@ -208,7 +208,7 @@ public void processLlmRequest_otherModel_doNotAddVertexRagStoreToConfig() { "projects/test-project/locations/us-central1", ragResources, vectorDistanceThreshold); - LlmRequest.Builder llmRequestBuilder = LlmRequest.builder().model("gemini-1-pro"); + LlmRequest.Builder llmRequestBuilder = LlmRequest.builder().model("other-model"); ToolContext toolContext = buildToolContext(); GenerateContentConfig initialConfig = GenerateContentConfig.builder().build(); llmRequestBuilder.config(initialConfig); diff --git a/core/src/test/java/com/google/adk/utils/ModelNameUtilsTest.java b/core/src/test/java/com/google/adk/utils/ModelNameUtilsTest.java index 37853c477..20dda7034 100644 --- a/core/src/test/java/com/google/adk/utils/ModelNameUtilsTest.java +++ b/core/src/test/java/com/google/adk/utils/ModelNameUtilsTest.java @@ -2,6 +2,7 @@ import static com.google.common.truth.Truth.assertThat; +import com.google.adk.models.Gemini; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -69,4 +70,103 @@ public void isGemini2Model_withApigeeProviderV1BetaGemini2Model_returnsTrue() { public void isGemini2Model_withNullModel_returnsFalse() { assertThat(ModelNameUtils.isGemini2Model(null)).isFalse(); } + + @Test + public void isGeminiModel_withGeminiModel_returnsTrue() { + assertThat(ModelNameUtils.isGeminiModel("gemini-1.5-flash")).isTrue(); + } + + @Test + public void isGeminiModel_withNonGeminiModel_returnsFalse() { + assertThat(ModelNameUtils.isGeminiModel("text-bison")).isFalse(); + } + + @Test + public void isGeminiModel_withPathBasedGeminiModel_returnsTrue() { + assertThat( + ModelNameUtils.isGeminiModel( + "projects/test-project/locations/us-central1/publishers/google/models/gemini-1.5-pro")) + .isTrue(); + } + + @Test + public void isGeminiModel_withPathBasedNonGeminiModel_returnsFalse() { + assertThat( + ModelNameUtils.isGeminiModel( + "projects/test-project/locations/us-central1/publishers/google/models/text-bison")) + .isFalse(); + } + + @Test + public void isGeminiModel_withApigeeGeminiModel_returnsTrue() { + assertThat(ModelNameUtils.isGeminiModel("apigee/gemini-1.5-flash")).isTrue(); + } + + @Test + public void isGeminiModel_withApigeeV1GeminiModel_returnsTrue() { + assertThat(ModelNameUtils.isGeminiModel("apigee/v1/gemini-1.5-flash")).isTrue(); + } + + @Test + public void isGeminiModel_withApigeeProviderGeminiModel_returnsTrue() { + assertThat(ModelNameUtils.isGeminiModel("apigee/gemini/gemini-1.5-flash")).isTrue(); + } + + @Test + public void isGeminiModel_withApigeeProviderVertexGeminiModel_returnsTrue() { + assertThat(ModelNameUtils.isGeminiModel("apigee/vertex_ai/gemini-1.5-flash")).isTrue(); + } + + @Test + public void isGeminiModel_withApigeeProviderV1GeminiModel_returnsTrue() { + assertThat(ModelNameUtils.isGeminiModel("apigee/gemini/v1/gemini-1.5-flash")).isTrue(); + } + + @Test + public void isGeminiModel_withApigeeProviderV1BetaGeminiModel_returnsTrue() { + assertThat(ModelNameUtils.isGeminiModel("apigee/vertex_ai/v1beta/gemini-1.5-flash")).isTrue(); + } + + @Test + public void isGeminiModel_withNullModel_returnsFalse() { + assertThat(ModelNameUtils.isGeminiModel(null)).isFalse(); + } + + @Test + public void isGeminiModel_withEmptyModel_returnsFalse() { + assertThat(ModelNameUtils.isGeminiModel("")).isFalse(); + } + + @Test + public void isInstanceOfGemini_withGeminiInstance_returnsTrue() { + assertThat(ModelNameUtils.isInstanceOfGemini(new Gemini("", ""))).isTrue(); + } + + @Test + public void isInstanceOfGemini_withNonGeminiInstance_returnsFalse() { + assertThat(ModelNameUtils.isInstanceOfGemini(new Object())).isFalse(); + } + + @Test + public void isInstanceOfGemini_withNullInstance_returnsFalse() { + assertThat(ModelNameUtils.isInstanceOfGemini(null)).isFalse(); + } + + private static class GeminiSubclass extends Gemini { + GeminiSubclass() { + super("test-model", "test-api-key"); + } + } + + private static class GeminiSubclassSubclass extends GeminiSubclass {} + + @Test + public void isInstanceOfGemini_withGeminiSubclassInstance_returnsTrue() { + assertThat(ModelNameUtils.isInstanceOfGemini(new GeminiSubclass())).isTrue(); + } + + @Test + public void isInstanceOfGemini_withSubclassOfGeminiSubclassInstance_returnsTrue() { + assertThat(ModelNameUtils.isInstanceOfGemini(new GeminiSubclassSubclass())).isTrue(); + } } diff --git a/dev/pom.xml b/dev/pom.xml index 1d2e2f0c7..57aa808c2 100644 --- a/dev/pom.xml +++ b/dev/pom.xml @@ -18,7 +18,7 @@ com.google.adk google-adk-parent - 1.2.0 + 0.8.1-SNAPSHOT google-adk-dev @@ -63,6 +63,11 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-webmvc-test + test + com.google.truth truth @@ -104,6 +109,10 @@ io.opentelemetry opentelemetry-sdk-trace + + io.opentelemetry + opentelemetry-sdk-metrics + com.flipkart.zjsonpatch zjsonpatch diff --git a/dev/src/main/java/com/google/adk/web/AdkWebServer.java b/dev/src/main/java/com/google/adk/web/AdkWebServer.java index 88723e4c7..0c29f8250 100644 --- a/dev/src/main/java/com/google/adk/web/AdkWebServer.java +++ b/dev/src/main/java/com/google/adk/web/AdkWebServer.java @@ -40,6 +40,8 @@ import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -119,10 +121,25 @@ public BaseMemoryService memoryService() { * @return Configured ObjectMapper instance */ @Bean + @Primary public ObjectMapper objectMapper() { return JsonBaseModel.getMapper(); } + /** + * Configures the message converter to use the custom ADK ObjectMapper. This ensures that Spring + * Web uses the correct JSON serialization settings (like omitting absent optional fields) and + * prevents double-serialization issues, particularly for Server-Sent Events (SSE). + * + * @param objectMapper The primary ObjectMapper configured for the ADK. + * @return A configured MappingJackson2HttpMessageConverter. + */ + @Bean + public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter( + ObjectMapper objectMapper) { + return new MappingJackson2HttpMessageConverter(objectMapper); + } + /** * Configures resource handlers for serving static content (like the Dev UI). Maps requests * starting with "/dev-ui/" to the directory specified by the 'adk.web.ui.dir' system property. diff --git a/dev/src/main/java/com/google/adk/web/controller/ExecutionController.java b/dev/src/main/java/com/google/adk/web/controller/ExecutionController.java index 7dfd85426..e88a83cef 100644 --- a/dev/src/main/java/com/google/adk/web/controller/ExecutionController.java +++ b/dev/src/main/java/com/google/adk/web/controller/ExecutionController.java @@ -22,11 +22,14 @@ import com.google.adk.runner.Runner; import com.google.adk.web.dto.AgentRunRequest; import com.google.adk.web.service.RunnerService; -import com.google.adk.web.service.SseEventStreamService; -import com.google.adk.web.service.eventprocessor.PassThroughEventProcessor; import com.google.common.collect.Lists; import io.reactivex.rxjava3.core.Flowable; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; +import java.io.IOException; import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -38,36 +41,18 @@ import org.springframework.web.server.ResponseStatusException; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; -/** - * Controller handling agent execution endpoints. - * - *

This controller provides both non-streaming and streaming (SSE) endpoints for agent execution. - * The SSE endpoint uses the {@link SseEventStreamService} for clean, reusable event streaming. - * - *

Note: The default SSE endpoint is now HttpServer-based at {@code /run_sse}. This - * Spring-based endpoint is available at {@code /run_sse_spring} for applications that prefer - * Spring's SseEmitter. - * - * @author Sandeep Belgavi - * @since January 24, 2026 - */ +/** Controller handling agent execution endpoints. */ @RestController public class ExecutionController { private static final Logger log = LoggerFactory.getLogger(ExecutionController.class); private final RunnerService runnerService; - private final SseEventStreamService sseEventStreamService; - private final PassThroughEventProcessor passThroughProcessor; + private final ExecutorService sseExecutor = Executors.newCachedThreadPool(); @Autowired - public ExecutionController( - RunnerService runnerService, - SseEventStreamService sseEventStreamService, - PassThroughEventProcessor passThroughProcessor) { + public ExecutionController(RunnerService runnerService) { this.runnerService = runnerService; - this.sseEventStreamService = sseEventStreamService; - this.passThroughProcessor = passThroughProcessor; } /** @@ -96,7 +81,11 @@ public List agentRun(@RequestBody AgentRunRequest request) { RunConfig runConfig = RunConfig.builder().setStreamingMode(StreamingMode.NONE).build(); Flowable eventStream = runner.runAsync( - request.userId, request.sessionId, request.newMessage, runConfig, request.stateDelta); + request.userId, + request.sessionId, + request.getNewMessage(), + runConfig, + request.stateDelta); List events = Lists.newArrayList(eventStream.blockingIterable()); log.info("Agent run for session {} generated {} events.", request.sessionId, events.size()); @@ -108,92 +97,147 @@ public List agentRun(@RequestBody AgentRunRequest request) { } /** - * Executes an agent run and streams the resulting events using Server-Sent Events (SSE) via - * Spring. - * - *

This endpoint uses the {@link SseEventStreamService} to provide clean, reusable SSE - * streaming using Spring's SseEmitter. Events are sent to the client in real-time as they are - * generated by the agent. + * Executes an agent run and streams the resulting events using Server-Sent Events (SSE). * - *

Note: This is the Spring-based SSE endpoint. The default SSE endpoint is - * HttpServer-based at {@code /run_sse} (zero dependencies). Use this endpoint if you prefer - * Spring's framework features. - * - *

Request Format: - * - *

{@code
-   * {
-   *   "appName": "my-app",
-   *   "userId": "user123",
-   *   "sessionId": "session456",
-   *   "newMessage": {
-   *     "role": "user",
-   *     "parts": [{"text": "Hello"}]
-   *   },
-   *   "streaming": true,
-   *   "stateDelta": {"key": "value"}
-   * }
-   * }
- * - *

Response: Server-Sent Events stream with Content-Type: text/event-stream - * - * @param request The AgentRunRequest containing run details - * @return SseEmitter that streams events to the client - * @throws ResponseStatusException if request validation fails - * @author Sandeep Belgavi - * @since January 24, 2026 + * @param request The AgentRunRequest containing run details. + * @return A Flux that will stream events to the client. */ - @PostMapping(value = "/run_sse_spring", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - public SseEmitter agentRunSseSpring(@RequestBody AgentRunRequest request) { - // Validate request + @PostMapping(value = "/run_sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public SseEmitter agentRunSse(@RequestBody AgentRunRequest request) { + SseEmitter emitter = new SseEmitter(60 * 60 * 1000L); // 1 hour timeout + if (request.appName == null || request.appName.trim().isEmpty()) { log.warn( - "appName cannot be null or empty in SSE request for appName: {}, session: {}", + "appName cannot be null or empty in SseEmitter request for appName: {}, session: {}", request.appName, request.sessionId); - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "appName cannot be null or empty"); + emitter.completeWithError( + new ResponseStatusException(HttpStatus.BAD_REQUEST, "appName cannot be null or empty")); + return emitter; } if (request.sessionId == null || request.sessionId.trim().isEmpty()) { log.warn( - "sessionId cannot be null or empty in SSE request for appName: {}, session: {}", + "sessionId cannot be null or empty in SseEmitter request for appName: {}, session: {}", request.appName, request.sessionId); - throw new ResponseStatusException( - HttpStatus.BAD_REQUEST, "sessionId cannot be null or empty"); + emitter.completeWithError( + new ResponseStatusException(HttpStatus.BAD_REQUEST, "sessionId cannot be null or empty")); + return emitter; } log.info( - "Spring SSE request received for POST /run_sse_spring for session: {}", request.sessionId); - - try { - // Get runner for the app - Runner runner = runnerService.getRunner(request.appName); - - // Build run config - RunConfig runConfig = - RunConfig.builder() - .setStreamingMode(request.getStreaming() ? StreamingMode.SSE : StreamingMode.NONE) - .build(); - - // Stream events using the service - return sseEventStreamService.streamEvents( - runner, - request.appName, - request.userId, - request.sessionId, - request.newMessage, - runConfig, - request.stateDelta, - passThroughProcessor); // Use pass-through processor for generic endpoint - - } catch (ResponseStatusException e) { - // Re-throw HTTP exceptions - throw e; - } catch (Exception e) { - log.error( - "Error setting up SSE stream for session {}: {}", request.sessionId, e.getMessage(), e); - throw new ResponseStatusException( - HttpStatus.INTERNAL_SERVER_ERROR, "Failed to setup SSE stream", e); - } + "SseEmitter Request received for POST /run_sse_emitter for session: {}", request.sessionId); + + final String sessionId = request.sessionId; + sseExecutor.execute( + () -> { + Runner runner; + try { + runner = this.runnerService.getRunner(request.appName); + } catch (ResponseStatusException e) { + log.warn( + "Setup failed for SseEmitter request for session {}: {}", + sessionId, + e.getMessage()); + try { + emitter.completeWithError(e); + } catch (Exception ex) { + log.warn( + "Error completing emitter after setup failure for session {}: {}", + sessionId, + ex.getMessage()); + } + return; + } + + final RunConfig runConfig = + RunConfig.builder() + .setStreamingMode(request.getStreaming() ? StreamingMode.SSE : StreamingMode.NONE) + .build(); + + Flowable eventFlowable = + runner.runAsync( + request.userId, + request.sessionId, + request.getNewMessage(), + runConfig, + request.stateDelta); + + Disposable disposable = + eventFlowable + .observeOn(Schedulers.io()) + .subscribe( + event -> { + try { + log.debug( + "SseEmitter: Sending event {} for session {}", event.id(), sessionId); + emitter.send(SseEmitter.event().data(event)); + } catch (IOException e) { + log.error( + "SseEmitter: IOException sending event for session {}: {}", + sessionId, + e.getMessage()); + throw new RuntimeException("Failed to send event", e); + } catch (Exception e) { + log.error( + "SseEmitter: Unexpected error sending event for session {}: {}", + sessionId, + e.getMessage(), + e); + throw new RuntimeException("Unexpected error sending event", e); + } + }, + error -> { + log.error( + "SseEmitter: Stream error for session {}: {}", + sessionId, + error.getMessage(), + error); + try { + emitter.completeWithError(error); + } catch (Exception ex) { + log.warn( + "Error completing emitter after stream error for session {}: {}", + sessionId, + ex.getMessage()); + } + }, + () -> { + log.debug( + "SseEmitter: Stream completed normally for session: {}", sessionId); + try { + emitter.complete(); + } catch (Exception ex) { + log.warn( + "Error completing emitter after normal completion for session {}:" + + " {}", + sessionId, + ex.getMessage()); + } + }); + emitter.onCompletion( + () -> { + log.debug( + "SseEmitter: onCompletion callback for session: {}. Disposing subscription.", + sessionId); + if (!disposable.isDisposed()) { + disposable.dispose(); + } + }); + emitter.onTimeout( + () -> { + log.debug( + "SseEmitter: onTimeout callback for session: {}. Disposing subscription and" + + " completing.", + sessionId); + if (!disposable.isDisposed()) { + disposable.dispose(); + } + emitter.complete(); + }); + }); + + log.debug("SseEmitter: Returning emitter for session: {}", sessionId); + return emitter; } } diff --git a/dev/src/main/java/com/google/adk/web/controller/httpserver/HttpServerSseController.java b/dev/src/main/java/com/google/adk/web/controller/httpserver/HttpServerSseController.java index 7777dfb95..767590da9 100644 --- a/dev/src/main/java/com/google/adk/web/controller/httpserver/HttpServerSseController.java +++ b/dev/src/main/java/com/google/adk/web/controller/httpserver/HttpServerSseController.java @@ -172,7 +172,11 @@ private void streamEvents( // Get event stream io.reactivex.rxjava3.core.Flowable eventFlowable = runner.runAsync( - request.userId, request.sessionId, request.newMessage, runConfig, request.stateDelta); + request.userId, + request.sessionId, + request.getNewMessage(), + runConfig, + request.stateDelta); // Use CountDownLatch to wait for stream completion java.util.concurrent.CountDownLatch latch = new java.util.concurrent.CountDownLatch(1); diff --git a/dev/src/main/java/com/google/adk/web/dto/AgentRunRequest.java b/dev/src/main/java/com/google/adk/web/dto/AgentRunRequest.java index 652de5aac..025aa64eb 100644 --- a/dev/src/main/java/com/google/adk/web/dto/AgentRunRequest.java +++ b/dev/src/main/java/com/google/adk/web/dto/AgentRunRequest.java @@ -17,6 +17,7 @@ package com.google.adk.web.dto; import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.adk.JsonBaseModel; import com.google.genai.types.Content; import java.util.Map; import javax.annotation.Nullable; @@ -36,7 +37,7 @@ public class AgentRunRequest { public String sessionId; @JsonProperty("newMessage") - public Content newMessage; + public Object newMessage; @JsonProperty("streaming") public boolean streaming = false; @@ -65,7 +66,17 @@ public String getSessionId() { } public Content getNewMessage() { - return newMessage; + if (newMessage instanceof Content) { + return (Content) newMessage; + } + if (newMessage != null) { + try { + return JsonBaseModel.getMapper().convertValue(newMessage, Content.class); + } catch (IllegalArgumentException e) { + throw new IllegalStateException("Failed to parse newMessage into Content", e); + } + } + return null; } public boolean getStreaming() { diff --git a/dev/src/test/java/com/google/adk/web/AdkWebServerTest.java b/dev/src/test/java/com/google/adk/web/AdkWebServerTest.java index ac5b39390..53505d7e2 100644 --- a/dev/src/test/java/com/google/adk/web/AdkWebServerTest.java +++ b/dev/src/test/java/com/google/adk/web/AdkWebServerTest.java @@ -25,8 +25,8 @@ import org.hamcrest.Matchers; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; diff --git a/dev/src/test/java/com/google/adk/web/AdkWebServerUITest.java b/dev/src/test/java/com/google/adk/web/AdkWebServerUITest.java index f4287f786..cfb162db2 100644 --- a/dev/src/test/java/com/google/adk/web/AdkWebServerUITest.java +++ b/dev/src/test/java/com/google/adk/web/AdkWebServerUITest.java @@ -24,8 +24,8 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; import org.springframework.test.web.servlet.MockMvc; /** diff --git a/maven_plugin/examples/custom_tools/pom.xml b/maven_plugin/examples/custom_tools/pom.xml index 910e74439..abd3c60f2 100644 --- a/maven_plugin/examples/custom_tools/pom.xml +++ b/maven_plugin/examples/custom_tools/pom.xml @@ -4,7 +4,7 @@ com.example custom-tools-example - 1.2.0 + 0.8.1-SNAPSHOT jar ADK Custom Tools Example diff --git a/maven_plugin/examples/simple-agent/pom.xml b/maven_plugin/examples/simple-agent/pom.xml index 17256f364..309fe9364 100644 --- a/maven_plugin/examples/simple-agent/pom.xml +++ b/maven_plugin/examples/simple-agent/pom.xml @@ -4,7 +4,7 @@ com.example simple-adk-agent - 1.2.0 + 0.8.1-SNAPSHOT jar Simple ADK Agent Example diff --git a/maven_plugin/pom.xml b/maven_plugin/pom.xml index 3cf9bd7bd..6ff3404f3 100644 --- a/maven_plugin/pom.xml +++ b/maven_plugin/pom.xml @@ -5,7 +5,7 @@ com.google.adk google-adk-parent - 1.2.0 + 0.8.1-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index 16ae4ad19..ffe904d74 100644 --- a/pom.xml +++ b/pom.xml @@ -17,7 +17,7 @@ com.google.adk google-adk-parent - 1.2.0 + 0.8.1-SNAPSHOT pom Google Agent Development Kit Maven Parent POM @@ -30,57 +30,65 @@ maven_plugin contrib/langchain4j contrib/spring-ai - contrib/sarvam-ai contrib/samples contrib/firestore-session-service tutorials/city-time-weather tutorials/live-audio-single-agent a2a - a2a/webservice 17 ${java.version} UTF-8 + 3.6.0 - 1.11.0 - 3.4.1 - 1.49.0 + 1.11.1 + 4.0.2 + + 1.51.0 0.14.0 - 2.38.0 - 1.32.0 - 4.32.0 + 2.47.0 + 1.41.0 + 4.33.5 5.11.4 5.20.0 1.6.0 - 2.19.0 - 4.12.0 - 3.3.6 + 2.20.2 + 5.3.2 + 3.7.0 0.18.1 3.41.0 3.9.0 - 1.8.0 + 1.11.0 2.0.17 - 1.4.4 + 1.4.5 1.0.0 - 3.1.5 + 3.1.12 3.7.0 2.35.1 - 3.27.3 - 1.4.0 + 3.27.7 + 2.15.0 3.9.0 - 5.4.3 - 1.2.0 + 5.6 + + com.fasterxml.jackson + jackson-bom + ${jackson.version} + pom + import + com.google.cloud libraries-bom - 26.53.0 + 26.76.0 pom import @@ -112,6 +120,13 @@ pom import + + com.squareup.okhttp3 + okhttp-bom + ${okhttp.version} + pom + import + @@ -144,11 +159,6 @@ google-genai ${google.genai.version} - - com.squareup.okhttp3 - okhttp - ${okhttp.version} - com.google.auto.value auto-value-annotations @@ -159,36 +169,36 @@ error_prone_annotations ${errorprone.version} - - com.fasterxml.jackson.core - jackson-core - ${jackson.version} - - - com.fasterxml.jackson.core - jackson-annotations - ${jackson.version} - - - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} - - - com.fasterxml.jackson.datatype - jackson-datatype-jdk8 - ${jackson.version} - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - ${jackson.version} - - - com.fasterxml.jackson.dataformat - jackson-dataformat-yaml - ${jackson.version} - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + com.google.protobuf protobuf-java @@ -274,11 +284,6 @@ assertj-core ${assertj.version} - - net.javacrumbs.future-converter - future-converter-java8-guava - ${future-converter-java8-guava.version} - @@ -292,6 +297,11 @@ + + org.apache.maven.plugins + maven-checkstyle-plugin + ${maven.checkstyle.plugin.version} + maven-clean-plugin 3.1.0 @@ -467,6 +477,40 @@ + + illegal-optional-check + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + illegal-optional-check + + check + + compile + + + + + + + + + + + + + + + + + + + + release-sonatype diff --git a/tutorials/city-time-weather/pom.xml b/tutorials/city-time-weather/pom.xml index b1acf7c56..f4e8bdb52 100644 --- a/tutorials/city-time-weather/pom.xml +++ b/tutorials/city-time-weather/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 1.2.0 + 0.8.1-SNAPSHOT ../../pom.xml diff --git a/tutorials/live-audio-single-agent/pom.xml b/tutorials/live-audio-single-agent/pom.xml index 463565e7c..b6e649222 100644 --- a/tutorials/live-audio-single-agent/pom.xml +++ b/tutorials/live-audio-single-agent/pom.xml @@ -20,7 +20,7 @@ com.google.adk google-adk-parent - 1.2.0 + 0.8.1-SNAPSHOT ../../pom.xml