aiml
SearchMy learning
Menu

Lesson 19 of 20Module 5

Deploy an ADK agent to Agent Runtime

Deploy an ADK agent with the native CLI, call Agent Runtime session and streaming methods, and distinguish endpoint mistakes from user-session errors.

Prerequisites

The course’s managed-session lesson, a Google Cloud project with deployment approval, gcloud and jq.

By the end, you can

  • Deploy an ADK agent package with the native Agent Runtime command.
  • Use ordinary and streaming runtime methods with the matching user and session.
  • Distinguish SDK session checks from application authorization.
Runtime
Python 3.12+, gcloud and jq; Google Cloud access for the recorded integration
Dependencies
google-adk[gcp] 2.12.0; google-cloud-aiplatform 2.6.0

Lab code, tests and fixtures (ZIP, 20 KB)Quiz check your understanding

Progress is saved in this browser.

adk deploy agent_engine packages an ADK agent for Agent Runtime. Unlike the previous lesson’s generated application project, this path starts from the agent directory and an ADK deployment configuration. Here we inspect the recorded deployment and use the runtime REST methods to create sessions, stream answers and add a completed conversation to Memory Bank.

Agent Runtime is the managed runtime within Google Cloud Agent Platform. The agent_engine CLI subcommand and reasoningEngines REST resource names are interface identifiers retained in this deployment path. They do not mean that the code is deploying a different application product each time a name changes.

When you need this. A service already has an ADK agent package and its own build conventions. You want the managed runtime without adopting the Agents CLI’s generated application layout. The deployment is only half the interface: the caller also needs to choose the right runtime method and keep user and session IDs associated.

Recorded example. The archived “Deploy an ADK agent to Agent Runtime” demonstration retains the earlier shipctl scenario; the current lab uses docuctl and does not claim a new model result.

One recorded deployed-agent conversationRecorded output (excerpt) · textFull output
# 1. Connection check: the methods this deployment serves
get_session, list_sessions, create_session, delete_session, async_get_session, async_list_sessions, async_create_session, async_delete_session, async_add_session_to_memory, async_search_memory, async_save_artifact, async_load_artifact, async_list_artifact_keys, async_delete_artifact, async_list_versions, async_list_artifact_versions, async_get_artifact_version, stream_query, async_stream_query, streaming_agent_run_with_events

# 2. Conversation 1, in a new session
# 3 s
session 7832467459528458240
> I run shipctl 1.x. How do I undo release r42 in production?
# 5 s
call   read_page {"page":"rollback"}
result read_page
answer In shipctl 1.x, use the `shipctl release revert` command:

```bash
shipctl release revert r42 --env production
```

*(Or simply `shipctl release revert r42` if production is the default environment).*

Note that database migrations are not rolled back automatically—if r42 ran a migration, you must deploy a new migration to reverse it.
data flowreturnartifact hand-offfailureagent.pyADK deploy configadk deploy agent_enginesourcefilesAdkAppAgent RuntimeRunner + LlmAgentdeployquery.sh:queryordinary methods:streamQueryasync_stream_queryJSONstreamwrong method surfacerecorded failed requestVertexAiSessionServiceeventsdata flowreturnartifact hand-offfailureagent.pyADK deploy configadk deploy agent_enginesource filesAdkAppAgent Runtime / Runnerdeployquery.sh:queryordinary:streamQuerystreamingJSONstreamstream method on queryrequest failswrong surfaceVertexAiSessionServiceevents
ADK packages the agent and deployment configuration for Agent Runtime. The caller uses query for ordinary methods and streamQuery for the streaming agent method. Sending a streaming method to query failed in the recorded run. User/session association is a service check; the caller still supplies user_id and must authorize it.

Lab setup. Download the complete lab ZIP, extract it and open a terminal in adk-agent-runtime/. Keep its code, tests and fixtures together. Create the pinned environment for local checks:

Create the lab environmentTerminal · bash
python3.12 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements-lock.txt

The saved cloud results can be read without provisioning services. The cloud steps require the project access listed in the prerequisites.

1. Package the same docs assistant for the managed runtime

The agent remains the docuctl docs assistant. Its model uses a global endpoint; the runtime and its managed data remain in the selected region. The configuration file sets resource bounds and the Memory Bank topic from the managed-services lesson:

