agent-starter-pack 0.18.1__py3-none-any.whl → 0.19.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of agent-starter-pack might be problematic. Click here for more details.

Files changed (46) hide show
  1. agent_starter_pack/agents/adk_a2a_base/.template/templateconfig.yaml +22 -0
  2. agent_starter_pack/agents/adk_a2a_base/README.md +22 -0
  3. agent_starter_pack/agents/adk_a2a_base/app/__init__.py +17 -0
  4. agent_starter_pack/agents/adk_a2a_base/app/agent.py +70 -0
  5. agent_starter_pack/agents/adk_a2a_base/notebooks/adk_a2a_app_testing.ipynb +600 -0
  6. agent_starter_pack/agents/adk_a2a_base/notebooks/evaluating_adk_agent.ipynb +1535 -0
  7. agent_starter_pack/agents/adk_a2a_base/tests/integration/test_agent.py +58 -0
  8. agent_starter_pack/base_template/.gitignore +1 -1
  9. agent_starter_pack/base_template/Makefile +11 -11
  10. agent_starter_pack/base_template/README.md +1 -1
  11. agent_starter_pack/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/deploy-to-prod.yaml +10 -2
  12. agent_starter_pack/base_template/{% if cookiecutter.cicd_runner == 'github_actions' %}.github{% else %}unused_github{% endif %}/workflows/staging.yaml +26 -5
  13. agent_starter_pack/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/deploy-to-prod.yaml +18 -3
  14. agent_starter_pack/base_template/{% if cookiecutter.cicd_runner == 'google_cloud_build' %}.cloudbuild{% else %}unused_.cloudbuild{% endif %}/staging.yaml +34 -3
  15. agent_starter_pack/cli/utils/cicd.py +20 -4
  16. agent_starter_pack/cli/utils/register_gemini_enterprise.py +79 -84
  17. agent_starter_pack/cli/utils/template.py +2 -0
  18. agent_starter_pack/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +104 -2
  19. agent_starter_pack/deployment_targets/agent_engine/tests/load_test/load_test.py +144 -0
  20. agent_starter_pack/deployment_targets/agent_engine/tests/{% if cookiecutter.is_adk_a2a %}helpers.py{% else %}unused_helpers.py{% endif %} +138 -0
  21. agent_starter_pack/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/agent_engine_app.py +88 -4
  22. agent_starter_pack/deployment_targets/agent_engine/{{cookiecutter.agent_directory}}/utils/deployment.py +4 -0
  23. agent_starter_pack/deployment_targets/cloud_run/Dockerfile +3 -0
  24. agent_starter_pack/deployment_targets/cloud_run/deployment/terraform/dev/service.tf +7 -0
  25. agent_starter_pack/deployment_targets/cloud_run/deployment/terraform/service.tf +16 -2
  26. agent_starter_pack/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +218 -1
  27. agent_starter_pack/deployment_targets/cloud_run/tests/load_test/README.md +2 -2
  28. agent_starter_pack/deployment_targets/cloud_run/tests/load_test/load_test.py +51 -4
  29. agent_starter_pack/deployment_targets/cloud_run/{{cookiecutter.agent_directory}}/server.py +66 -0
  30. agent_starter_pack/resources/locks/uv-adk_a2a_base-agent_engine.lock +4224 -0
  31. agent_starter_pack/resources/locks/uv-adk_a2a_base-cloud_run.lock +4819 -0
  32. agent_starter_pack/resources/locks/uv-adk_base-agent_engine.lock +230 -236
  33. agent_starter_pack/resources/locks/uv-adk_base-cloud_run.lock +290 -296
  34. agent_starter_pack/resources/locks/uv-adk_live-agent_engine.lock +230 -236
  35. agent_starter_pack/resources/locks/uv-adk_live-cloud_run.lock +290 -296
  36. agent_starter_pack/resources/locks/uv-agentic_rag-agent_engine.lock +234 -239
  37. agent_starter_pack/resources/locks/uv-agentic_rag-cloud_run.lock +294 -299
  38. agent_starter_pack/resources/locks/uv-crewai_coding_crew-agent_engine.lock +221 -228
  39. agent_starter_pack/resources/locks/uv-crewai_coding_crew-cloud_run.lock +279 -286
  40. agent_starter_pack/resources/locks/uv-langgraph_base_react-agent_engine.lock +226 -233
  41. agent_starter_pack/resources/locks/uv-langgraph_base_react-cloud_run.lock +298 -305
  42. {agent_starter_pack-0.18.1.dist-info → agent_starter_pack-0.19.0.dist-info}/METADATA +2 -1
  43. {agent_starter_pack-0.18.1.dist-info → agent_starter_pack-0.19.0.dist-info}/RECORD +46 -36
  44. {agent_starter_pack-0.18.1.dist-info → agent_starter_pack-0.19.0.dist-info}/WHEEL +0 -0
  45. {agent_starter_pack-0.18.1.dist-info → agent_starter_pack-0.19.0.dist-info}/entry_points.txt +0 -0
  46. {agent_starter_pack-0.18.1.dist-info → agent_starter_pack-0.19.0.dist-info}/licenses/LICENSE +0 -0
