![[Agent Platform in Practice (2)] From Monolithic Cloud Run to Google Cloud Agent Runtime: Refactoring LINE AI Business Card King](https://media2.dev.to/dynamic/image/width=1200,height=627,fit=cover,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv26ia4v0iyw0egmy64ac.png)
Recap In the previous article, "[Agent Platform in Practice (1)] Building a LINE AI...

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:
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./webhook entry point cannot directly provide a standard Agent interface.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!
agents-cli Official GuideBefore 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".
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 |
agents-cliIn 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)"]
.agents-cli-spec.md as the Single Source of Truth for the entire development cycle.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.
adk (Python) and adk_go (Golang)!NewRootAgent(ctx) in app/agent.go.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.go test) measures code contracts and type correctness; while agents-cli eval measures Agent behavior quality (Tool Trajectory, response quality, and safety)."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.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.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 |
When planning to migrate line-card-assistant to Agent Runtime, a common concern is:
"According to
agents-clispecifications, 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):
agents-cli deploy directly (Default Scale to Zero):
--cpu 1, --memory 4Gi, --concurrency 8, --min-instances 0, --max-instances 10.0. Since Agent Runtime is billed based on actual vCPU-hours and Memory-hours, no compute fees are generated when idling at 0!service.tf) for production deployment (Default min_instances = 1):
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.agents-cli deploy --min-instances 0 during deployment or change min_instances back to 0 in service.tf.Understanding the characteristics of Agent Runtime, you'll find that splitting the system into two layers is a perfect match:
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.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!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:
agents-cli Environment and adk_go Project Scaffolding (The focus of this article!)
google-agents-cli to 1.10+ and create .agents-cli-spec.md and agents-cli-manifest.yamladk_go + agent_runtime standard launcher architecture (app/agent.go, A2A, /app-info, Sessions, and Observability)NewRootAgent state managementsession/vertexai) and GCS Artifact Service to support cross-instance HITL pause/resumeCardStore) to support Cloud SQL / Firestore cloud persistent storageagents-cli eval Automated Evaluation and Benchmarking
agents-cli eval evaluation datasets and quality flywheel (OCR, Fan-Out/Fan-In, HITL loops, Hybrid Search)agents-cli deploy and establish Terraform IaC, CI/CD, and Cloud Trace observabilityadk_go + agent_runtime Dual-Channel ArchitectureIn 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
agents-cli-manifest.yaml and .agents-cli-spec.mdTo 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"
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
app/agent.goIn 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)
}
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!
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:
setupObservability): Automatically exports Traces, Logs, and Metrics to Google Cloud Trace and Cloud Logging.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()./api/ and /ui/): Directly starts the ADK built-in streaming conversation API and web debugging interface via api.NewLauncher() and webui.NewLauncher()./.well-known/agent-card.json and provides a JSON-RPC call endpoint at /a2a/app!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!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:
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=...)
...
}
ev.ContentIn 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)
This way:
StateDelta to render the beautiful LINE card.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!
Dockerfile: One Image for Both Agent Runtime and LINE WebhookFinally, 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
ENTRYPOINT ["/agent"] starts the Agent Runtime standard server (listening on 0.0.0.0:8080)./bin/line-card-assistant!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:
agents-cli ecosystem: Equipped with agents-cli-manifest.yaml and .agents-cli-spec.md.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.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!
agents-cli and Agent Platform official documentation:
devchallengeWe're excited to announce the winners of the Hacktoberfest Open-Source AI Challenge Week 2: Side...
aiWhen we use generative AI to proofread our daily writing—whether technical reports, business...
aiA 2026 Harvard study of 718 companies found that AI agents write 30% more code and close zero additional tickets because review times jump 49%. Here is what happened when I put my 1986 Commitment Machine into live production across Gemini, Claude, and GPT-OSS.
claudeChoose Claude Models on Google Cloud (Google managed) for in-boundary security, regional data...
devchallengeWe are thrilled to announce the winners of the Sanity Challenge, our partnership with Sanity built...
geminiHow we built a 483 KB zero-WebKit macOS Universal Binary and a single-file Ubuntu HUD with...
Workflows from the Neura Market marketplace related to this Midjourney resource