docs_helper/.agent_engine_config.jsonSource · jsonDownload
{
  "display_name": "docs-helper",
  "description": "docuctl docs assistant",
  "labels": {"aiml-lesson": "deploy-an-adk-agent-to-agent-runtime"},
  "min_instances": 0,
  "max_instances": 1,
  "resource_limits": {"cpu": "1", "memory": "1Gi"},
  "context_spec": {"memory_bank_config": {
    "ttl_config": {"default_ttl": "86400s"},
    "customization_configs": [{"memory_topics": [
      {"managed_memory_topic": {"managed_topic_enum": "USER_PREFERENCES"}},
      {"custom_memory_topic": {"label": "tool_versions",
        "description": "Which version of a CLI, SDK or service the developer runs, for example 'runs docuctl 1.x'."}}
    ]}]
  }}
}

This is a deployment configuration, not a measured capacity result. Check the official service limits and your workload before choosing different values. The lab includes pinned dependencies and the current docuctl agent source; the saved output retains the original shipctl run.

2. Deploy with the native ADK command

Deploy the agent directory with adk deploy agent_engine. ADK copies the source, reads .agent_engine_config.json, generates requirements.txt and creates the Agent Runtime instance:

Deploy the agent to Agent RuntimeTerminal · bash
adk deploy agent_engine --project $GOOGLE_CLOUD_PROJECT \
  --region us-central1 --display_name docs-helper docs_helper
Recorded adk deploy agent_engine outputRecorded output (excerpt) · textFull output
$ adk deploy agent_engine --project $GOOGLE_CLOUD_PROJECT --region us-central1 --display_name docs-helper docs_helper
Copying agent source code...
Copying agent source code complete.
Resolving files and dependencies...
Reading agent platform config from ./docs_helper/.agent_engine_config.json
Overriding display_name in agent platform config with docs-helper
Creating ./docs_helper_tmp20261021_213335/agents/docs_helper/requirements.txt...
Using google-adk[a2a]==2.12.0 in requirements
Created ./docs_helper_tmp20261021_213335/agents/docs_helper/requirements.txt
Initializing Agent Platform client...
Agent Platform client initialized with project and region.
Deploying to Agent Platform...
Created a new instance: projects/PROJECT_NUMBER/locations/us-central1/reasoningEngines/60776879614853120
Creating Dockerfile...
Dockerfile created at ./docs_helper_tmp20261021_213335/Dockerfile.
Deployed to Agent Platform: projects/PROJECT_NUMBER/locations/us-central1/reasoningEngines/60776879614853120

The installed command still uses agent_engine although the product is called Agent Runtime. --agent_engine_id selects an existing instance for update; omitting it creates a new one. The installed help marks --staging_bucket deprecated and unused. These distinctions matter when adapting an older deployment script.

The recording used an existing approved project. Inspect its deployment output before adapting the command to a different project or region.

3. Call session methods and stream an agent turn

query.sh obtains an access token from gcloud, calls ordinary methods through :query, and sends the streaming method to :streamQuery?alt=sse. It prints the available methods before using them, so an SDK or deployment change is visible to the caller.

query.shSource · bashDownload
#!/usr/bin/env bash
# Talk to the deployed docs assistant over the Agent Runtime REST API.
# Usage: PROJECT_ID=... RESOURCE_ID=... ./query.sh
set -euo pipefail
LOCATION_ID=${LOCATION_ID:-us-central1}
URL="https://${LOCATION_ID}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${LOCATION_ID}/reasoningEngines/${RESOURCE_ID}"
AUTH="Authorization: Bearer $(gcloud auth print-access-token)"

query() {  # query CLASS_METHOD INPUT_JSON: one JSON response
  curl -s -H "$AUTH" -H "Content-Type: application/json" "$URL:query" \
    -d "{\"class_method\": \"$1\", \"input\": $2}"
}
stream() {  # stream USER SESSION MESSAGE: one JSON event per line
  curl -s -H "$AUTH" -H "Content-Type: application/json" "$URL:streamQuery?alt=sse" \
    -d "$(jq -n --arg u "$1" --arg s "$2" --arg m "$3" \
      '{class_method: "async_stream_query", input: {user_id: $u, session_id: $s, message: $m}}')"
}
show() {  # print tool calls and the final text of a stream
  jq -r '.content.parts[]? | if .function_call then "call   \(.function_call.name) \(.function_call.args|tojson)"
    elif .function_response then "result \(.function_response.name)"
    elif (.text and (.thought|not)) then "answer \(.text)" else empty end'
}
timed() { local s=$(date +%s); "$@"; echo "# $(( $(date +%s) - s )) s" >&2; }