@@ -213,18 +213,52 @@ def test_feedback_endpoint(server_fixture: subprocess.Popen[str]) -> None:
213
213
  logger.info("Feedback endpoint test passed")
214
214
  {% else %}
215
215
 
216
+ # mypy: disable-error-code="arg-type"
217
+ {%- if cookiecutter.is_adk_a2a %}
218
+
219
+ import os
220
+
221
+ import pytest
222
+ import pytest_asyncio
223
+ from google.adk.artifacts import InMemoryArtifactService
224
+ from google.adk.sessions import InMemorySessionService
225
+
226
+ from {{cookiecutter.agent_directory}}.agent_engine_app import AgentEngineApp
227
+ from tests.helpers import (
228
+ build_get_request,
229
+ build_post_request,
230
+ poll_task_completion,
231
+ )
232
+ {%- elif cookiecutter.is_adk %}
233
+
216
234
  import logging
217
235
 
218
236
  import pytest
219
- {%- if cookiecutter.is_adk %}
220
237
  from google.adk.events.event import Event
221
238
 
222
239
  from {{cookiecutter.agent_directory}}.agent import root_agent
223
240
  from {{cookiecutter.agent_directory}}.agent_engine_app import AgentEngineApp
224
241
  {%- else %}
225
242
 
243
+ import logging
244
+
245
+ import pytest
246
+
226
247
  from {{cookiecutter.agent_directory}}.agent_engine_app import AgentEngineApp
227
248
  {%- endif %}
249
+ {%- if cookiecutter.is_adk_a2a %}
250
+
251
+
252
+ @pytest_asyncio.fixture
253
+ async def agent_app() -> AgentEngineApp:
254
+ """Fixture to create and set up AgentEngineApp instance"""
255
+ app = await AgentEngineApp.create(
256
+ artifact_service_builder=lambda: InMemoryArtifactService(),
257
+ session_service_builder=lambda: InMemorySessionService(),
258
+ )
259
+ app.set_up()
260
+ return app
261
+ {%- else %}
228
262
 
229
263
 
230
264
  @pytest.fixture
@@ -237,8 +271,75 @@ def agent_app() -> AgentEngineApp:
237
271
  {%- endif %}
238
272
  app.set_up()
239
273
  return app
