Skip to content

Commit db58ecd

Browse files
authored
Merge branch 'main' into fix/skill-toolset-instruction-tool-filter
2 parents 57ea45c + bb56aa5 commit db58ecd

25 files changed

Lines changed: 3319 additions & 92 deletions
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Eventarc Tools Sample
2+
3+
## Introduction
4+
5+
This sample agent demonstrates the Eventarc first-party tool in ADK,
6+
distributed via the `google.adk.integrations.eventarc` module. This tool suite currently includes:
7+
8+
1. `publish_message`
9+
10+
Publishes a structured event in CloudEvents format to a Google Cloud Eventarc message bus.
11+
12+
## How to use
13+
14+
Set up environment variables in your `.env` file for using
15+
[Google AI Studio](https://google.github.io/adk-docs/get-started/quickstart/#gemini---google-ai-studio)
16+
or
17+
[Google Cloud Vertex AI](https://google.github.io/adk-docs/get-started/quickstart/#gemini---google-cloud-vertex-ai)
18+
for the LLM service for your agent. For example, for using Google AI Studio you
19+
would set:
20+
21+
- GOOGLE_GENAI_USE_VERTEXAI=FALSE
22+
- GOOGLE_API_KEY={your api key}
23+
24+
### With Application Default Credentials
25+
26+
This mode is useful for quick development when the agent builder is the only
27+
user interacting with the agent. The tools are run with these credentials.
28+
29+
1. Create application default credentials on the machine where the agent would
30+
be running by following https://cloud.google.com/docs/authentication/provide-credentials-adc.
31+
32+
1. Set `CREDENTIALS_TYPE=None` in `agent.py`
33+
34+
1. Run the agent
35+
36+
### With Service Account Keys
37+
38+
This mode is useful for quick development when the agent builder wants to run
39+
the agent with service account credentials. The tools are run with these
40+
credentials.
41+
42+
1. Create service account key by following https://cloud.google.com/iam/docs/service-account-creds#user-managed-keys.
43+
44+
1. Set `CREDENTIALS_TYPE=AuthCredentialTypes.SERVICE_ACCOUNT` in `agent.py`
45+
46+
1. Download the key file and replace `"service_account_key.json"` with the path
47+
48+
1. Run the agent
49+
50+
### With Interactive OAuth
51+
52+
1. Follow
53+
https://developers.google.com/identity/protocols/oauth2#1.-obtain-oauth-2.0-credentials-from-the-dynamic_data.setvar.console_name.
54+
to get your client id and client secret. Be sure to choose "web" as your client
55+
type.
56+
57+
1. Follow https://developers.google.com/workspace/guides/configure-oauth-consent to add scope "https://www.googleapis.com/auth/cloud-platform".
58+
59+
1. Follow https://developers.google.com/identity/protocols/oauth2/web-server#creatingcred to add http://localhost/dev-ui/ to "Authorized redirect URIs".
60+
61+
Note: localhost here is just a hostname that you use to access the dev ui,
62+
replace it with the actual hostname you use to access the dev ui.
63+
64+
1. For 1st run, allow popup for localhost in Chrome.
65+
66+
1. Configure your `.env` file to add two more variables before running the agent:
67+
68+
- OAUTH_CLIENT_ID={your client id}
69+
- OAUTH_CLIENT_SECRET={your client secret}
70+
71+
Note: don't create a separate .env, instead put it to the same .env file that
72+
stores your Vertex AI or Dev ML credentials
73+
74+
1. Set `CREDENTIALS_TYPE=AuthCredentialTypes.OAUTH2` in `agent.py` and run the agent
75+
76+
### With Agent Identity (in Agent Runtime / Vertex AI Reasoning Engine)
77+
78+
When deploying this agent to Agent Runtime, it can use its unique SPIFFE-based Agent Identity to authenticate. This is the recommended security best practice.
79+
80+
1. **Configure Deployment**: Create a `.agent_engine_config.json` file in this directory to specify the identity type:
81+
82+
```json
83+
{
84+
"identity_type": "AGENT_IDENTITY"
85+
}
86+
```
87+
88+
1. **Use Default Credentials**: Leave `CREDENTIALS_TYPE = None` in `agent.py` (which is the default). This configures the agent to use Application Default Credentials (ADC), which automatically resolves to the Agent Identity in the container runtime environment.
89+
90+
1. **Deploy the Agent**: Deploy your agent using the ADK CLI:
91+
92+
```bash
93+
uv run adk deploy agent_engine \
94+
--project=YOUR_PROJECT_ID \
95+
--region=YOUR_REGION \
96+
--display_name=eventarc-agent-test \
97+
contributing/samples/integrations/eventarc
98+
```
99+
100+
Take note of the generated **Reasoning Engine ID** (e.g., `1234567890`) and the **Project Number** of your project.
101+
102+
1. **Grant IAM Permissions**: Grant the Eventarc Message Bus User role (`roles/eventarc.messageBusUser`) to the Agent Identity principal at the project level:
103+
104+
```bash
105+
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
106+
--member="principal://agents.global.org-171145599760.system.id.goog/resources/aiplatform/projects/YOUR_PROJECT_NUMBER/locations/YOUR_REGION/reasoningEngines/YOUR_REASONING_ENGINE_ID" \
107+
--role="roles/eventarc.messageBusUser"
108+
```
109+
110+
*Note: Eventarc Advanced message buses require `roles/eventarc.messageBusUser` for publishing, rather than `roles/eventarc.publisher`.*
111+
112+
## Sample prompts
113+
114+
- "Publish an event of type 'com.example.hello' to bus 'projects/my-project/locations/global/messageBuses/my-bus' with data 'Hello World' and source '//my/agent'"
115+
- "Send a JSON payload to Eventarc bus 'projects/my-project/locations/global/messageBuses/my-bus' representing a user sign-up event"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
import textwrap
17+
18+
from google.adk.agents.llm_agent import LlmAgent
19+
from google.adk.auth.auth_credential import AuthCredentialTypes
20+
from google.adk.integrations.eventarc import EventarcCredentialsConfig
21+
from google.adk.integrations.eventarc import EventarcToolConfig
22+
from google.adk.integrations.eventarc import EventarcToolset
23+
import google.auth
24+
25+
# Define the desired credential type.
26+
# By default use Application Default Credentials (ADC) from the local
27+
# environment, which can be set up by following
28+
# https://cloud.google.com/docs/authentication/provide-credentials-adc.
29+
CREDENTIALS_TYPE = None
30+
31+
# Define an appropriate application name
32+
EVENTARC_AGENT_NAME = "adk_sample_eventarc_agent"
33+
34+
35+
# Define Eventarc tool config.
36+
# You can optionally set the project_id here, or let the agent infer it from context/user input.
37+
tool_config = EventarcToolConfig(project_id=os.getenv("GOOGLE_CLOUD_PROJECT"))
38+
39+
if CREDENTIALS_TYPE == AuthCredentialTypes.OAUTH2:
40+
# Initialize the tools to do interactive OAuth
41+
# The environment variables OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET
42+
# must be set
43+
credentials_config = EventarcCredentialsConfig(
44+
client_id=os.getenv("OAUTH_CLIENT_ID"),
45+
client_secret=os.getenv("OAUTH_CLIENT_SECRET"),
46+
)
47+
elif CREDENTIALS_TYPE == AuthCredentialTypes.SERVICE_ACCOUNT:
48+
# Initialize the tools to use the credentials in the service account key.
49+
# If this flow is enabled, make sure to replace the file path with your own
50+
# service account key file
51+
# https://cloud.google.com/iam/docs/service-account-creds#user-managed-keys
52+
creds, _ = google.auth.load_credentials_from_file("service_account_key.json")
53+
credentials_config = EventarcCredentialsConfig(credentials=creds)
54+
else:
55+
# Initialize the tools to use the application default credentials.
56+
# https://cloud.google.com/docs/authentication/provide-credentials-adc
57+
application_default_credentials, _ = google.auth.default()
58+
credentials_config = EventarcCredentialsConfig(
59+
credentials=application_default_credentials
60+
)
61+
62+
eventarc_toolset = EventarcToolset(
63+
credentials_config=credentials_config, tool_config=tool_config
64+
)
65+
66+
# The variable name `root_agent` determines what your root agent is for the
67+
# debug CLI
68+
root_agent = LlmAgent(
69+
name=EVENTARC_AGENT_NAME,
70+
description=(
71+
"Agent to publish structured CloudEvents to Google Cloud Eventarc."
72+
),
73+
instruction=textwrap.dedent("""\
74+
You are a cloud engineer agent with access to Google Cloud Eventarc tools.
75+
You can publish CloudEvents structured messages to Eventarc message buses.
76+
"""),
77+
tools=[eventarc_toolset],
78+
)

src/google/adk/a2a/converters/event_converter.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,13 @@ def convert_a2a_task_to_event(
234234
):
235235
message = a2a_task.status.message
236236
elif a2a_task.history:
237-
message = a2a_task.history[-1]
237+
# Only pick agent-role messages from history; a trailing user
238+
# message should not be misattributed as agent output.
239+
agent_messages = [
240+
m for m in a2a_task.history if m.role == _compat.ROLE_AGENT
241+
]
242+
if agent_messages:
243+
message = agent_messages[-1]
238244

239245
# Convert message if available
240246
if message:
@@ -288,6 +294,8 @@ def convert_a2a_message_to_event(
288294
if a2a_message is None:
289295
raise ValueError("A2A message cannot be None")
290296

297+
genai_role = _compat.role_to_str(a2a_message.role)
298+
291299
if not a2a_message.parts:
292300
logger.warning(
293301
"A2A message has no parts, creating event with empty content"
@@ -300,7 +308,7 @@ def convert_a2a_message_to_event(
300308
),
301309
author=author or "a2a agent",
302310
branch=invocation_context.branch if invocation_context else None,
303-
content=genai_types.Content(role="model", parts=[]),
311+
content=genai_types.Content(role=genai_role, parts=[]),
304312
)
305313

306314
try:
@@ -355,7 +363,7 @@ def convert_a2a_message_to_event(
355363
if long_running_tool_ids
356364
else None,
357365
content=genai_types.Content(
358-
role="model",
366+
role=genai_role,
359367
parts=output_parts,
360368
),
361369
)

src/google/adk/agents/remote_a2a_agent.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -543,21 +543,26 @@ async def _handle_a2a_response(
543543
for part in event.content.parts:
544544
part.thought = True
545545
_add_mock_function_call(event, update.status.state)
546-
elif isinstance(update, A2ATaskArtifactUpdateEvent) and (
547-
not update.append or update.last_chunk
548-
):
546+
elif isinstance(update, A2ATaskArtifactUpdateEvent):
549547
# This is a streaming task artifact update.
550-
# We only handle full artifact updates and ignore partial updates.
551-
# Note: Depends on the server implementation, there is no clear
552-
# definition of what a partial update is currently. We use the two
553-
# signals:
554-
# 1. append: True for partial updates, False for full updates.
555-
# 2. last_chunk: True for full updates, False for partial updates.
556-
event = convert_a2a_task_to_event(
557-
task, self.name, ctx, self._a2a_part_converter
548+
# Convert only the parts carried by this update. Converting the
549+
# accumulated task here would re-emit earlier chunks of the same
550+
# artifact, duplicating already-streamed content.
551+
if not update.artifact.parts:
552+
return None
553+
event = convert_a2a_message_to_event(
554+
_compat.make_message(
555+
message_id="",
556+
role="agent",
557+
parts=update.artifact.parts,
558+
),
559+
self.name,
560+
ctx,
561+
self._a2a_part_converter,
558562
)
559563
if not event:
560564
return None
565+
event.partial = not update.last_chunk
561566
else:
562567
# This is a streaming update without a message (e.g. status change)
563568
# or a partial artifact update. We don't emit an event for these

src/google/adk/features/_feature_registry.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ class FeatureName(str, Enum):
4141
DAYTONA_ENVIRONMENT = "DAYTONA_ENVIRONMENT"
4242
E2B_ENVIRONMENT = "E2B_ENVIRONMENT"
4343
ENVIRONMENT_SIMULATION = "ENVIRONMENT_SIMULATION"
44+
EVENTARC_TOOL_CONFIG = "EVENTARC_TOOL_CONFIG"
45+
EVENTARC_TOOLSET = "EVENTARC_TOOLSET"
4446
GCS_ADMIN_TOOLSET = "GCS_ADMIN_TOOLSET"
4547
GCS_TOOL_SETTINGS = "GCS_TOOL_SETTINGS"
4648
GCS_TOOLSET = "GCS_TOOLSET"
@@ -144,6 +146,12 @@ class FeatureConfig:
144146
FeatureName.ENVIRONMENT_SIMULATION: FeatureConfig(
145147
FeatureStage.EXPERIMENTAL, default_on=True
146148
),
149+
FeatureName.EVENTARC_TOOL_CONFIG: FeatureConfig(
150+
FeatureStage.EXPERIMENTAL, default_on=True
151+
),
152+
FeatureName.EVENTARC_TOOLSET: FeatureConfig(
153+
FeatureStage.EXPERIMENTAL, default_on=True
154+
),
147155
FeatureName.GCS_ADMIN_TOOLSET: FeatureConfig(
148156
FeatureStage.EXPERIMENTAL, default_on=True
149157
),

src/google/adk/flows/llm_flows/agent_transfer.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
from __future__ import annotations
1818

1919
import typing
20-
from typing import Any
2120
from typing import AsyncGenerator
21+
from typing import Sequence
2222

2323
from typing_extensions import override
2424

@@ -30,8 +30,8 @@
3030
from ._base_llm_processor import BaseLlmRequestProcessor
3131

3232
if typing.TYPE_CHECKING:
33-
from ...agents import BaseAgent
34-
from ...agents import LlmAgent
33+
from ...agents.base_agent import BaseAgent
34+
from ...agents.llm_agent import LlmAgent
3535

3636

3737
class _AgentTransferLlmRequestProcessor(BaseLlmRequestProcessor):
@@ -72,8 +72,12 @@ async def run_async(
7272
request_processor = _AgentTransferLlmRequestProcessor()
7373

7474

75-
def _build_target_agents_info(target_agent: Any) -> str:
76-
# TODO: Refactor the annotation of the parameters
75+
class _AgentLike(typing.Protocol):
76+
name: str
77+
description: str
78+
79+
80+
def _build_target_agents_info(target_agent: _AgentLike) -> str:
7781
return f"""
7882
Agent name: {target_agent.name}
7983
Agent description: {target_agent.description}
@@ -85,17 +89,16 @@ def _build_target_agents_info(target_agent: Any) -> str:
8589

8690
def _build_transfer_instruction_body(
8791
tool_name: str,
88-
target_agents: list[Any],
92+
target_agents: Sequence[_AgentLike],
8993
) -> str:
9094
"""Build the core transfer instruction text.
91-
TODO: Refactor the annotation of the parameters
9295
9396
This is the agent-tree-agnostic portion of transfer instructions. It
94-
works with any objects having ``.name`` and ``.description`` attributes
97+
works with any BaseAgent implementation.
9598
9699
Args:
97100
tool_name: The name of the transfer tool (e.g. 'transfer_to_agent').
98-
target_agents: Objects with ``.name`` and ``.description``.
101+
target_agents: Agents available as transfer targets.
99102
100103
Returns:
101104
Instruction text for the LLM about agent transfers.
@@ -128,8 +131,8 @@ def _build_transfer_instruction_body(
128131

129132
def _build_transfer_instructions(
130133
tool_name: str,
131-
agent: 'LlmAgent',
132-
target_agents: list['BaseAgent'],
134+
agent: LlmAgent,
135+
target_agents: Sequence[BaseAgent],
133136
) -> str:
134137
"""Build instructions for agent transfer (agent-tree variant).
135138

0 commit comments

Comments
 (0)