echo "# 1. Connection check: the methods this deployment serves"
curl -s -H "$AUTH" "$URL" | jq -r '[.spec.classMethods[].name] | join(", ")'

echo; echo "# 2. Conversation 1, in a new session"
S1=$(timed query async_create_session '{"user_id": "dana"}' | jq -r '.output.id')
echo "session $S1"
echo "> I run docuctl 1.x. How do I undo release r42 in production?"
timed stream dana "$S1" "I run docuctl 1.x. How do I undo release r42 in production?" | show

echo; echo "# 3. Add the finished session to Memory Bank"
SESSION=$(query async_get_session "{\"user_id\": \"dana\", \"session_id\": \"$S1\"}" | jq -c '.output')
query async_add_session_to_memory "{\"session\": $SESSION}" | jq -c '.'
for i in $(seq 1 40); do  # memories are extracted in the background
  FOUND=$(query async_search_memory '{"user_id": "dana", "query": "docuctl version"}' | jq -c '[.output.memories[]?.content.parts[0].text]')
  [ "$FOUND" != "[]" ] && break; sleep 5
done
echo "memories after about $(( i * 5 )) s: $FOUND"

echo; echo "# 4. Conversation 2, in another new session"
S2=$(query async_create_session '{"user_id": "dana"}' | jq -r '.output.id')
echo "> How do I undo the last production release?"
timed stream dana "$S2" "How do I undo the last production release?" | show

The wrapper’s input is {class_method, input}. async_create_session returns an ID; async_stream_query needs that ID, the user ID and a message. A stream is not an ordinary JSON return value, which is why the two surfaces are distinct. Keep request construction and response parsing explicit when diagnosing a failed call.

A streaming method on the ordinary query surface. Select the endpoint from the runtime’s registered streaming operations.

A missing user session. The SDK raises when its session service finds no matching conversation. The local test uses the in-memory service; the saved cloud log is the separate remote observation.

Recorded endpoint and user-session failuresRecorded output (excerpt) · textFull output
# 1. A streaming method sent to :query
{"code":400,"message":"Reasoning Engine Execution failed.\nPlease refer to our documentation (https://cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/troubleshooting/use) for checking logs and other troubleshooting tips.\nError Details: Internal Server Error","status":"FAILED_PRECONDITION"}

# 2. Dana's session id sent with another user id


# The service log for both requests
$ gcloud logging read "resource.type=\"aiplatform.googleapis.com/ReasoningEngine\" AND resource.labels.reasoning_engine_id=\"$RESOURCE_ID\" AND severity>=ERROR" --project $PROJECT_ID --freshness=15m
ValueError: Session 1529679781023449088 does not belong to user lee.
ValueError: [TypeError("'async_generator' object is not iterable"), TypeError('vars() argument must have __dict__ attribute')]
TypeError: vars() argument must have __dict__ attribute
TypeError: 'async_generator' object is not iterable
ValueError: [TypeError("'async_generator' object is not iterable"), TypeError('vars() argument must have __dict__ attribute')]
TypeError: vars() argument must have __dict__ attribute

Sending async_stream_query to :query produced a failed request and a service log about an async generator. The fix is the streaming endpoint, not a retry of the same JSON request. A request with another user ID and the session ID also failed in the service log.

That user-ID check prevents accidental mixing of sessions; it is not an identity boundary for your application. An authorized runtime caller supplies the user_id field. Your caller must map its authenticated subject to that field rather than accepting an arbitrary value from a client.

Offline SDK and configuration checksRecorded output · textDownload
test_a_missing_user_session_does_not_return_another_users_history (test_runtime.RuntimeConfiguration.test_a_missing_user_session_does_not_return_another_users_history) ... ok
test_deployment_resource_limits_are_explicit (test_runtime.RuntimeConfiguration.test_deployment_resource_limits_are_explicit) ... ok
test_model_and_regional_services_can_use_different_locations (test_runtime.RuntimeConfiguration.test_model_and_regional_services_can_use_different_locations) ... ok
test_read_page_rejects_a_path_outside_the_docs (test_runtime.RuntimeConfiguration.test_read_page_rejects_a_path_outside_the_docs) ... ok
test_streaming_method_is_registered_on_the_stream_surface (test_runtime.RuntimeConfiguration.test_streaming_method_is_registered_on_the_stream_surface) ... ok