274
+ {% endif %}
275
+ {%- if cookiecutter.is_adk_a2a %}
276
+
277
+
278
+ @pytest.mark.asyncio
279
+ async def test_agent_on_message_send(agent_app: AgentEngineApp) -> None:
280
+ """Test complete A2A message workflow from send to task completion with artifacts."""
281
+ # Send message
282
+ message_data = {
283
+ "message": {
284
+ "messageId": f"msg-{os.urandom(8).hex()}",
285
+ "content": [{"text": "What is the capital of France?"}],
286
+ "role": "ROLE_USER",
287
+ },
288
+ }
289
+ response = await agent_app.on_message_send(
290
+ request=build_post_request(message_data),
291
+ context=None,
292
+ )
293
+
294
+ # Verify task creation
295
+ assert "task" in response and "id" in response["task"], (
296
+ "Expected task with ID in response"
297
+ )
298
+
299
+ # Poll for completion
300
+ final_response = await poll_task_completion(agent_app, response["task"]["id"])
301
+
302
+ # Verify artifacts
303
+ assert final_response.get("artifacts"), "Expected artifacts in completed task"
304
+ artifact = final_response["artifacts"][0]
305
+ assert artifact.get("parts") and artifact["parts"][0].get("text"), (
306
+ "Expected artifact with text content"
307
+ )
308
+
309
+
310
+ @pytest.mark.asyncio
311
+ async def test_agent_card(agent_app: AgentEngineApp) -> None:
312
+ """Test agent card retrieval and validation of required A2A fields."""
313
+ response = await agent_app.handle_authenticated_agent_card(
314
+ request=build_get_request(None),
315
+ context=None,
316
+ )
317
+
318
+ # Verify core agent card fields
319
+ assert response.get("name") == "root_agent", "Expected agent name 'root_agent'"
320
+ assert response.get("protocolVersion") == "0.3.0", "Expected protocol version 0.3.0"
321
+ assert response.get("preferredTransport") == "HTTP+JSON", (
322
+ "Expected HTTP+JSON transport"
323
+ )
324
+
325
+ # Verify capabilities
326
+ capabilities = response.get("capabilities", {})
327
+ assert capabilities.get("streaming") is False, "Expected streaming disabled"
328
+
329
+ # Verify skills
330
+ skills = response.get("skills", [])
331
+ assert len(skills) > 0, "Expected at least one skill"
332
+ for skill in skills:
333
+ assert all(key in skill for key in ["id", "name", "description"]), (
334
+ "Expected id, name, and description in each skill"
335
+ )
336
+
337
+ # Verify extended card support
338
+ assert response.get("supportsAuthenticatedExtendedCard") is True, (
339
+ "Expected supportsAuthenticatedExtendedCard to be True"
340
+ )
341
+ {% elif cookiecutter.is_adk %}
240
342
 
241
- {% if cookiecutter.is_adk %}
242
343
  @pytest.mark.asyncio
243
344
  async def test_agent_stream_query(agent_app: AgentEngineApp) -> None:
244
345
  """
@@ -293,6 +394,7 @@ def test_agent_feedback(agent_app: AgentEngineApp) -> None:
293
394
 
294
395
  logging.info("All assertions passed for agent feedback test")
295
396
  {% else %}
397
+
296
398
  def test_agent_stream_query(agent_app: AgentEngineApp) -> None:
297
399
  """
298
400
  Integration test for the agent stream query functionality.
@@ -139,6 +139,149 @@ class RemoteAgentUser(WebSocketUser):
139
139
  # Set the host via command line: locust -f load_test.py --host=https://your-deployed-service.run.app
140
140
  host = "http://localhost:8000" # Default for local testing
141
141
  {%- else %}
