DEV Community

Evan Lin for Google Developer Experts

Posted on Originally published at evanlin.com on

[Agent Platform in Practice (2)] From Monolithic Cloud Run to Google Cloud Agent Runtime: Refactoring LINE AI Business Card King

LINE Card Assistant + Google Cloud Agent Platform Architecture

Recap

In the previous article, "[Agent Platform in Practice (1)] Building a LINE AI Business Card Assistant with Go and Google ADK 2.0 Graph Workflow", we used Golang (google.golang.org/adk/v2/workflow) and the LINE Messaging API SDK v8 to build line-card-assistant (LINE AI Business Card Assistant) from scratch. We successfully implemented and deployed the four core capabilities of ADK 2.0 Graph Workflow—Deterministic Router, AddFanOut / AddFanIn for parallel background investigation and aggregation, and NewRequestInputEvent for Human-in-the-Loop (HITL) pause and loop correction—to Google Cloud Run.

However, as we evolve this monolithic LINE Bot into a Production-grade Agent Platform, we immediately encounter several architectural questions:

  1. Where is the cross-instance HITL Session state stored? In the first version, we used session.InMemoryService() with local SQLite. When Cloud Run switches container instances due to auto-scaling or redeployment, the HITL pause state (Checkpoint) waiting for the user to click "✅ Confirm Save" disappears due to memory reset.
  2. How can this business card Agent be called by other Agents (Agent-to-Agent, A2A)? If the company has another "CRM Sales Assistant Agent" or "Calendar Scheduling Agent" that wants to query contacts in our Business Card Assistant or ask it to write an icebreaker email via a standard A2A protocol, a monolithic server tied to the LINE Webhook /webhook entry point cannot directly provide a standard Agent interface.
  3. How to systematically evaluate and observe the entire Graph? After changing a line of Prompt or adjusting a node, how do we ensure that OCR accuracy, Fan-Out parallel tool call trajectories (Tool Trajectory), and search quality haven't degraded?

To bridge the gap between "writing an Agent prototype" and "operating an enterprise-grade Agent," Google officially launched a new command-line and skill toolkit—agents-cli (Google Agents CLI), serving as the unified development entry point for the Google Cloud Agent Platform (now integrated as Gemini Enterprise Agent Platform).