----------------------------------------------------------------------
Ran 5 tests in 0.339s

OK

4. Add the completed conversation to Memory Bank

After the first turn, the script retrieves the session and calls async_add_session_to_memory. It polls async_search_memory before opening another session. The second answer used the recalled CLI version, as the saved output shows:

Recorded memory ingestion and new conversationRecorded output (excerpt) · textFull output
 Add the finished session to Memory Bank
{"output":null}
memories after about 25 s: ["I run shipctl 1.x."]

# 4. Conversation 2, in another new session
> How do I undo the last production release?
# 6 s
call   load_memory {"query":"shipctl major version"}
call   read_page {"page":"rollback"}
result load_memory
result read_page
answer Since you are on **shipctl 1.x**, run:

```bash
shipctl release revert <release-id>
```

Specify the release identifier you want to revert back to (you can find previous releases with `shipctl releases`).

Note that database migrations are not automatically rolled back—if the release ran a migration, you'll need to deploy a reverse migration.

This is the same managed-memory mechanism as the earlier lesson; the difference is that the managed runtime exposes it through registered methods. An ingestion response is not a searchable-memory acknowledgement. The polling loop is bounded, and an empty result should be reported to the caller rather than interpreted as remembered data.

Remove only the lesson deployment

The recorded instance was deleted with force=true, which also removed its sessions and memories. For your own run, inspect the project, region, resource ID and lesson label before deletion, and wait for the delete operation to finish. A deployment name is not enough to identify an owned resource. The saved delete output records the original teardown.

What’s next

Deploy an ADK agent to Cloud Run uses the API server’s HTTP interface behind Cloud Run IAM and makes the session-store choice explicit.

Read the unchanged docs assistant
docs_helper/agent.pySource · pythonDownload
"""The docs assistant on Gemini Enterprise Agent Platform: Gemini through Vertex, recall through Memory Bank."""
import os
from pathlib import Path

from google.adk import Agent
from google.adk.models.google_llm import Gemini
from google.adk.tools import load_memory
from google.genai import types

DOCS = Path(__file__).resolve().parent / "docs"
PAGES = sorted(p.stem for p in DOCS.glob("*.md"))
# The recorded run uses global for this model. Sessions and Memory Bank stay regional
# (GOOGLE_CLOUD_LOCATION), so the model client gets its own location.
MODEL_LOCATION = os.environ.get("DOCS_MODEL_LOCATION", "global")


def read_page(page: str) -> dict:
    """Read one page of the docuctl documentation: config, deploy, rollback or tokens."""
    if page not in PAGES:
        return {"error": f"No page {page!r}. Pages: {', '.join(PAGES)}."}
    return {"page": page, "text": (DOCS / f"{page}.md").read_text(encoding="utf-8")}


INSTRUCTION = """You answer questions from developers about docuctl, the team's deploy CLI.
Read the relevant page with read_page before you answer. Answers depend on the developer's
docuctl major version (1.x or 2.x). If the developer has not said it in this conversation,
call load_memory to look for it in earlier conversations before you ask. Keep the answer
under 80 words."""

root_agent = Agent(
    name="docs_helper",
    model=Gemini(model="gemini-3.8-flash", client_kwargs={"location": MODEL_LOCATION}),
    instruction=INSTRUCTION,
    tools=[read_page, load_memory],
    generate_content_config=types.GenerateContentConfig(
        temperature=0.2, thinking_config=types.ThinkingConfig(thinking_level="low")),
)

Check your understanding

A registered async_stream_query method fails when sent to :query. What should you correct first?

Choose one answer, then check your reasoning.

Read the explanation

The script routes streaming operations through :streamQuery?alt=sse. An async stream is not an ordinary JSON query result.

References

Official documentation and installed ADK / Google Cloud AI Platform APIs checked October 22, 2026.

Written by Arjun Prabhulal.

Module 5

Deploy agents on Google Cloud