142
+ {%- if cookiecutter.is_adk_a2a %}
143
+
144
+ import json
145
+ import logging
146
+ import os
147
+ import time
148
+
149
+ from locust import HttpUser, between, task
150
+
151
+ # Configure logging
152
+ logging.basicConfig(
153
+ level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
154
+ )
155
+ logger = logging.getLogger(__name__)
156
+
157
+ # Initialize Vertex AI and load agent config
158
+ with open("deployment_metadata.json") as f:
159
+ remote_agent_engine_id = json.load(f)["remote_agent_engine_id"]
160
+
161
+ parts = remote_agent_engine_id.split("/")
162
+ project_id = parts[1]
163
+ location = parts[3]
164
+ engine_id = parts[5]
165
+
166
+ # Convert remote agent engine ID to URLs
167
+ base_url = f"https://{location}-aiplatform.googleapis.com"
168
+ a2a_base_path = f"/v1beta1/projects/{project_id}/locations/{location}/reasoningEngines/{engine_id}/a2a/v1"
169
+
170
+ logger.info("Using remote agent engine ID: %s", remote_agent_engine_id)
171
+ logger.info("Using base URL: %s", base_url)
172
+ logger.info("Using API base path: %s", a2a_base_path)
173
+
174
+
175
+ class SendMessageUser(HttpUser):
176
+ """Simulates a user interacting with the send message API."""
177
+
178
+ wait_time = between(1, 3) # Wait 1-3 seconds between tasks
179
+ host = base_url # Set the base host URL for Locust
180
+
181
+ @task
182
+ def send_message_and_poll(self) -> None:
183
+ """Simulates a chat interaction: sends a message and polls for completion."""
184
+ headers = {"Content-Type": "application/json"}
185
+ headers["Authorization"] = f"Bearer {os.environ['_AUTH_TOKEN']}"
186
+
187
+ data = {
188
+ "message": {
189
+ "messageId": "msg-id",
190
+ "content": [{"text": "Hello! What's the weather in New York?"}],
191
+ "role": "ROLE_USER",
192
+ }
193
+ }
194
+
195
+ e2e_start_time = time.time()
196
+ with self.client.post(
197
+ f"{a2a_base_path}/message:send",
198
+ headers=headers,
199
+ json=data,
200
+ catch_response=True,
201
+ name="/v1/message:send",
202
+ ) as response:
203
+ if response.status_code != 200:
204
+ response.failure(
205
+ f"Send failed with status code: {response.status_code}"
206
+ )
207
+ return
208
+
209
+ response.success()
210
+ response_data = response.json()
211
+
212
+ # Extract task ID
213
+ try:
214
+ task_id = response_data["task"]["id"]
215
+ except (KeyError, TypeError) as e:
216
+ logger.error(f"Failed to extract task ID: {e}")
217
+ return
218
+
219
+ # Poll for task completion
220
+ max_polls = 20 # Maximum number of poll attempts
221
+ poll_interval = 0.5 # Seconds between polls
222
+ poll_count = 0
223
+
224
+ while poll_count < max_polls:
225
+ poll_count += 1
226
+ time.sleep(poll_interval)
227
+
228
+ with self.client.get(
229
+ f"{a2a_base_path}/tasks/{task_id}",
230
+ headers=headers,
231
+ catch_response=True,
232
+ name="/v1/tasks/{id}",
233
+ ) as poll_response:
234
+ if poll_response.status_code != 200:
235
+ poll_response.failure(
236
+ f"Poll failed with status code: {poll_response.status_code}"
237
+ )
238
+ return
239
+
240
+ poll_data = poll_response.json()
241
+
242
+ try:
243
+ task_state = poll_data["status"]["state"]
244
+ except (KeyError, TypeError) as e:
245
+ logger.error(f"Failed to extract task state: {e}")
246
+ poll_response.failure(f"Invalid response format: {e}")
247
+ return
248
+
249
+ # Check if task is complete
250
+ if task_state in ["TASK_STATE_COMPLETED"]:
251
+ poll_response.success()
252
+
253
+ # Measure end-to-end time
254
+ e2e_duration = (time.time() - e2e_start_time) * 1000
255
+
256
+ # Fire custom event for end-to-end metrics
257
+ self.environment.events.request.fire(
258
+ request_type="E2E",
259
+ name="message:send_and_complete",
260
+ response_time=e2e_duration,
261
+ response_length=len(json.dumps(poll_data)),
262
+ response=poll_response,
263
+ context={"poll_count": poll_count},
264
+ )
265
+ return
266
+
267
+ elif task_state in ["TASK_STATE_WORKING"]:
268
+ poll_response.success()
269
+
270
+ else:
271
+ poll_response.failure(f"Task failed with state: {task_state}")
272
+ return
273
+
274
+ # Timeout - task didn't complete in time
275
+ self.environment.events.request.fire(
276
+ request_type="TIMEOUT",
277
+ name="message:timeout",
278
+ response_time=(time.time() - e2e_start_time) * 1000,
279
+ response_length=0,
280
+ response=None,
281
+ context={"poll_count": poll_count},
282
+ exception=TimeoutError(f"Task did not complete after {max_polls} polls"),
283
+ )
284
+ {%- else %}
142
285
 