In this second episode of the series, we will dive deep into the agents-cli official development guide, answer frequently asked questions about Agent Runtime scaling (Scale-to-Zero) and billing mechanisms, and walk you through how we completed the Phase 1 architecture upgrade (PR #10): refactoring the project into a dual-channel architecture that complies with adk_go + agent_runtime specifications without breaking the original LINE Bot functionality!


1. In-depth Analysis of Google Cloud Agent Platform and the agents-cli Official Guide

Before diving into the code, let's clarify Google's latest technical landscape for the Agent Platform through two core documents: "Getting Started - agents-cli" and "The Lifecycle - agents-cli".

1. Product Terminology Comparison: What are Agent Platform and Agent Runtime?

If you've followed Google Cloud's AI product line over the past year, you might have heard of Vertex AI Agent Engine, Reasoning Engine, Agent Platform, or Gemini Enterprise Agent Platform (GEAP). In the latest agents-cli v1.10+ specifications, the official documentation has clearly converged these terms and underlying resources:

Common Developer Terms Google Cloud Official Positioning & Underlying Resources agents-cli Corresponding Parameter
Agent Platform / Gemini Platform Google Cloud's overall platform for hosting enterprise-grade AI Agents (covering Runtime, Sessions, Memory Bank, Agent Gateway, Agent Registry, and Eval) Unified operation via agents-cli
Agent Runtime / Agent Engine A fully managed container execution environment designed specifically for AI Agents. Underlying GCP resource path: projects/{project}/locations/{location}/reasoningEngines/{id} --deployment-target agent_runtime
Agent Platform Sessions A cloud-managed conversation and state persistence service (ADK automatically binds VertexAiEngineSessionService) --session-type agent_platform_sessions
Gemini Enterprise / Agent Registry An enterprise Agent publishing and directory center, allowing deployed Agents to be discovered and called directly in the Gemini Enterprise interface or via A2A agents-cli publish gemini

2. The Seven-Stage Development Lifecycle of agents-cli

In the past, we were used to a "write code → go test → gcloud run deploy" workflow. However, in "The Lifecycle" document, agents-cli defines a Seven-Stage Quality Flywheel that integrates Coding Agent Skills (supporting Antigravity CLI, Gemini CLI, Claude Code, etc.):

flowchart LR
    P0["Phase 0-1\nUnderstand & Recipes\n(.agents-cli-spec.md)"] --> P2["Phase 2\nScaffold\n(create / enhance)"]
    P2 --> P3["Phase 3\nBuild & Smoke Test\n(run / playground)"]
    P3 --> P4["Phase 4\nEvaluate\n(eval run Quality Flywheel)"]
    P4 -->|Iterative Correction| P3
    P4 --> P5["Phase 5\nDeploy\n(agent_runtime / cloud_run)"]
    P5 --> P6["Phase 6-7\nPublish & Observe\n(Cloud Trace / BigQuery)"]
Enter fullscreen mode Exit fullscreen mode
  1. Phase 0 & 1 — Understand & Study Recipes: Before writing a single line of code, document the Agent's core goals, external tools, security boundaries, and deployment targets in the project root's .agents-cli-spec.md as the Single Source of Truth for the entire development cycle.
  2. Phase 2 — Scaffold: Use agents-cli scaffold create or run agents-cli scaffold enhance . on an existing project to automatically generate the directory structure, configuration files (agents-cli-manifest.yaml), A2A endpoints, Session switchers, and OpenTelemetry observation modules that comply with official standards.
    • Supports two major official built-in templates: adk (Python) and adk_go (Golang)!
  3. Phase 3 — Build & Playground:
    • Go projects only need to focus on implementing and exporting NewRootAgent(ctx) in app/agent.go.
    • During development, you can use agents-cli run "your test question" for a quick terminal smoke test, or run agents-cli playground (or make playground) to open a browser-based visual interactive interface for multi-turn conversation testing.
  4. Phase 4 — Evaluate (The core of the Quality Flywheel): The official guide emphasizes: "Unit Test (go test) measures code contracts and type correctness; while agents-cli eval measures Agent behavior quality (Tool Trajectory, response quality, and safety)."
  5. Phase 5 — Deploy: Use agents-cli deploy to package and deploy the project to agent_runtime, cloud_run, or gke, and automatically record the deployed resource ID in deployment_metadata.json.
  6. Phase 6 & 7 — Publish & Observe: Register to the enterprise directory via agents-cli publish gemini, and combine with OpenTelemetry to automatically export all Graph node latencies, LLM Prompt/Response, and Token consumption to Cloud Trace, Cloud Logging, and BigQuery Agent Analytics.

3. Agent Runtime vs. Cloud Run vs. GKE: Which Deployment Target Should I Choose?

agents-cli supports three containerized deployment targets (all of which build based on the Dockerfile in the project root, making Go a first-class citizen on all three platforms). We have summarized the comparison based on the official Deployment Decision Matrix:

Evaluation Dimension Agent Runtime (agent_runtime) Cloud Run (cloud_run) GKE (gke)
Infrastructure Management Fully managed (built for Agents), automatically built from Dockerfile and mounted to reasoningEngines Containerized serverless hosting, requires manual configuration of network and service parameters GKE Autopilot, provides full K8s control but with the highest operational complexity
Session State Management Native built-in Agent Platform Sessions (automatically injects GOOGLE_CLOUD_AGENT_ENGINE_ID at runtime and enables cloud persistent Sessions) In-Memory by default, requires additional Cloud SQL setup or manual connection to managed Sessions In-Memory by default, requires additional Cloud SQL setup or managed Sessions
API & Access Interface Built-in :query / :streamQuery, Console Playground, and /api/{container_path} HTTP Passthrough and A2A Native public or IAM-protected HTTPS URL, best for receiving external Webhooks (like LINE / Slack) Custom K8s Ingress / Gateway
Enterprise Integration Native support for Managed OAuth 2.0 User Consent, Agent Identity, Agent Gateway, and PSC-I Private Networking Supports VPC Egress, IAP, but does not support managed Agent OAuth flows Full K8s network control
Billing Model Billed based on actual vCPU-hours + Memory-hours used (no compute fees when scaled to 0) Billed based on Request duration or Instance seconds Billed based on K8s Node Pod resources

4. Key Clarification: Will Instances Moved to Agent Runtime Scale to Zero?

When planning to migrate line-card-assistant to Agent Runtime, a common concern is:

"According to agents-cli specifications, will instances moved to Agent Runtime never scale to 0 and constantly burn money?"

The answer is: Agent Runtime supports Scale to Zero, and agents-cli adopts a clever dual-mode design: "Scale to 0 by default for development, and pre-warm 1 instance by default for production!"

According to the agents-cli official deployment specifications (Sizing a Deployment):

  1. When using agents-cli deploy directly (Default Scale to Zero):
    • The CLI's default resource allocation is identical to Cloud Run: --cpu 1, --memory 4Gi, --concurrency 8, --min-instances 0, --max-instances 10.
    • This means when there is no traffic to the Agent Runtime, the number of instances will automatically scale down to 0. Since Agent Runtime is billed based on actual vCPU-hours and Memory-hours, no compute fees are generated when idling at 0!
  2. When using Terraform (service.tf) for production deployment (Default min_instances = 1):
    • When you generate Terraform IaC (deployment/terraform/single-project/service.tf) for a production environment, the template defaults min_instances to 1 to avoid cold starts for the first user.
    • If you want to save money in production and can tolerate a few seconds of cold start, you can specify agents-cli deploy --min-instances 0 during deployment or change min_instances back to 0 in service.tf.

💡 Why is a "Dual-layer Decoupled Architecture" best for a LINE Bot with Agent Runtime?

Understanding the characteristics of Agent Runtime, you'll find that splitting the system into two layers is a perfect match:

  • Outer Layer: LINE Webhook Gateway (Cloud Run, min-instances=0): Since the LINE server sends Webhooks with only an X-Line-Signature HMAC signature, it cannot carry a Google Cloud IAM OAuth Token to call an IAM-protected Agent Runtime API. Use a lightweight Go Cloud Run service dedicated to "verifying the signature within 1 second, returning HTTP 200, and triggering the LINE loading animation." It scales to 0 when no one is sending business cards.
  • Inner Layer: Core Brain (Agent Runtime, min-instances=0): Focuses on running the ADK 2.0 Graph Workflow, managing cloud persistent VertexAiEngineSessionService (ensuring HITL pause states aren't lost across containers!), and simultaneously exposing A2A (/.well-known/agent-card.json) and the Agent Engine Playground to the enterprise!

2. Planning the Migration Blueprint: Creating a 5-Phase Roadmap Using GitHub CLI

To complete this architectural evolution methodically, we used the gh CLI in the GitHub Repository (kkdai/line-card-assistant) to establish 5 Milestones (Phases), 8 implementation Issues, and an overview tracking list Roadmap Issue #9:


3. Phase 1 Hands-on Breakdown: Transforming an Existing Go Project into an adk_go + agent_runtime Dual-Channel Architecture

In Phase 1 (corresponding to the merged PR #10), our goal was to: retain the original cmd/server/main.go independent LINE Webhook server while introducing the standard agents-cli (adk_go + agent_runtime) architecture in the project root, allowing the same codebase to possess both superpowers!

Let's look at the key files changed in this upgrade and the new features they bring:

line-card-assistant/
├── .agents-cli-spec.md # [New] Phase 0 Architecture Spec (Single Source of Truth)
├── agents-cli-manifest.yaml # [New] agents-cli v1.10.0 project config (adk_go + agent_runtime)
├── deployment_metadata.json # [New] Records remote Agent Runtime resource ID and A2A status
├── main.go # [New] Agent Runtime / ADK WebUI / A2A standard HTTP entry point
├── apploader.go # [New] Loader supporting /ui/?app=app and rootAgent.Name() dual aliases
├── appurl.go # [New] Automatically calculates Cloud Run / Agent Runtime public URL (for A2A Card)
├── observability.go # [New] OpenTelemetry Cloud Trace / Logging / Metrics auto-mounting
├── app/
│ ├── agent.go # [New] Standard export interface: NewRootAgent(ctx) returns 12-node Graph Workflow
│ └── agent_test.go # [New] Verifies Root Agent execution and search output directly via ADK Runner
├── appinfo/
│ └── appinfo.go # [New] GET /app-info endpoint, auto-parses Root Workflow and embedded CompanyEnrichAgent
├── sessions/
│ └── sessions.go # [New] Auto-switches between VertexAiEngineSessionService and local InMemoryService
├── cmd/server/main.go # [Retained] Original independent LINE Bot Webhook server
└── internal/workflow/ # [Enhanced] Graph Workflow nodes upgraded to support dual-channel input and streaming Content output
Enter fullscreen mode Exit fullscreen mode

1. Creating agents-cli-manifest.yaml and .agents-cli-spec.md

To make agents-cli recognize a Go project, the key is the agents-cli-manifest.yaml in the root directory:

# Managed by agents-cli. Do not edit manually.
acli_version: "1.10.0"
project_name: "line-card-assistant"
root_agent_name: "line_card_assistant"
base_template: "adk_go"
language: "go"
framework: "google-adk"
deployment_target: "agent_runtime"
agent_directory: "app"
region: "asia-east1"
agent_guidance_filename: "GEMINI.md"
is_a2a: true
prototype: true
generated_at: "2026-10-17T16:12:00Z"
Enter fullscreen mode Exit fullscreen mode

After configuration, running agents-cli info in the project root allows the CLI to immediately identify this as a Go project that supports A2A and targets deployment to the asia-east1 Agent Runtime:

$ agents-cli info
CLI version: 1.10.0
Project root: /Users/evanlin/Documents/line-card-assistant
Project name: line-card-assistant
Root agent name: line_card_assistant
Language: go
Deployment target: agent_runtime
Agent directory: app
Region: asia-east1
A2A: yes
Enter fullscreen mode Exit fullscreen mode

2. Encapsulating the Standard Root Agent Entry Point: app/agent.go

In the adk_go template specification, all Agent logic should be exported via NewRootAgent(ctx context.Context) (agent.Agent, error) under the app package.

Since we already used ADK 2.0's workflowagent.New to encapsulate the 12-node Graph Workflow into a standard agent.Agent in the first article, in app/agent.go, we only need to initialize the underlying CardStore (SQLite or Memory) and model.LLM (auto-detecting Vertex AI ADC or Gemini API Key), then call workflow.NewCardAssistantAgent for a seamless connection!

package app

import (
    "context"
    "log/slog"
    "os"
    "strings"

    "github.com/kkdai/line-card-assistant/internal/config"
    "github.com/kkdai/line-card-assistant/internal/store"
    "github.com/kkdai/line-card-assistant/internal/workflow"
    "google.golang.org/adk/v2/agent"
    "google.golang.org/adk/v2/model"
)

const RootAgentName = workflow.WorkflowName // "line_card_assistant"

// NewRootAgent constructs the root ADK v2 Graph Workflow agent for Agent Runtime,
// ADK WebUI playground, A2A server, and evaluation CLI.
func NewRootAgent(ctx context.Context) (agent.Agent, error) {
    cfg := config.Load()

    var cardStore store.CardStore
    if strings.TrimSpace(cfg.SQLitePath) != "" {
        sqliteStore, err := store.NewSQLiteStore(cfg.SQLitePath)
        if err != nil {
            slog.Warn("failed to open SQLite store, falling back to in-memory store", "err", err)
            cardStore = store.NewMemoryStore()
        } else {
            cardStore = sqliteStore
        }
    } else {
        cardStore = store.NewMemoryStore()
    }

    var llm model.LLM
    if cfg.UseVertexAI || cfg.GoogleAPIKey != "" || os.Getenv("GOOGLE_CLOUD_AGENT_ENGINE_ID") != "" {
        realLLM, err := workflow.NewGeminiModel(ctx, cfg.GeminiModel, cfg.GoogleAPIKey, cfg.UseVertexAI, cfg.GoogleCloudProject, cfg.GoogleCloudLocation)
        if err != nil {
            slog.Warn("failed to initialize Gemini model, falling back to mock LLM", "err", err)
            llm = workflow.NewMockLLM()
        } else {
            llm = realLLM
        }
    } else {
        llm = workflow.NewMockLLM()
    }

    return NewRootAgentWithDeps(cardStore, llm)
}

func NewRootAgentWithDeps(cardStore store.CardStore, llm model.LLM) (agent.Agent, error) {
    return workflow.NewCardAssistantAgent(cardStore, llm)
}
Enter fullscreen mode Exit fullscreen mode

Note a clever detail here: when deployed in Agent Runtime, the system automatically injects the GOOGLE_CLOUD_AGENT_ENGINE_ID environment variable. When running unit tests (go test ./...) without an API Key, it automatically falls back to workflow.NewMockLLM(), ensuring offline tests remain 100% fast and deterministic!


3. Agent Runtime Standard HTTP Server and A2A Endpoints (Root main.go)

Next, let's look at the new main.go in the project root. When the container is deployed to Agent Runtime (or executed locally via make playground), this entry point mounts five standard services:

  1. OpenTelemetry Observability (setupObservability): Automatically exports Traces, Logs, and Metrics to Google Cloud Trace and Cloud Logging.
  2. Session Service Auto-switching (sessions.SessionService()): In sessions/sessions.go, if VERTEX_AI_SEARCH_ENGINE_ID (Agent Engine ID) is detected, it automatically enables the cloud persistent NewVertexAiEngineSessionService; for local development, it seamlessly uses session.InMemoryService().
  3. ADK REST API and WebUI (/api/ and /ui/): Directly starts the ADK built-in streaming conversation API and web debugging interface via api.NewLauncher() and webui.NewLauncher().
  4. A2A (Agent-to-Agent) Protocol Server: Automatically exposes this Agent's capability description card (AgentCard) at /.well-known/agent-card.json and provides a JSON-RPC call endpoint at /a2a/app!
  5. Agent Structure Introspection Endpoint (GET /app-info): Analyzes the entire Agent Tree via appinfo/appinfo.go. It not only lists the Root Graph Workflow but also registers the CompanyEnrichAgent (gemini-2.5-flash)—which is embedded in CompanyEnrichNode and equipped with geminitool.GoogleSearch{}—into the /app-info response. This allows agents-cli eval and the Google Cloud Console Playground to clearly see all sub-agents and tools within the system!

4. Key Transformation: Enabling Graph Workflow to Support Both "LINE Webhook" and "ADK WebUI / A2A" Dual Channels!

In the first version, our RouterNode assumed all input came from the LINE Webhook Handler, which pre-filled StateKeyInputType or StateKeyImageBytes into the Session State. Furthermore, terminal nodes (like FormatCarouselNode, BuildPreviewNode, SaveCardNode) only wrote results to StateKeyReplyText and StateKeyFlexCarouselJSON for the LINE Bot to retrieve and call ReplyMessage.

However, when we open the ADK WebUI (make playground), the Agent Engine Console Playground, or talk directly to the Root Agent via agents-cli run "Find someone who does AI for me", the input follows the standard ADK ctx.UserContent() (containing text Part.Text or uploaded image Part.InlineData), and the output is read from ev.Content in the event stream!

Therefore, we made two fully backward-compatible enhancements in internal/workflow/nodes_ingest.go and internal/workflow/nodes_search.go:

(1) RouterNode Automatically Parses Images and Text from ctx.UserContent()

// When uploading a business card image directly from ADK WebUI / A2A / Agent Engine Playground,
// automatically extract InlineData.Data from ctx.UserContent().Parts and direct to RouteIngest!
if len(getBytesState(ctx, StateKeyImageBytes)) == 0 && ctx.UserContent() != nil {
    for _, part := range ctx.UserContent().Parts {
        if part != nil && part.InlineData != nil && len(part.InlineData.Data) > 0 {
            setState(ctx, ev, StateKeyImageBytes, part.InlineData.Data)
            if getStringState(ctx, StateKeyInputType) == "" {
                setState(ctx, ev, StateKeyInputType, RouteIngest)
            }
            break
        }
    }
}

// If it's text input and the input parameter is empty, automatically extract query text or commands from ctx.UserContent().Parts
if inputType == "" {
    sTrim, _ := input.(string)
    sTrim = strings.TrimSpace(sTrim)
    if sTrim == "" && ctx.UserContent() != nil {
        for _, part := range ctx.UserContent().Parts {
            if part != nil && strings.TrimSpace(part.Text) != "" {
                sTrim = strings.TrimSpace(part.Text)
                break
            }
        }
    }
    // Automatically determine if it's searching for a card (RouteSearch) or generating an icebreaker (action=draft_email&card_id=...)
    ...
}
Enter fullscreen mode Exit fullscreen mode

(2) Terminal Nodes Synchronously Write to ev.Content

In BuildPreviewNode, SaveCardNode, FormatCarouselNode, and EmailDraftNode, in addition to writing to StateDelta for the LINE Bot, we added a line:

ev.Content = genai.NewContentFromText(flexJSON, genai.RoleModel)
Enter fullscreen mode Exit fullscreen mode

This way:

  • Sending or searching cards from LINE: The LINE Webhook still gets the Flex JSON from StateDelta to render the beautiful LINE card.
  • Testing from ADK WebUI (make playground) or Agent Engine Playground: Without needing a LINE account, you can upload a business card image or type "Find someone I met at Cloud Summit who does AI" directly in the browser. You'll see the full execution trajectory (Graph Node transitions) and the output Flex JSON / icebreaker draft directly in the conversation window!

We also added a TestRootAgentDirectSearchExecution unit test in app/agent_test.go, using the standard ADK runner.Runner to send a genai.Content query to app.NewRootAgent, verifying that the business card Carousel JSON can be obtained directly from the Event Stream without going through the LINE Engine!


5. Multi-binary Dockerfile: One Image for Both Agent Runtime and LINE Webhook

Finally, to allow the same container image to be deployed to Agent Runtime via agents-cli deploy and to Cloud Run as an independent LINE Bot server, we updated the Dockerfile to compile two static executables in a multi-stage build:

# Build the Agent Runtime / ADK WebUI / A2A entrypoint binary (root main.go)
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o /out/agent .

# Also build the standalone LINE Bot Webhook server binary (cmd/server/main.go)
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o /out/line-card-assistant ./cmd/server
Enter fullscreen mode Exit fullscreen mode
  • The default ENTRYPOINT ["/agent"] starts the Agent Runtime standard server (listening on 0.0.0.0:8080).
  • If it needs to run as a monolithic LINE Webhook Server on Cloud Run, simply specify the Container Command as /bin/line-card-assistant!

4. Summary and Teaser for the Next Step

Through the Phase 1 transformation (PR #10), our LINE AI Business Card Assistant (line-card-assistant) has officially evolved from a simple LINE Bot application into an adk_go Agent that complies with Google Cloud Agent Platform standards:

  1. Full support for the agents-cli ecosystem: Equipped with agents-cli-manifest.yaml and .agents-cli-spec.md.
  2. Dual-channel execution capability: Can interact via LINE chat and directly through the ADK WebUI via make playground, be called by other Agents via the A2A protocol, or undergo multimodal business card recognition and search testing in the Agent Engine Playground.
  3. Cloud-native ready: Built-in VertexAiEngineSessionService auto-switching and OpenTelemetry Cloud Trace tracking capabilities.

In the next article of this series, we will enter Phase 2 & Phase 3, where we will connect HITL pause states and image artifacts to cloud persistent storage and refactor the system into a dual-layer decoupled architecture of "Cloud Run LINE Webhook Gateway + Agent Runtime Core Brain." Stay tuned!


Reference Resources and Further Reading

Top comments (0)