143
286
  import json
144
287
  import logging
@@ -253,3 +396,4 @@ class ChatStreamUser(HttpUser):
253
396
  else:
254
397
  response.failure(f"Unexpected status code: {response.status_code}")
255
398
  {%- endif %}
399
+ {%- endif %}
@@ -0,0 +1,138 @@
1
+ # Copyright 2025 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
+ # mypy: disable-error-code="arg-type"
16
+
17
+ """Helper functions for testing AgentEngineApp with A2A protocol."""
18
+
19
+ import asyncio
20
+ import json
21
+ from collections.abc import Awaitable, Callable
22
+ from typing import TYPE_CHECKING, Any
23
+
24
+ from starlette.requests import Request
25
+
26
+ if TYPE_CHECKING:
27
+ from {{cookiecutter.agent_directory}}.agent_engine_app import AgentEngineApp
28
+
29
+ # Test constants
30
+ POLL_MAX_ATTEMPTS = 30
31
+ POLL_INTERVAL_SECONDS = 1.0
32
+ TEST_ARTIFACTS_BUCKET = "test-artifacts-bucket"
33
+
34
+
35
+ def receive_wrapper(data: dict[str, Any] | None) -> Callable[[], Awaitable[dict]]:
36
+ """Creates a mock ASGI receive callable for testing.
37
+
38
+ Args:
39
+ data: Dictionary to encode as JSON request body
40
+
41
+ Returns:
42
+ Async callable that returns mock ASGI receive message
43
+ """
44
+
45
+ async def receive() -> dict:
46
+ byte_data = json.dumps(data).encode("utf-8")
47
+ return {"type": "http.request", "body": byte_data, "more_body": False}
48
+
49
+ return receive
50
+
51
+
52
+ def build_post_request(
53
+ data: dict[str, Any] | None = None, path_params: dict[str, str] | None = None
54
+ ) -> Request:
55
+ """Builds a mock Starlette Request object for a POST request with JSON data.
56
+
57
+ Args:
58
+ data: JSON data to include in request body
59
+ path_params: Path parameters to include in request scope
60
+
61
+ Returns:
62
+ Mock Starlette Request object
63
+ """
64
+ scope: dict[str, Any] = {
65
+ "type": "http",
66
+ "http_version": "1.1",
67
+ "headers": [(b"content-type", b"application/json")],
68
+ "app": None,
69
+ }
70
+ if path_params:
71
+ scope["path_params"] = path_params
72
+ receiver = receive_wrapper(data)
73
+ return Request(scope, receiver)
74
+
75
+
76
+ def build_get_request(path_params: dict[str, str] | None) -> Request:
77
+ """Builds a mock Starlette Request object for a GET request.
78
+
79
+ Args:
80
+ path_params: Path parameters to include in request scope
81
+
82
+ Returns:
83
+ Mock Starlette Request object
84
+ """
85
+ scope: dict[str, Any] = {
86
+ "type": "http",
87
+ "http_version": "1.1",
88
+ "query_string": b"",
89
+ "app": None,
90
+ }
91
+ if path_params:
92
+ scope["path_params"] = path_params
93
+
94
+ async def receive() -> dict:
95
+ return {"type": "http.disconnect"}
96
+
97
+ return Request(scope, receive)
98
+
99
+
100
+ async def poll_task_completion(
101
+ agent_app: "AgentEngineApp",
102
+ task_id: str,
103
+ max_attempts: int = POLL_MAX_ATTEMPTS,
104
+ interval: float = POLL_INTERVAL_SECONDS,
105
+ ) -> dict[str, Any]:
106
+ """Poll for task completion and return final response.
107
+
108
+ Args:
109
+ agent_app: The AgentEngineApp instance to poll
110
+ task_id: The task ID to poll for
111
+ max_attempts: Maximum number of polling attempts
112
+ interval: Seconds to wait between polls
113
+
114
+ Returns:
115
+ Final task response when completed
116
+
117
+ Raises:
118
+ AssertionError: If task fails or times out
119
+ """
120
+ for _ in range(max_attempts):
121
+ poll_request = build_get_request({"id": task_id})
122
+ response = await agent_app.on_get_task(
123
+ request=poll_request,
124
+ context=None,
125
+ )
126
+
127
+ task_state = response.get("status", {}).get("state", "")
128
+
129
+ if task_state == "TASK_STATE_COMPLETED":
130
+ return response
131
+ elif task_state == "TASK_STATE_FAILED":
132
+ raise AssertionError(f"Task failed: {response}")
133
+
134
+ await asyncio.sleep(interval)
135
+
136
+ raise AssertionError(
137
+ f"Task did not complete within {max_attempts * interval} seconds"
138
+ )
@@ -14,6 +14,10 @@
14
14
 
15
15
  # mypy: disable-error-code="attr-defined,arg-type"
16
16
  {%- if cookiecutter.is_adk %}
17
+ {%- if cookiecutter.is_adk_a2a %}
18
+ import asyncio
19
+ import copy
20
+ {%- endif %}
17
21
  import logging
18
22
  import os
19
23
  from typing import Any
@@ -21,18 +25,35 @@ from typing import Any
21
25
  import click
22
26
  import google.auth
23
27
  import vertexai
28
+ {%- if cookiecutter.is_adk_a2a %}
29
+ from a2a.types import AgentCapabilities, AgentCard, TransportProtocol
30
+ from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutor
31
+ from google.adk.a2a.utils.agent_card_builder import AgentCardBuilder
32
+ from google.adk.apps.app import App
33
+ {%- endif %}
24
34
  from google.adk.artifacts import GcsArtifactService
35
+ {%- if cookiecutter.is_adk_a2a %}
36
+ from google.adk.runners import Runner
37
+ from google.adk.sessions import InMemorySessionService
38
+ {%- endif %}
25
39
  from google.cloud import logging as google_cloud_logging
26
40
  from opentelemetry import trace
27
41
  from opentelemetry.sdk.trace import TracerProvider, export
28
42
  from vertexai._genai.types import AgentEngine, AgentEngineConfig{%- if cookiecutter.is_adk_live %}, AgentServerMode{%- endif %}
29
43
  {%- if cookiecutter.is_adk_live %}
30
44
  from vertexai.preview.reasoning_engines import AdkApp
45
+ {%- elif cookiecutter.is_adk_a2a %}
46
+ from vertexai.preview.reasoning_engines import A2aAgent
31
47
  {%- else %}
32
48
  from vertexai.agent_engines.templates.adk import AdkApp
33
49
  {%- endif %}
50
+ {%- if cookiecutter.is_adk_a2a %}
51
+
52
+ from {{cookiecutter.agent_directory}}.agent import app
53
+ {%- else %}
34
54
 
35
55
  from {{cookiecutter.agent_directory}}.agent import root_agent
56
+ {%- endif %}
36
57
  from {{cookiecutter.agent_directory}}.utils.deployment import (
37
58
  parse_env_vars,
38
59
  print_deployment_success,
@@ -41,9 +62,53 @@ from {{cookiecutter.agent_directory}}.utils.deployment import (
41
62
  from {{cookiecutter.agent_directory}}.utils.gcs import create_bucket_if_not_exists
42
63
  from {{cookiecutter.agent_directory}}.utils.tracing import CloudTraceLoggingSpanExporter
43
64
  from {{cookiecutter.agent_directory}}.utils.typing import Feedback
65
+ {%- if cookiecutter.is_adk_a2a %}
66
+
67
+
68
+ class AgentEngineApp(A2aAgent):
69
+ @staticmethod
70
+ async def create(
71
+ artifact_service_builder: Any = None,
72
+ session_service_builder: Any = None,
73
+ ) -> Any:
74
+ """Create an AgentEngineApp instance."""
75
+
76
+ def create_runner() -> Runner:
77
+ """Create a Runner for the AgentEngineApp."""
78
+ return Runner(
79
+ app=app,
80
+ session_service=session_service_builder()
81
+ if session_service_builder
82
+ else None,
83
+ artifact_service=artifact_service_builder()
84
+ if artifact_service_builder
85
+ else None,
86
+ )
87
+
88
+ return AgentEngineApp(
89
+ agent_executor_builder=lambda: A2aAgentExecutor(runner=create_runner()),
90
+ agent_card=await AgentEngineApp.build_agent_card(app=app),
91
+ )
92
+
93
+ @staticmethod
94
+ async def build_agent_card(app: App) -> AgentCard:
95
+ """Builds the Agent Card dynamically from the app."""
96
+ agent_card_builder = AgentCardBuilder(
97
+ agent=app.root_agent,
98
+ # Agent Engine does not support streaming yet
99
+ capabilities=AgentCapabilities(streaming=False),
100
+ rpc_url="http://localhost:9999/",
101
+ agent_version=os.getenv("AGENT_VERSION", "0.1.0"),
102
+ )
103
+ agent_card = await agent_card_builder.build()
104
+ agent_card.preferred_transport = TransportProtocol.http_json # Http Only.
105
+ agent_card.supports_authenticated_extended_card = True
106
+ return agent_card
107
+ {% else %}
44
108
 
45
109
 
46
110
  class AgentEngineApp(AdkApp):
111
+ {%- endif %}
47
112
  def set_up(self) -> None:
48
113
  """Set up logging and tracing for the agent engine app."""
49
114
  import logging
@@ -74,6 +139,16 @@ class AgentEngineApp(AdkApp):
74
139
  operations = super().register_operations()
75
140
  operations[""] = operations.get("", []) + ["register_feedback"]
76
141
  return operations
142
+ {%- if cookiecutter.is_adk_a2a %}
143
+
144
+ def clone(self) -> "AgentEngineApp":
145
+ """Returns a clone of the Agent Engine application."""
146
+ template_attributes = self._tmpl_attrs
147
+ return self.__class__(
148
+ agent_card=copy.deepcopy(self.agent_card),
149
+ agent_executor_builder=self._tmpl_attrs.get("agent_executor_builder"),
150
+ )
151
+ {%- endif %}
77
152
 
78
153
  {%- else %}
79
154
  import logging
@@ -272,7 +347,7 @@ def deploy_agent_engine_app(
272
347
  if not staging_bucket_uri:
273
348
  staging_bucket_uri = f"gs://{project}-agent-engine"
274
349
  if not artifacts_bucket_name:
275
- artifacts_bucket_name = f"gs://{project}-agent-engine"
350
+ artifacts_bucket_name = f"{project}-agent-engine"
276
351
 
277
352
  {%- if "adk" in cookiecutter.tags %}
278
353
  create_bucket_if_not_exists(
@@ -303,16 +378,25 @@ def deploy_agent_engine_app(
303
378
  # Read requirements
304
379
  with open(requirements_file) as f:
305
380
  requirements = f.read().strip().split("\n")
306
- {% if cookiecutter.is_adk %}
381
+ {%- if cookiecutter.is_adk_a2a %}
382
+ agent_engine = asyncio.run(
383
+ AgentEngineApp.create(
384
+ artifact_service_builder=lambda: GcsArtifactService(
385
+ bucket_name=artifacts_bucket_name
386
+ ),
387
+ session_service_builder=lambda: InMemorySessionService(),
388
+ )
389
+ )
390
+ {%- elif cookiecutter.is_adk %}
307
391
  agent_engine = AgentEngineApp(
308
392
  agent=root_agent,
309
393
  artifact_service_builder=lambda: GcsArtifactService(
310
394
  bucket_name=artifacts_bucket_name
311
395
  ),
312
396
  )
313
- {% else %}
397
+ {%- else %}
314
398
  agent_engine = AgentEngineApp(project_id=project)
315
- {% endif %}
399
+ {%- endif %}
316
400
  # Set worker parallelism to 1
317
401
  env_vars["NUM_WORKERS"] = "1"
318
402
 
@@ -80,6 +80,10 @@ def print_deployment_success(
80
80
  {%- if cookiecutter.is_adk %}
81
81
  {%- if cookiecutter.is_adk_live %}
82
82
  print("\n✅ Deployment successful! Run your agent with: `make playground-remote`")
83
+ {%- elif cookiecutter.is_adk_a2a %}
84
+ print(
85
+ "\n✅ Deployment successful! Test your agent: notebooks/adk_a2a_app_testing.ipynb"
86
+ )
83
87
  {%- else %}
84
88
  print(
85
89
  "\n✅ Deployment successful! Test your agent: notebooks/adk_app_testing.ipynb"
@@ -43,6 +43,9 @@ RUN uv sync --frozen
43
43
  ARG COMMIT_SHA=""
44
44
  ENV COMMIT_SHA=${COMMIT_SHA}
45
45
 
46
+ ARG AGENT_VERSION=0.0.0
47
+ ENV AGENT_VERSION=${AGENT_VERSION}
48
+
46
49
  EXPOSE 8080
47
50
 
48
51
  CMD ["uv", "run", "uvicorn", "{{cookiecutter.agent_directory}}.server:app", "--host", "0.0.0.0", "--port", "8080"]
@@ -150,6 +150,13 @@ resource "google_cloud_run_v2_service" "app" {
150
150
  containers {
151
151
  image = "us-docker.pkg.dev/cloudrun/container/hello"
152
152
 
153
+ {%- if cookiecutter.is_adk_a2a %}
154
+ env {
155
+ name = "APP_URL"
156
+ value = "https://${var.project_name}-${data.google_project.project.number}.${var.region}.run.app"
157
+ }
158
+
159
+ {%- endif %}
153
160
  resources {
154
161
  limits = {
155
162
  cpu = "4"
@@ -148,7 +148,7 @@ resource "google_alloydb_user" "db_user" {
148
148
 
149
149
  {%- endif %}
150
150
 
151
- resource "google_cloud_run_v2_service" "app_staging" {
151
+ resource "google_cloud_run_v2_service" "app_staging" {
152
152
  name = var.project_name
153
153
  location = var.region
154
154
  project = var.staging_project_id
@@ -172,6 +172,13 @@ resource "google_cloud_run_v2_service" "app_staging" {
172
172
  # Placeholder, will be replaced by the CI/CD pipeline
173
173
  image = "us-docker.pkg.dev/cloudrun/container/hello"
174
174
 
175
+ {%- if cookiecutter.is_adk_a2a %}
176
+ env {
177
+ name = "APP_URL"
178
+ value = "https://${var.project_name}-${data.google_project.project["staging"].number}.${var.region}.run.app"
179
+ }
180
+
181
+ {%- endif %}
175
182
  resources {
176
183
  limits = {
177
184
  cpu = "4"
@@ -266,7 +273,7 @@ resource "google_cloud_run_v2_service" "app_staging" {
266
273
  depends_on = [google_project_service.deploy_project_services]
267
274
  }
268
275
 
269
- resource "google_cloud_run_v2_service" "app_prod" {
276
+ resource "google_cloud_run_v2_service" "app_prod" {
270
277
  name = var.project_name
271
278
  location = var.region
272
279
  project = var.prod_project_id
@@ -290,6 +297,13 @@ resource "google_cloud_run_v2_service" "app_prod" {
290
297
  # Placeholder, will be replaced by the CI/CD pipeline
291
298
  image = "us-docker.pkg.dev/cloudrun/container/hello"
292
299
 
300
+ {%- if cookiecutter.is_adk_a2a %}
301
+ env {
302
+ name = "APP_URL"
303
+ value = "https://${var.project_name}-${data.google_project.project["prod"].number}.${var.region}.run.app"
304
+ }
305
+
306
+ {%- endif %}
293
307
  resources {
294
308
  limits = {
295
309
  cpu = "4"