<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Evan Lin</title>
    <description>The latest articles on DEV Community by Evan Lin (@evanlin).</description>
    <link>https://dev.to/evanlin</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F409957%2Fc150d4a7-cb20-469d-a230-bac27232c577.jpeg</url>
      <title>DEV Community: Evan Lin</title>
      <link>https://dev.to/evanlin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/evanlin"/>
    <language>en</language>
    <item>
      <title>[AI in Action] Rewriting a Daily LINE Bot from Python to Go 1.26 on Cloud Run: 1/4 Resource Usage, Google ADK v1.9.0, and Zero-D</title>
      <dc:creator>Evan Lin</dc:creator>
      <pubDate>Tue, 03 Nov 2026 13:29:47 +0000</pubDate>
      <link>https://dev.to/gde/ai-in-action-rewriting-a-daily-line-bot-from-python-to-go-126-on-cloud-run-14-resource-usage-2o62</link>
      <guid>https://dev.to/gde/ai-in-action-rewriting-a-daily-line-bot-from-python-to-go-126-on-cloud-run-14-resource-usage-2o62</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feeinkseadxf4b4hjpdxu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feeinkseadxf4b4hjpdxu.png" alt="LINE Bot Helper: Python to Go 1.26 &amp;amp; Cloud Run Architecture (ADK v1.9.0)" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Background
&lt;/h1&gt;

&lt;p&gt;I have a primary LINE Bot that I use heavily every day: &lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-python" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/linebot-helper-python&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Initially, it was just a small tool where you could "drop a URL and get a Traditional Chinese summary," but over the past year and a half, I've gradually integrated various new Gemini capabilities into it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-source Content Loading and Document Conversion&lt;/strong&gt;: A three-layer web crawler Fallback Chain (HTTP $\rightarrow$ Firecrawl $\rightarrow$ SingleFile Headless Chromium), YouTube video summaries, automatic conversion of PDF / Word / PPT / Excel / EPUB to Markdown, and one-click generation of FB / LinkedIn / Threads viral social media copy Carousels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bookmarks and Grounding Deep Research Reports&lt;/strong&gt;: &lt;code&gt;/save&lt;/code&gt;, &lt;code&gt;/list&lt;/code&gt;, and &lt;code&gt;/search&lt;/code&gt; bookmark management. Clicking "📄 Detailed Research Report" on a card combines Google Search Grounding to write a complete Markdown report, rendered as an independent HTML page (&lt;code&gt;/reports/{id}&lt;/code&gt;) and permanently saved in Cloud Firestore.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Maps Grounding and Gemini Batch API Gourmet Big Data Analysis&lt;/strong&gt;: Send LINE location coordinates to search for nearby gas stations, parking lots, and restaurants. It can also pull 50+ latest reviews for specific restaurants and feed them into the &lt;strong&gt;Gemini Batch API&lt;/strong&gt; (enjoying a 50% cost discount) for deep analysis of signature dishes and pitfalls, pushed back to LINE via Webhook or background polling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agentic Vision Visual Annotation&lt;/strong&gt;: After uploading an image, &lt;code&gt;gemini-3-flash-preview&lt;/code&gt; writes Python code to draw bounding boxes, which are then saved as images and pushed back to the chat room.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LINE Voice Message TTS and LIFF Real-time Two-way Voice Assistant&lt;/strong&gt;: Voice messages are automatically transcribed and paired with &lt;code&gt;gemini-3.1-flash-tts-preview&lt;/code&gt; + &lt;code&gt;ffmpeg&lt;/code&gt; to generate M4A voice readings. Opening LIFF allows two-way streaming of &lt;code&gt;gemini-3.1-flash-live-preview&lt;/code&gt; via WebSocket, supporting hands-free VAD, Barge-in interruption, and Live Tool Calling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The features are great, but the cost is obvious: &lt;strong&gt;the resident memory for Python + FastAPI + a large number of document parsing and AI packages kept increasing&lt;/strong&gt;. To ensure background tasks and multi-concurrency didn't lag, this Cloud Run service was unknowingly scaled up to &lt;strong&gt;4 vCPU / 4 GiB Memory&lt;/strong&gt; (with &lt;code&gt;--no-cpu-throttling&lt;/code&gt; enabled).&lt;/p&gt;

&lt;p&gt;As someone who has been writing Go for a long time, seeing a personal daily assistant consume 4 vCPU / 4 GiB was becoming hard to stomach. So, I decided to do one thing: &lt;strong&gt;rewrite the entire &lt;code&gt;linebot-helper-python&lt;/code&gt; 100% into the latest Go 1.26 version (&lt;a href="https://github.com/kkdai/linebot-helper-go" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/linebot-helper-go&lt;/code&gt;&lt;/a&gt;), deploy it to Cloud Run in the same GCP project, and ensure it shares Firestore data with the old version for zero-downtime switching at any time.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  1. Setting Rules Before Rewriting: Breaking Down 11 Milestones with GitHub Issues
&lt;/h1&gt;

&lt;p&gt;Moving a nearly 90KB &lt;code&gt;main.py&lt;/code&gt; plus a dozen modules from Python to Go can easily result in an unmaintainable maze if boundaries aren't defined and testing isn't done along the way.&lt;/p&gt;

&lt;p&gt;Therefore, after creating &lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-go" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/linebot-helper-go&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;, the first task wasn't writing code, but mapping out all features from the Python version. I opened &lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-go/issues/12" rel="noopener noreferrer"&gt;Tracking Issue #12&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;11 sub-task Issues (&lt;a href="https://github.com/kkdai/linebot-helper-go/issues/1" rel="noopener noreferrer"&gt;#1&lt;/a&gt; ~ &lt;a href="https://github.com/kkdai/linebot-helper-go/issues/11" rel="noopener noreferrer"&gt;#11&lt;/a&gt;)&lt;/strong&gt; on GitHub, with each Issue corresponding to an independent package and complete unit tests:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Milestone Issue&lt;/th&gt;
&lt;th&gt;Corresponding Go Package&lt;/th&gt;
&lt;th&gt;Core Responsibilities and Compatibility Highlights&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-go/issues/1" rel="noopener noreferrer"&gt;#1&lt;/a&gt; Project Skeleton and Dockerfile&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;main.go&lt;/code&gt;, &lt;code&gt;internal/config&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Go 1.26 &lt;code&gt;net/http&lt;/code&gt;, 100% environment variable compatibility with Python version, Multi-stage Dockerfile (integrating Chromium / &lt;code&gt;single-file&lt;/code&gt; / &lt;code&gt;ffmpeg&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-go/issues/2" rel="noopener noreferrer"&gt;#2&lt;/a&gt; Gemini Dual-channel Client&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;internal/gemini&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Supports both &lt;strong&gt;Vertex AI (ADC)&lt;/strong&gt; and &lt;strong&gt;Gemini Developer API (&lt;code&gt;GOOGLE_AI_API_KEY&lt;/code&gt;)&lt;/strong&gt;, covering Grounding, Code Execution, and TTS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-go/issues/3" rel="noopener noreferrer"&gt;#3&lt;/a&gt; Firestore Persistence Layer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;internal/firestore&lt;/code&gt;, &lt;code&gt;internal/services&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Fully compatible with Python version's &lt;code&gt;chat_sessions&lt;/code&gt;, &lt;code&gt;bookmarks&lt;/code&gt;, &lt;code&gt;reports&lt;/code&gt;, and &lt;code&gt;batch_jobs&lt;/code&gt; collection schemas; auto-downgrades to in-memory storage without credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-go/issues/4" rel="noopener noreferrer"&gt;#4&lt;/a&gt; LINE Messaging API and Flex&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;internal/line&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HMAC-SHA256 signature verification, Reply / Push / Loading Animation, Social copy and Bookmark Flex Carousel assembly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-go/issues/5" rel="noopener noreferrer"&gt;#5&lt;/a&gt; URL Crawler and Multi-format Doc Conversion&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;internal/loader&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;HTTP (including PTT &lt;code&gt;over18=1&lt;/code&gt;) $\rightarrow$ Firecrawl $\rightarrow$ SingleFile three-layer time-budget Fallback, and PDF / DOCX / PPTX / XLSX / EPUB / ODF to Markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-go/issues/6" rel="noopener noreferrer"&gt;#6&lt;/a&gt; AI Toolset&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;internal/ai&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Summaries, viral social copy, Grounding research reports, YouTube analysis, Agentic Vision, GitHub Issues aggregation, Maps search&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-go/issues/7" rel="noopener noreferrer"&gt;#7&lt;/a&gt; ADK Orchestrator and Specialized Agents&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;internal/agents&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Integrates official &lt;code&gt;google.golang.org/adk v1.9.0&lt;/code&gt;, establishing a Root &lt;code&gt;Orchestrator&lt;/code&gt; and 5 specialized Agents: &lt;code&gt;Chat&lt;/code&gt; / &lt;code&gt;Content&lt;/code&gt; / &lt;code&gt;Location&lt;/code&gt; / &lt;code&gt;Vision&lt;/code&gt; / &lt;code&gt;GitHub&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-go/issues/8" rel="noopener noreferrer"&gt;#8&lt;/a&gt; Asynchronous Webhook and Postback&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;internal/handlers&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Webhook returns &lt;code&gt;200 OK&lt;/code&gt; instantly, background Goroutines handle events, 9 types of Postback dispatching, and &lt;code&gt;/reports/{id}&lt;/code&gt;, &lt;code&gt;/images/{id}&lt;/code&gt;, &lt;code&gt;/audio/{id}&lt;/code&gt; endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-go/issues/9" rel="noopener noreferrer"&gt;#9&lt;/a&gt; Gemini Batch API Gourmet Analysis&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;internal/services/batch_service.go&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JSONL batch job upload, Static HMAC Webhook, Dynamic RS256 JWKS Webhook verification, and 60s background polling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-go/issues/10" rel="noopener noreferrer"&gt;#10&lt;/a&gt; LIFF Real-time Voice Assistant&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;internal/voice&lt;/code&gt;, &lt;code&gt;static/liff&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/ws/voice/{session_id}&lt;/code&gt; two-way WebSocket Relay, Push-to-Talk / hands-free VAD, Barge-in, Session Resumption, and Live Tool Calling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-go/issues/11" rel="noopener noreferrer"&gt;#11&lt;/a&gt; Cloud Run Deployment and Verification&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cloud Run (&lt;code&gt;us-central1&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Deployed using existing Python version environment variables, stress tested, and container resource specs downsized&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The overall architecture is shown in the diagram below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dot"&gt;&lt;code&gt;&lt;span class="nv"&gt;flowchart&lt;/span&gt; &lt;span class="nv"&gt;TB&lt;/span&gt;
    &lt;span class="k"&gt;subgraph&lt;/span&gt; &lt;span class="nv"&gt;Client&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"LINE Client and LIFF"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;LINE&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"LINE App Chatroom\n(Text / URL / Image / Voice / Location / Postback)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;LIFF&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"LIFF Real-time Voice Assistant\n(/liff/ + WebSocket PCM 16kHz/24kHz)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;subgraph&lt;/span&gt; &lt;span class="nv"&gt;CloudRun&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Google Cloud Run: linebot-helper-go (Go 1.26, 1 vCPU / 1 GiB)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;Mux&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Go 1.26 net/http Server\n(HMAC Verification + Immediate 200 OK)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;VoiceRelay&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"internal/voice\nGemini Live BidiGenerateContent Relay"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;Orch&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"internal/agents (google.golang.org/adk v1.9.0)\nRoot Orchestrator + 5 Sub-Agents"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"internal/loader\nHTTP -&amp;gt; Firecrawl -&amp;gt; SingleFile\n+ PDF/Office/EPUB to Markdown"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;BatchSvc&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"internal/services/batch_service\nGemini Batch API + JWKS/HMAC Webhook + 60s Poller"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;subgraph&lt;/span&gt; &lt;span class="nv"&gt;GCP&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Google Cloud &amp;amp; AI Backend"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;Vertex&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Vertex AI / Gemini Developer API\n(Search Grounding / Maps / Vision / TTS / Live)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;FS&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Cloud Firestore\nchat_sessions | bookmarks\nreports | batch_jobs"&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;end&lt;/span&gt;

    &lt;span class="nv"&gt;LINE&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;|&lt;/span&gt;&lt;span class="s2"&gt;"POST /"&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Mux&lt;/span&gt;
    &lt;span class="nv"&gt;LIFF&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;--&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s2"&gt;"GET /ws/voice/{id}"&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;VoiceRelay&lt;/span&gt;
    &lt;span class="nv"&gt;Mux&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Orch&lt;/span&gt;
    &lt;span class="nv"&gt;Mux&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Loader&lt;/span&gt;
    &lt;span class="nv"&gt;Mux&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;BatchSvc&lt;/span&gt;
    &lt;span class="nv"&gt;VoiceRelay&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;--&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s2"&gt;"WebSocket"&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Vertex&lt;/span&gt;
    &lt;span class="nv"&gt;VoiceRelay&lt;/span&gt; &lt;span class="err"&gt;-.&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="s2"&gt;"delegate_to_orchestrator"&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Orch&lt;/span&gt;
    &lt;span class="nv"&gt;Orch&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Vertex&lt;/span&gt;
    &lt;span class="nv"&gt;Orch&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Loader&lt;/span&gt;
    &lt;span class="nv"&gt;Orch&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;--&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;FS&lt;/span&gt;
    &lt;span class="nv"&gt;BatchSvc&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;--&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Vertex&lt;/span&gt;
    &lt;span class="nv"&gt;BatchSvc&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;--&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;FS&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  2. Five Engineering Designs Particularly Noteworthy During the Rewriting Process
&lt;/h1&gt;

&lt;h2&gt;
  
  
  1. Dual-track Gemini Client for Vertex AI and Developer API
&lt;/h2&gt;

&lt;p&gt;In this LINE Bot, not all features use the same Gemini endpoint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;General Chat, Grounding Search, Google Maps Grounding, Image Analysis&lt;/strong&gt;: Uses &lt;strong&gt;Vertex AI&lt;/strong&gt; (using Application Default Credentials of the Cloud Run service account, no API Key required).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native TTS Voice Synthesis (&lt;code&gt;gemini-3.1-flash-tts-preview&lt;/code&gt;), Gemini Batch API, and Gemini Live API WebSocket&lt;/strong&gt;: Uses &lt;strong&gt;Gemini Developer API&lt;/strong&gt; (using &lt;code&gt;GOOGLE_AI_API_KEY&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agentic Vision (&lt;code&gt;gemini-3-flash-preview&lt;/code&gt; + Code Execution)&lt;/strong&gt;: Prioritizes Developer API, with automatic Fallback to Vertex AI if no Key is set.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the Go version's &lt;a href="https://github.com/kkdai/linebot-helper-go/blob/main/internal/gemini/client.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/gemini/client.go&lt;/code&gt;&lt;/a&gt;, we converged these two paths under the same &lt;code&gt;gemini.Client&lt;/code&gt;. Even converting the Raw PCM (&lt;code&gt;24kHz, 16-bit, mono&lt;/code&gt;) returned by TTS into LINE-supported &lt;code&gt;audio/mp4&lt;/code&gt; (&lt;code&gt;.m4a&lt;/code&gt;) via &lt;code&gt;ffmpeg&lt;/code&gt; and calculating audio milliseconds is handled seamlessly within the same module.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. 100% Firestore Schema Compatibility with the Python Version (Key to Zero-Downtime Switching)
&lt;/h2&gt;

&lt;p&gt;The biggest fear when rewriting a service is that "after switching, user chat history, half-saved bookmarks, or previously generated research report URLs will all become invalid."&lt;/p&gt;

&lt;p&gt;To prevent this, we strictly aligned the 4 Firestore Collection names and field structures in &lt;a href="https://github.com/kkdai/linebot-helper-go/tree/main/internal/firestore" rel="noopener noreferrer"&gt;&lt;code&gt;internal/firestore&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/kkdai/linebot-helper-go/tree/main/internal/services" rel="noopener noreferrer"&gt;&lt;code&gt;internal/services&lt;/code&gt;&lt;/a&gt; with the Python version:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;chat_sessions&lt;/code&gt;: Stores multi-turn chat history and the last GPS coordinates sent by the user (&lt;code&gt;last_location&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;bookmarks&lt;/code&gt;: Stores &lt;code&gt;/save&lt;/code&gt; bookmarks, viral social copy candidate bookmarks (&lt;code&gt;status: candidate&lt;/code&gt; $\rightarrow$ &lt;code&gt;saved&lt;/code&gt;), and 7-day cached &lt;code&gt;report_id&lt;/code&gt; and &lt;code&gt;report_generated_at&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;reports&lt;/code&gt;: Stores the fully rendered HTML of Grounding deep research reports (supporting permanent retrieval via &lt;code&gt;/reports/{report_id}&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;batch_jobs&lt;/code&gt;: Stores the mapping between Gemini Batch API task IDs and LINE &lt;code&gt;user_id&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means &lt;strong&gt;the Python version and Go version can simultaneously read and write to the same Firestore online&lt;/strong&gt;. The moment the LINE Webhook is pointed to the new version, all old bookmarks and report links remain completely unaffected; if I ever want to switch back to the Python version, I only need to change one line of the Webhook URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Native Go Implementation for Multi-format Document to Markdown Conversion (Saying Goodbye to Heavy Python Packages)
&lt;/h2&gt;

&lt;p&gt;The Python version used packages like &lt;code&gt;anydoc&lt;/code&gt; to convert PDF, DOCX, PPTX, XLSX, ODT, EPUB, CSV, and RTF to Markdown before feeding them to Gemini for summarization. These packages in Python often pull in a long list of C extensions and dependencies.&lt;/p&gt;

&lt;p&gt;In the Go version &lt;a href="https://github.com/kkdai/linebot-helper-go/blob/main/internal/loader/doc_converter.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/loader/doc_converter.go&lt;/code&gt;&lt;/a&gt;, since &lt;code&gt;.docx&lt;/code&gt;, &lt;code&gt;.pptx&lt;/code&gt;, &lt;code&gt;.xlsx&lt;/code&gt;, &lt;code&gt;.odt&lt;/code&gt;, and &lt;code&gt;.epub&lt;/code&gt; are essentially &lt;strong&gt;ZIP-packaged XML files&lt;/strong&gt;, we used the Go standard library's &lt;code&gt;archive/zip&lt;/code&gt; + &lt;code&gt;encoding/xml&lt;/code&gt; along with a lightweight PDF text extractor. With less than a few hundred lines of pure Go code, we can cleanly convert all mainstream office documents and e-book formats into structured Markdown. It compiles fast and has almost zero extra memory overhead at runtime!&lt;/p&gt;




&lt;h1&gt;
  
  
  3. Facing the Architecture Honestly: Python Version's "Conceptual ADK" vs. Go Version's Actual Implementation of &lt;code&gt;google.golang.org/adk v1.9.0&lt;/code&gt;
&lt;/h1&gt;

&lt;p&gt;While checking the code, we found an interesting historical trace:&lt;/p&gt;

&lt;p&gt;If you open the beginning of the original Python version's &lt;a href="https://github.com/kkdai/linebot-helper-python/blob/main/agents/orchestrator.py" rel="noopener noreferrer"&gt;&lt;code&gt;agents/orchestrator.py&lt;/code&gt;&lt;/a&gt;, you'll see a comment saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Orchestrator Agent - Main control Agent based on Google ADK&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But if you look at the Python version's &lt;code&gt;requirements.txt&lt;/code&gt; and the entire source code, you'll find that &lt;strong&gt;&lt;code&gt;google-adk&lt;/code&gt; isn't actually installed&lt;/strong&gt;, and there isn't a single line of &lt;code&gt;from google.adk import ...&lt;/code&gt;! The Python version at that time merely "borrowed the hierarchical concept of ADK," manually creating &lt;code&gt;Orchestrator&lt;/code&gt;, &lt;code&gt;ChatAgent&lt;/code&gt;, &lt;code&gt;ContentAgent&lt;/code&gt;, etc., using pure Python classes, with the underlying layer calling &lt;code&gt;google-genai&lt;/code&gt; directly.&lt;/p&gt;

&lt;p&gt;For this Go rewrite, we decided to do it for real—directly introducing the latest official Google &lt;strong&gt;&lt;code&gt;google.golang.org/adk v1.9.0&lt;/code&gt;&lt;/strong&gt; in &lt;a href="https://github.com/kkdai/linebot-helper-go/blob/main/go.mod" rel="noopener noreferrer"&gt;&lt;code&gt;go.mod&lt;/code&gt;&lt;/a&gt; (paired with &lt;code&gt;google.golang.org/genai v1.57.0&lt;/code&gt;):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Comparison Item&lt;/th&gt;
&lt;th&gt;Original Python Version (&lt;code&gt;linebot-helper-python&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;New Go Version (&lt;code&gt;linebot-helper-go&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ADK Package Dependency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;❌ None (not installed)&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;&lt;code&gt;google.golang.org/adk v1.9.0&lt;/code&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Agent Definition&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Custom Python Class&lt;/td&gt;
&lt;td&gt;Uses &lt;code&gt;llmagent.New(llmagent.Config{...})&lt;/code&gt; to create &lt;code&gt;agent.Agent&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Sub-Agents Hierarchy&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual object attributes&lt;/td&gt;
&lt;td&gt;Root &lt;code&gt;orchestrator&lt;/code&gt; mounts 5 specialized sub-agents via &lt;code&gt;SubAgents: []agent.Agent{...}&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tools Binding&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Manual method calls&lt;/td&gt;
&lt;td&gt;Uses &lt;code&gt;functiontool.New()&lt;/code&gt; to register type-safe Go function tools (&lt;code&gt;load_url_content&lt;/code&gt;, &lt;code&gt;summarize_youtube_video&lt;/code&gt;, &lt;code&gt;search_nearby_places&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Model Interface&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Direct call to &lt;code&gt;google.genai.Client&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Implements &lt;code&gt;google.golang.org/adk/model.LLM&lt;/code&gt; and &lt;code&gt;adkgemini.NewModel&lt;/code&gt; (Vertex AI Backend)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Next Step: From "Building an ADK Agent Tree" to "Fully Driven by ADK Runner" (&lt;a href="https://github.com/kkdai/linebot-helper-go/issues/13" rel="noopener noreferrer"&gt;Issue #13&lt;/a&gt;)
&lt;/h3&gt;

&lt;p&gt;To ensure the first migration version passes all existing test cases 100%, &lt;code&gt;ProcessText&lt;/code&gt; currently retains the original fast intent routing. However, we have already planned the following 5 exciting ADK upgrades in &lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-go/issues/13" rel="noopener noreferrer"&gt;Issue #13 (&lt;code&gt;[RFC] Google ADK (v1.9.0) Deep Integration and Architecture Evolution Proposal&lt;/code&gt;)&lt;/a&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Replace Hardcoded Regex Routing with &lt;code&gt;runner.Runner&lt;/code&gt; + &lt;code&gt;SubAgents&lt;/code&gt; / &lt;code&gt;agenttool&lt;/code&gt;&lt;/strong&gt;: When a user enters a composite command in LINE (e.g., "Help me find the restaurant mentioned in this article and save it to bookmarks"), ADK will automatically transfer or call multiple Sub-Agents in parallel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add &lt;code&gt;BookmarkAgent&lt;/code&gt; and Research Report &lt;code&gt;functiontool&lt;/code&gt;&lt;/strong&gt;: Users won't need to remember &lt;code&gt;/save&lt;/code&gt; or &lt;code&gt;/search&lt;/code&gt; slash commands; they can simply say "Find the bookmark about MCP I saved last time and make a research report" to trigger it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Implement &lt;code&gt;adk/session.Service&lt;/code&gt; for Cloud Firestore&lt;/strong&gt;: Wrap the existing &lt;code&gt;firestore.KVStore&lt;/code&gt; into a standard &lt;code&gt;session.Service&lt;/code&gt;, allowing ADK Runner to natively manage multi-turn conversation events and states.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Introduce &lt;code&gt;workflowagents&lt;/code&gt; (&lt;code&gt;sequentialagent&lt;/code&gt; / &lt;code&gt;parallelagent&lt;/code&gt;)&lt;/strong&gt;: Deconstruct deep research reports into a sequential workflow of "Extraction $\rightarrow$ Google Search Verification $\rightarrow$ Report Writing"; change viral copy for FB / LinkedIn / Threads to parallel Sub-Agents for simultaneous output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connect LIFF Real-time Voice Assistant's &lt;code&gt;delegate_to_orchestrator&lt;/code&gt; directly to ADK Runner&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h1&gt;
  
  
  4. Cloud Run Deployment and Slimming Down: Machine Specs Cut Directly to 1/4!
&lt;/h1&gt;

&lt;p&gt;After completing all 10 core packages and unit tests (&lt;code&gt;go vet ./... &amp;amp;&amp;amp; go test ./...&lt;/code&gt; all passed), we deployed &lt;code&gt;linebot-helper-go&lt;/code&gt; directly to Cloud Run.&lt;/p&gt;

&lt;p&gt;Let's look at the container resource configuration comparison between the Python and Go versions on the same GCP project:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cloud Run Service&lt;/th&gt;
&lt;th&gt;Runtime Environment&lt;/th&gt;
&lt;th&gt;CPU&lt;/th&gt;
&lt;th&gt;Memory&lt;/th&gt;
&lt;th&gt;Concurrency&lt;/th&gt;
&lt;th&gt;CPU Allocation Mode&lt;/th&gt;
&lt;th&gt;Relative Resource Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;linebot-helper-python&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Python 3.11 + FastAPI&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4 vCPU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4 GiB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;Always allocated (&lt;code&gt;--no-cpu-throttling&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;100%&lt;/code&gt; (Baseline)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;linebot-helper-go&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Go 1.26 Native Compilation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 vCPU&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1 GiB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;160&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Always allocated (&lt;code&gt;--no-cpu-throttling&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;25%&lt;/code&gt; (75% Savings)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You might ask: &lt;strong&gt;The resident memory of the Go compiled executable is only 20–30 MB, why not just use &lt;code&gt;512 MiB&lt;/code&gt; instead of &lt;code&gt;1 vCPU / 1 GiB&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both numbers have practical engineering considerations behind them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Why &lt;code&gt;1 vCPU&lt;/code&gt;?&lt;/strong&gt; Because the LINE Bot needs to return &lt;code&gt;200 OK&lt;/code&gt; instantly upon receiving a Webhook and continue running Gemini generation and research report rendering in background Goroutines. Plus, there's a background Ticker polling the Gemini Batch API every 60 seconds. Therefore, &lt;code&gt;--no-cpu-throttling&lt;/code&gt; (CPU always allocated) must be enabled. In Cloud Run specifications, the minimum CPU threshold for enabling &lt;code&gt;--no-cpu-throttling&lt;/code&gt; is &lt;code&gt;1 vCPU&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why &lt;code&gt;1 GiB Memory&lt;/code&gt;?&lt;/strong&gt; The Go program itself is indeed under 30 MB, but don't forget our 3rd layer Fallback for the URL crawler has &lt;strong&gt;&lt;code&gt;single-file&lt;/code&gt; + Headless Chromium&lt;/strong&gt; installed in the container (used to deal with dynamic rendering sites that even Firecrawl blocks), plus voice message synthesis requires calling &lt;code&gt;ffmpeg&lt;/code&gt;. When Headless Chromium is occasionally awakened, it instantly consumes 300–400 MB of memory. Keeping &lt;code&gt;1 GiB&lt;/code&gt; ensures Chromium never OOMs (Out of Memory) upon startup while cutting machine costs to &lt;strong&gt;1/4&lt;/strong&gt; of the original Python version!&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The deployment command is very clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud run deploy linebot-helper-go &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt; &amp;lt;YOUR_GCP_PROJECT_ID&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-central1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpu&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--memory&lt;/span&gt; 1Gi &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allow-unauthenticated&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-cpu-throttling&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  5. Seven Devilish Details Most Likely to be Tripped Over or Missed in Cloud Deployment
&lt;/h1&gt;

&lt;p&gt;Moving a LINE Bot from local testing to Cloud Run, or switching from an old Python service to a new Go service, "successful compilation" often only accounts for 60% of the work. The real traps are usually hidden in Cloud Run's runtime lifecycle, network proxies, container sandboxing, and external endpoint settings.&lt;/p&gt;

&lt;p&gt;Here are the &lt;strong&gt;7 key blind spots&lt;/strong&gt; most easily overlooked during this deployment process:&lt;/p&gt;

&lt;h2&gt;
  
  
  Omission 1: Forgot to add &lt;code&gt;--no-cpu-throttling&lt;/code&gt;, or passed &lt;code&gt;r.Context()&lt;/code&gt; into a background Goroutine
&lt;/h2&gt;

&lt;p&gt;LINE Webhook specifications require the server to return &lt;code&gt;200 OK&lt;/code&gt; quickly after receiving an event. Our heavy tasks (such as Grounding deep research reports taking 1–2 minutes, Agentic Vision writing code to draw, voice transcription plus TTS, and the Gemini Batch API Poller polling every 60 seconds) all return &lt;code&gt;200 OK&lt;/code&gt; first, then run &lt;code&gt;go func()&lt;/code&gt; in the background and use &lt;code&gt;PushMessage&lt;/code&gt; to push the result.&lt;/p&gt;

&lt;p&gt;There are two traps here that both beginners and experts are extremely likely to fall into:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Run "freezes the CPU" by default the moment the HTTP Response is sent&lt;/strong&gt;: If you don't add &lt;code&gt;--no-cpu-throttling&lt;/code&gt;, as soon as &lt;code&gt;handleWebhookCallback&lt;/code&gt; returns &lt;code&gt;200 OK&lt;/code&gt;, Cloud Run will drop the container CPU to near 0 within tens of milliseconds. Your background Goroutine will be stuck in mid-air like it was paused, only moving a few steps when the next HTTP request comes in minutes later. The user will never receive the push!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Background Goroutines must never use &lt;code&gt;r.Context()&lt;/code&gt;&lt;/strong&gt;: In Go's &lt;code&gt;net/http&lt;/code&gt;, &lt;code&gt;r.Context()&lt;/code&gt; is triggered as &lt;code&gt;Canceled&lt;/code&gt; the moment the HTTP Handler ends (sends &lt;code&gt;200 OK&lt;/code&gt;)! If you casually write &lt;code&gt;go s.handleMessageEvent(r.Context(), event)&lt;/code&gt;, background requests calling Vertex AI or Firestore will die directly with a &lt;code&gt;context canceled&lt;/code&gt; error.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ❌ Incorrect: r.Context() will be canceled the moment 200 OK is returned; background tasks will fail.&lt;/span&gt;
&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handleMessageEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;eventCopy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// ✅ Correct: Detach from the HTTP Request lifecycle, use an independent Background Context (set Timeout internally).&lt;/span&gt;
&lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handleMessageEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;eventCopy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Omission 2: Vertex AI &lt;code&gt;GOOGLE_CLOUD_LOCATION=global&lt;/code&gt; Domain Concatenation Trap
&lt;/h2&gt;

&lt;p&gt;When reusing old environment variables, we saw &lt;code&gt;GOOGLE_CLOUD_LOCATION=global&lt;/code&gt; (because many Gemini 3.x Preview models in Vertex AI are under the &lt;code&gt;global&lt;/code&gt; location).&lt;/p&gt;

&lt;p&gt;If you write a Vertex AI REST Client in Go and follow the habit for general regions by using &lt;code&gt;fmt.Sprintf("https://%s-aiplatform.googleapis.com", location)&lt;/code&gt;, when &lt;code&gt;location&lt;/code&gt; is &lt;code&gt;"global"&lt;/code&gt;, the resulting URL becomes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;❌ &lt;code&gt;https://global-aiplatform.googleapis.com&lt;/code&gt; (&lt;strong&gt;This domain doesn't exist in GCP; DNS lookup will fail!&lt;/strong&gt;)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In Vertex AI specifications, only regional endpoints (like &lt;code&gt;us-central1&lt;/code&gt;, &lt;code&gt;asia-east1&lt;/code&gt;) have a prefix; when the Location is &lt;code&gt;global&lt;/code&gt;, you must switch to the prefix-less &lt;code&gt;https://aiplatform.googleapis.com&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;vertexEndpointBase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VertexBaseURL&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimRight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VertexBaseURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Location&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Location&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"global"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"https://aiplatform.googleapis.com"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://%s-aiplatform.googleapis.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Omission 3: Moving Environment Variables Using &lt;code&gt;--set-env-vars&lt;/code&gt; Leading to Character Truncation or Secret Leakage
&lt;/h2&gt;

&lt;p&gt;When moving over a dozen environment variables from &lt;code&gt;linebot-helper-python&lt;/code&gt; to &lt;code&gt;linebot-helper-go&lt;/code&gt;, never manually string together &lt;code&gt;--set-env-vars="KEY1=VAL1,KEY2=VAL2..."&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If any Token or Prompt string contains a comma &lt;code&gt;,&lt;/code&gt; or equals sign &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;gcloud&lt;/code&gt;'s parameter parsing will split at the wrong position or even error out.&lt;/li&gt;
&lt;li&gt;Putting environment variables directly in the command line parameters leaves the entire Secret string in the terminal history or conversation logs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The safest way&lt;/strong&gt;: Directly export the temporary &lt;code&gt;/tmp/env.yaml&lt;/code&gt; locally using &lt;code&gt;gcloud run services describe --format=json&lt;/code&gt;, feed it in during deployment using &lt;code&gt;--env-vars-file=/tmp/env.yaml&lt;/code&gt;, and &lt;code&gt;rm -f /tmp/env.yaml&lt;/code&gt; immediately after deployment. This way, no Secrets are printed, and all special characters are 100% preserved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Omission 4: Ignoring Cloud Run Reverse Proxy's &lt;code&gt;X-Forwarded-Proto&lt;/code&gt;, Leading to LINE Rejecting Images and Audio
&lt;/h2&gt;

&lt;p&gt;This LINE Bot needs to dynamically assemble "public URLs pointing to its own server" in four places:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Research report page: &lt;code&gt;https://&amp;lt;host&amp;gt;/reports/{report_id}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Agentic Vision annotated image: &lt;code&gt;https://&amp;lt;host&amp;gt;/images/{image_id}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;TTS voice synthesis audio file: &lt;code&gt;https://&amp;lt;host&amp;gt;/audio/{audio_id}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Gemini Batch API Dynamic Webhook: &lt;code&gt;https://&amp;lt;host&amp;gt;/api/gemini-callback/dynamic&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Note: &lt;strong&gt;LINE Messaging API's &lt;code&gt;ImageMessage&lt;/code&gt; and &lt;code&gt;AudioMessage&lt;/code&gt; strictly require URLs to be &lt;code&gt;https://&lt;/code&gt;&lt;/strong&gt;. In the Cloud Run architecture, HTTPS TLS is terminated at the outer Google Front End (GFE), so &lt;code&gt;r.TLS&lt;/code&gt; is &lt;code&gt;nil&lt;/code&gt; when it reaches the Go &lt;code&gt;http.Request&lt;/code&gt; inside the container. If you don't read the &lt;code&gt;X-Forwarded-Proto&lt;/code&gt; and &lt;code&gt;X-Forwarded-Host&lt;/code&gt; headers, the assembled &lt;code&gt;http://&lt;/code&gt; URL will be rejected by the LINE Push API!&lt;/p&gt;

&lt;h2&gt;
  
  
  Omission 5: Pursuing Minimalist Go Images (&lt;code&gt;scratch&lt;/code&gt; / &lt;code&gt;distroless&lt;/code&gt;) but Forgetting External Executables and Chromium Sandbox Restrictions
&lt;/h2&gt;

&lt;p&gt;When writing Go services, we often use &lt;code&gt;FROM gcr.io/distroless/static&lt;/code&gt; to compress the Docker Image to under 20MB. But doing this for this multi-functional LINE Bot will immediately hit a wall:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Missing &lt;code&gt;ffmpeg&lt;/code&gt;&lt;/strong&gt;: After receiving PCM from Gemini for TTS synthesis, calling &lt;code&gt;exec.Command("ffmpeg", ...)&lt;/code&gt; to convert to &lt;code&gt;.m4a&lt;/code&gt; will report &lt;code&gt;executable file not found&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing Headless Chromium, Chinese Fonts, and Container Sandbox Flags&lt;/strong&gt;: The 3rd layer Fallback of the crawler uses &lt;code&gt;single-file&lt;/code&gt; to drive Headless Chromium. In Cloud Run's container sandbox environment, if you don't add &lt;code&gt;--no-sandbox&lt;/code&gt;, &lt;code&gt;--disable-gpu&lt;/code&gt;, and &lt;code&gt;--disable-dev-shm-usage&lt;/code&gt; (because the container's default &lt;code&gt;/dev/shm&lt;/code&gt; shared memory is small) to Chromium, or if the base image doesn't have &lt;code&gt;fonts-noto-cjk&lt;/code&gt; installed (resulting in garbled text on captured Chinese web pages) and &lt;code&gt;ca-certificates&lt;/code&gt; (HTTPS certificate verification failure), the crawler will silently crash in the cloud.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Therefore, our &lt;a href="https://github.com/kkdai/linebot-helper-go/blob/main/Dockerfile" rel="noopener noreferrer"&gt;&lt;code&gt;Dockerfile&lt;/code&gt;&lt;/a&gt; uses &lt;code&gt;golang:1.26-bookworm&lt;/code&gt; for compilation, and the Runtime stage uses &lt;code&gt;debian:bookworm-slim&lt;/code&gt; with explicit installation of &lt;code&gt;ca-certificates&lt;/code&gt;, &lt;code&gt;ffmpeg&lt;/code&gt;, &lt;code&gt;chromium&lt;/code&gt;, &lt;code&gt;fonts-noto-cjk&lt;/code&gt;, and &lt;code&gt;@single-file/cli&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Omission 6: State Splitting Between In-Memory Caching and Cloud Run Auto-scaling (Multi-Instance)
&lt;/h2&gt;

&lt;p&gt;Although we have persisted the four core states—chat history, bookmarks, research reports, and Batch Job mappings—to Cloud Firestore, some binary data with a "lifecycle of only 5 minutes" is stored in a Go in-memory map:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Images just uploaded by users, waiting for the "Identify Image / Agentic Vision" Quick Reply selection (&lt;code&gt;imageTempStore&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;PNGs just drawn with bounding boxes, waiting for the LINE server to pull them (&lt;code&gt;/images/{image_id}&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;M4A audio files just synthesized, waiting for the LINE server to pull them (&lt;code&gt;/audio/{audio_id}&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;This works perfectly under a single instance, but once Cloud Run scales to 2 or more instances due to a burst in traffic, state splitting occurs&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User sends an image to &lt;strong&gt;Instance A&lt;/strong&gt; (image stored in A's memory).&lt;/li&gt;
&lt;li&gt;3 seconds later, the user clicks the "Agentic Vision" button, and the Webhook hits &lt;strong&gt;Instance B&lt;/strong&gt; $\rightarrow$ B can't find the image and replies "⚠️ Image expired, please resend!"&lt;/li&gt;
&lt;li&gt;Or &lt;strong&gt;Instance A&lt;/strong&gt; generates &lt;code&gt;/audio/{id}&lt;/code&gt; and calls the LINE Reply API, but when the LINE media server comes to download the audio, it hits &lt;strong&gt;Instance B&lt;/strong&gt;, gets a &lt;code&gt;404&lt;/code&gt;, and the user only sees an unplayable voice bubble in the chat room.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Solution&lt;/strong&gt;: Since a single Go instance can easily handle high concurrency (we set &lt;code&gt;containerConcurrency&lt;/code&gt; to &lt;code&gt;160&lt;/code&gt;), for personal or small-to-medium assistants, it's recommended to limit &lt;code&gt;--max-instances&lt;/code&gt; to &lt;code&gt;1&lt;/code&gt; (if multi-instance high availability is needed in the future, the caching for &lt;code&gt;/images&lt;/code&gt; and &lt;code&gt;/audio&lt;/code&gt; must be rewritten to Cloud Storage or Firestore).&lt;/p&gt;

&lt;h2&gt;
  
  
  Omission 7: Only Changing the Messaging API Webhook During Switching, but Missing Firebase Hosting WebSocket Restrictions and LIFF Endpoint
&lt;/h2&gt;

&lt;p&gt;When preparing to switch from the old service to the new one, the following three peripheral settings are most commonly missed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Checkpoint&lt;/th&gt;
&lt;th&gt;Why is it easily missed?&lt;/th&gt;
&lt;th&gt;Actual Impact and Correct Practice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1. Is the existing Webhook hidden behind Firebase Hosting?&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;When querying existing settings via LINE API (&lt;code&gt;GET /v2/bot/channel/webhook/endpoint&lt;/code&gt;), we found the original Webhook pointed to &lt;code&gt;*.firebaseapp.com&lt;/code&gt; (rewritten to Cloud Run by &lt;code&gt;firebase.json&lt;/code&gt;).&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Firebase Hosting does not support WebSocket proxying!&lt;/strong&gt; If the LIFF voice assistant (&lt;code&gt;/ws/voice/{id}&lt;/code&gt;) goes through the Firebase Hosting domain, the WebSocket handshake will fail. Therefore, when switching, point directly to the Cloud Run URL or ensure LIFF uses a direct connection.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2. LIFF Endpoint URL of the LINE Login Channel&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;LIFF App settings are not in the Messaging API Channel but in the &lt;strong&gt;LINE Login Channel&lt;/strong&gt; tab under the same Provider.&lt;/td&gt;
&lt;td&gt;If you only change the Webhook and not the LIFF Endpoint, the chat room text will be handled by the new Go version, but opening the voice assistant will still connect back to the old Python version.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3. Gemini Batch API Static Webhook / Eventarc&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;If Eventarc or a project-level static Webhook is set in GCP to point to &lt;code&gt;/api/gemini-callback/static&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;Remember to update the target URL as well (while the Dynamic Webhook &lt;code&gt;/api/gemini-callback/dynamic&lt;/code&gt; will automatically capture the Host of the incoming request, requiring no manual modification).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h1&gt;
  
  
  6. Inventory Before Switching: Identifying Three "Zombie Endpoints" from GCP Logs and Planning to Incorporate &lt;code&gt;n8n&lt;/code&gt; Schedules!
&lt;/h1&gt;

&lt;p&gt;While writing the "Switching from Python Version" steps in the README, I originally listed:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Change external schedules calling &lt;code&gt;/hn&lt;/code&gt;, &lt;code&gt;/hf&lt;/code&gt;, and &lt;code&gt;/urls&lt;/code&gt; to point to the new service."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But then I thought: &lt;strong&gt;Wait, are these three endpoints actually still being called by my &lt;code&gt;n8n&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We immediately used &lt;code&gt;gcloud logging read&lt;/code&gt; to pull execution records for the &lt;code&gt;linebot-helper-python&lt;/code&gt; and &lt;code&gt;n8n&lt;/code&gt; services in the GCP project, and found a big surprise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/hn&lt;/code&gt;, &lt;code&gt;/hf&lt;/code&gt;, and &lt;code&gt;/urls&lt;/code&gt; were endpoints written in the first version in &lt;strong&gt;November 2024&lt;/strong&gt;, intended for external schedules to POST URL arrays for the LINE Bot to summarize and push.&lt;/li&gt;
&lt;li&gt;But the &lt;code&gt;n8n&lt;/code&gt; service currently in the cloud (running four active workflows: &lt;strong&gt;"RSS to Sheet"&lt;/strong&gt;, &lt;strong&gt;"Daily Tasks (Fetch Papers/Share Videos)"&lt;/strong&gt;, &lt;strong&gt;"Daily LINE Sender"&lt;/strong&gt;, and &lt;strong&gt;"AI Trends Automated Newsletter"&lt;/strong&gt;) had already been changed to &lt;strong&gt;run Firecrawl internally in &lt;code&gt;n8n&lt;/code&gt;, call Gemini for summaries, assemble Flex JSON, and directly hit &lt;code&gt;https://api.line.me/v2/bot/message/push&lt;/code&gt;&lt;/strong&gt;!&lt;/li&gt;
&lt;li&gt;In other words, the number of calls to &lt;code&gt;/hn&lt;/code&gt;, &lt;code&gt;/hf&lt;/code&gt;, and &lt;code&gt;/urls&lt;/code&gt; on &lt;code&gt;linebot-helper-python&lt;/code&gt; from &lt;code&gt;n8n&lt;/code&gt; over the past few months &lt;strong&gt;was 0&lt;/strong&gt;!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Even more interestingly, while checking &lt;code&gt;n8n&lt;/code&gt;'s Debug Logs, we accidentally caught &lt;code&gt;n8n&lt;/code&gt; failing to push YouTube new video summaries at 6 AM because of word count or thumbnail format issues in the Flex Carousel assembled by the Javascript node, with the LINE API returning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Workflow execution finished with error: A message (messages[0]) in the request body is invalid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This discovery led us to make two immediate decisions:&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision 1: Immediately remove &lt;code&gt;/hn&lt;/code&gt;, &lt;code&gt;/hf&lt;/code&gt;, and &lt;code&gt;/urls&lt;/code&gt; from &lt;code&gt;linebot-helper-go&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Since no external services are calling them, keeping them on the public Cloud Run only adds three attack surfaces without signature protection. After removing them from &lt;code&gt;server.go&lt;/code&gt; and redeploying, hitting &lt;code&gt;/hn&lt;/code&gt;, &lt;code&gt;/hf&lt;/code&gt;, or &lt;code&gt;/urls&lt;/code&gt; online returns &lt;code&gt;404 Not Found&lt;/code&gt;. When switching, I only need to change the &lt;strong&gt;Webhook URL&lt;/strong&gt; and &lt;strong&gt;LIFF Endpoint URL&lt;/strong&gt; in the LINE Developers Console!&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision 2: Open &lt;a href="https://github.com/kkdai/linebot-helper-go/issues/14" rel="noopener noreferrer"&gt;Issue #14&lt;/a&gt; ~ &lt;a href="https://github.com/kkdai/linebot-helper-go/issues/19" rel="noopener noreferrer"&gt;Issue #19&lt;/a&gt; to incorporate &lt;code&gt;n8n&lt;/code&gt; workflows into &lt;code&gt;linebot-helper-go&lt;/code&gt;!
&lt;/h3&gt;

&lt;p&gt;Since &lt;code&gt;linebot-helper-go&lt;/code&gt; is already a high-speed Go server always resident at &lt;code&gt;1 vCPU / 1 GiB&lt;/code&gt; (&lt;code&gt;--no-cpu-throttling&lt;/code&gt;), and it has a built-in three-layer crawler &lt;code&gt;loader.Loader&lt;/code&gt;, strongly-typed LINE Flex Builder, Gemini Batch API, Firestore &lt;code&gt;BookmarkService&lt;/code&gt;, and &lt;code&gt;ReportStore&lt;/code&gt;, why maintain a separate &lt;code&gt;n8n&lt;/code&gt; + Postgres database?&lt;/p&gt;

&lt;p&gt;More importantly—&lt;strong&gt;if these daily schedules are all moved to &lt;code&gt;linebot-helper-go&lt;/code&gt; and pushed through the same LINE Official Account, a perfect closed loop of "Push $\rightarrow$ Interaction" can be achieved&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-go/issues/14" rel="noopener noreferrer"&gt;Issue #14 (&lt;code&gt;Batch Task 1/5&lt;/code&gt;)&lt;/a&gt;&lt;/strong&gt;: Establish a protected &lt;code&gt;POST /api/tasks/{task_name}&lt;/code&gt; schedule endpoint and a Firestore &lt;code&gt;feed_items&lt;/code&gt; deduplication queue (replacing the old method of comparing rows in Google Sheets), unifying pushes using the main official account.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-go/issues/15" rel="noopener noreferrer"&gt;Issue #15 (&lt;code&gt;Batch Task 2/5&lt;/code&gt;)&lt;/a&gt;&lt;/strong&gt;: Fetch &lt;strong&gt;Hugging Face Daily Papers&lt;/strong&gt; every day at 10:00. When pushing paper summary cards, directly attach &lt;strong&gt;"🔖 Save Bookmark"&lt;/strong&gt; and &lt;strong&gt;"📄 Detailed Research Report"&lt;/strong&gt; buttons, allowing one-click generation of Grounding deep web reports for good papers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-go/issues/16" rel="noopener noreferrer"&gt;Issue #16 (&lt;code&gt;Batch Task 3/5&lt;/code&gt;)&lt;/a&gt;&lt;/strong&gt;: Inspect &lt;strong&gt;YouTube subscription channel new videos&lt;/strong&gt; every day at 06:00. Use Go's strongly-typed Flex Builder to completely solve &lt;code&gt;n8n&lt;/code&gt;'s &lt;code&gt;400 Invalid Request Body&lt;/code&gt; issue, and attach a &lt;strong&gt;"📝 Detailed Summary"&lt;/strong&gt; Postback button directly to the card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-go/issues/17" rel="noopener noreferrer"&gt;Issue #17 (&lt;code&gt;Batch Task 4/5&lt;/code&gt;)&lt;/a&gt;&lt;/strong&gt;: Integrate multi-source RSS inspection from HN, Hatena IT, Google Dev Blog, LY Blog, Steam, etc., with daily 06:00 featured pushes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-go/issues/18" rel="noopener noreferrer"&gt;Issue #18 (&lt;code&gt;Batch Task 5/5&lt;/code&gt;)&lt;/a&gt;&lt;/strong&gt;: Aggregate weekly featured content and bookmarks every Friday at 17:00, rendering them directly into a beautiful &lt;code&gt;/reports/{id}&lt;/code&gt; web version &lt;strong&gt;AI Trends Weekly&lt;/strong&gt; pushed to LINE.&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  7. One Last Security Reminder: Don't Leave Real Cloud Run URLs in Public Git History
&lt;/h1&gt;

&lt;p&gt;After writing the deployment records and switching tutorial, I did one very important cleanup task: &lt;strong&gt;replaced all occurrences of &lt;code&gt;.run.app&lt;/code&gt; real service URLs, GCP Project Numbers, and LIFF IDs in &lt;code&gt;README.md&lt;/code&gt; and GitHub Issue comments with &lt;code&gt;&amp;lt;YOUR_CLOUD_RUN_SERVICE_URL&amp;gt;&lt;/code&gt; placeholders, and rewrote the Git Commit history to eliminate traces.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The reason is simple: The LINE Bot's Cloud Run service must be set to &lt;code&gt;--allow-unauthenticated&lt;/code&gt; to receive Webhooks from the LINE platform. Although our &lt;code&gt;POST /&lt;/code&gt; has strict &lt;code&gt;X-Line-Signature&lt;/code&gt; verification (requests without a Channel Secret signature are blocked with a &lt;code&gt;400&lt;/code&gt;), writing the real &lt;code&gt;.run.app&lt;/code&gt; URL directly in a public GitHub Repo's README still easily attracts unnecessary automated crawler scans and traffic noise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sharing architecture and implementation with public code while keeping real endpoints only in your own configuration backend&lt;/strong&gt; is the most reassuring way to maintain a personal AI assistant in the long run.&lt;/p&gt;




&lt;h1&gt;
  
  
  8. Cloud Run Deployment and Migration Self-Checklist
&lt;/h1&gt;

&lt;p&gt;If you also plan to rewrite an existing Python LINE Bot into Go and move it to Cloud Run, you can use this checklist to tick off items before switching traffic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; &lt;strong&gt;1. CPU Throttling Setting&lt;/strong&gt;: As long as there are background Goroutines (Webhook returns &lt;code&gt;200 OK&lt;/code&gt; followed by background push, or background periodic Ticker), be sure to add &lt;code&gt;--no-cpu-throttling&lt;/code&gt; (and set CPU to at least &lt;code&gt;1&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;2. Context Lifecycle Decoupling&lt;/strong&gt;: Background Goroutines are prohibited from passing in &lt;code&gt;r.Context()&lt;/code&gt;; always use &lt;code&gt;context.Background()&lt;/code&gt; with an independent &lt;code&gt;context.WithTimeout&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;3. Vertex AI &lt;code&gt;global&lt;/code&gt; Domain Judgment&lt;/strong&gt;: If &lt;code&gt;GOOGLE_CLOUD_LOCATION=global&lt;/code&gt;, confirm the Endpoint Base URL is &lt;code&gt;https://aiplatform.googleapis.com&lt;/code&gt; instead of &lt;code&gt;global-aiplatform.googleapis.com&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;4. External Binary Tools and Fonts&lt;/strong&gt;: If &lt;code&gt;ffmpeg&lt;/code&gt;, &lt;code&gt;single-file&lt;/code&gt;, or Headless Chromium are called inside the container, confirm the final stage Docker Image includes the corresponding executables, &lt;code&gt;ca-certificates&lt;/code&gt;, &lt;code&gt;fonts-noto-cjk&lt;/code&gt;, and Chromium's &lt;code&gt;--no-sandbox --disable-dev-shm-usage&lt;/code&gt; parameters.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;5. Reverse Proxy HTTPS Headers&lt;/strong&gt;: When dynamically generating &lt;code&gt;/images/{id}&lt;/code&gt;, &lt;code&gt;/audio/{id}&lt;/code&gt;, and &lt;code&gt;/reports/{id}&lt;/code&gt; URLs, prioritize reading &lt;code&gt;X-Forwarded-Proto&lt;/code&gt; and &lt;code&gt;X-Forwarded-Host&lt;/code&gt; to ensure media URLs sent to LINE are always &lt;code&gt;https://&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;6. In-Memory Caching and Instance Count&lt;/strong&gt;: If there are short-lived memory maps (like cached images/audio), it's recommended to set &lt;code&gt;--max-instances=1&lt;/code&gt; and increase &lt;code&gt;concurrency&lt;/code&gt;, or switch to GCS / Firestore to avoid state splitting across multiple instances.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;7. Dual-channel LINE Setting Synchronization&lt;/strong&gt;: In addition to updating the &lt;strong&gt;Messaging API&lt;/strong&gt; Webhook URL, don't forget to update the &lt;strong&gt;LIFF Endpoint URL&lt;/strong&gt; of the &lt;strong&gt;LINE Login Channel&lt;/strong&gt; under the same Provider, and confirm the WebSocket path is not intercepted by a CDN / Firebase Hosting that doesn't support WS.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;8. Clean Up Obsolete Endpoints and Public Information&lt;/strong&gt;: Confirm useless old endpoints (like &lt;code&gt;/hn&lt;/code&gt;, &lt;code&gt;/hf&lt;/code&gt;, &lt;code&gt;/urls&lt;/code&gt;) from Cloud Logging and remove them, and ensure the public Repo and Git history do not contain real &lt;code&gt;.run.app&lt;/code&gt; URLs and keys.&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Summary and Open Source Project Links
&lt;/h1&gt;

&lt;p&gt;From this rewriting journey from &lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-python" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/linebot-helper-python&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; to &lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-go" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/linebot-helper-go&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;, here are the most valuable takeaways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Open Issues Before Writing Code&lt;/strong&gt;: 11 milestone Issues made the large-scale language rewrite highly controllable, with each module independently passing unit tests before being layered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared Data Layer is the Foundation for Zero-Downtime Migration&lt;/strong&gt;: As long as the Firestore Collection Schemas are aligned, services in the two languages can be switched painlessly at any time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go's Resource Efficiency on Cloud Run is Extremely High&lt;/strong&gt;: Even with Headless Chromium and &lt;code&gt;ffmpeg&lt;/code&gt; mounted, switching from Python's &lt;code&gt;4 vCPU / 4 GiB&lt;/code&gt; to Go's &lt;code&gt;1 vCPU / 1 GiB&lt;/code&gt; cut the bill to 25%, with even faster response times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check Cloud Logging Before Switching&lt;/strong&gt;: Real traffic logs don't lie; you can see at a glance which endpoints are already retired and which external &lt;code&gt;n8n&lt;/code&gt; workflows are actually better suited to be incorporated into the same service.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The complete source code and subsequent ADK v1.9.0 / Batch Tasks evolution plans are all open-sourced on GitHub: &lt;strong&gt;&lt;a href="https://github.com/kkdai/linebot-helper-go" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/linebot-helper-go&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;. Feel free to refer to it and exchange ideas!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>backend</category>
      <category>cloud</category>
      <category>go</category>
    </item>
    <item>
      <title>[AI in Practice] Rewriting a LINE Research Bot from Python Managed Agents to Go ADK v1.9.0: From Sandbox Files to Session State,</title>
      <dc:creator>Evan Lin</dc:creator>
      <pubDate>Tue, 03 Nov 2026 13:28:58 +0000</pubDate>
      <link>https://dev.to/gde/ai-in-practice-rewriting-a-line-research-bot-from-python-managed-agents-to-go-adk-v190-from-4c50</link>
      <guid>https://dev.to/gde/ai-in-practice-rewriting-a-line-research-bot-from-python-managed-agents-to-go-adk-v190-from-4c50</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgz8wlbx0imdo4ue58z5i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgz8wlbx0imdo4ue58z5i.png" alt="LINE Research Bot: Managed Agents (Python) → ADK for Go v1.9.0 on Cloud Run" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Recap
&lt;/h1&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/gde/geminiagent-google-managed-agents-api-4e43"&gt;previous post "Comprehensive Analysis of Managed Agents API"&lt;/a&gt;, I created an open-source LINE Research Planner Bot: &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-research-bot" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/line-research-bot&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;How it works is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Send " &lt;strong&gt;Research the selection of SOTA open-source vector databases&lt;/strong&gt; " to the LINE Bot. The Bot will sequentially run through three stages: &lt;strong&gt;PLAN → SEARCH_COMPARE → WRITE_REPORT&lt;/strong&gt;. First, it designs 4–8 sets of search keywords, then uses Google Search + URL Context to read real web pages and cross-compare "consensus / disagreement / gaps," and finally writes a Traditional Chinese Markdown report with footnote citations.&lt;/li&gt;
&lt;li&gt;The report is rendered into HTML by Cloud Run and uploaded to Cloud Storage. LINE receives a Flex card with a summary, main sources, and a "Read Full Report" button.&lt;/li&gt;
&lt;li&gt;If you later send " &lt;strong&gt;Go deeper into Chapter 2, add Japanese sources&lt;/strong&gt; ", the Bot will deepen the same report, increment the version number by +1, and automatically save the old version as a snapshot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first version used &lt;strong&gt;Python 3.11 + FastAPI + &lt;code&gt;google-genai&lt;/code&gt; (&lt;code&gt;enterprise=True&lt;/code&gt;)&lt;/strong&gt; to call the Pre-GA &lt;strong&gt;Managed Agents API&lt;/strong&gt;. It relied on the official sandbox's &lt;strong&gt;persistent filesystem&lt;/strong&gt;: &lt;code&gt;/workspace/plan.json&lt;/code&gt;, &lt;code&gt;/workspace/sources.json&lt;/code&gt;, and &lt;code&gt;/workspace/report.md&lt;/code&gt; survived across turns under the same &lt;code&gt;environment_id&lt;/code&gt;, allowing the Agent to read the files itself when deepening the research.&lt;/p&gt;

&lt;p&gt;It was fun, but after running it for a while, a few things kept me uneasy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pre-GA, no SLA&lt;/strong&gt;: Names like &lt;code&gt;base_agent: antigravity-preview-05-2026&lt;/code&gt; clearly indicate they will change one day.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;gsutil&lt;/code&gt; in the sandbox is mocked&lt;/strong&gt; (Pitfall #2 from the previous post). As a result, after writing the report, the Agent still had to stuff the entire Markdown back into a JSON for Cloud Run to upload. The value of the sandbox filesystem was essentially reduced to "remembering three files across turns."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Must use &lt;code&gt;background=True&lt;/code&gt; + manual polling loop&lt;/strong&gt;: If Stage 2/3 was slow, the timeout had to be adjusted (I once tuned it from 240 seconds all the way to 600 seconds).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So this time, I decided: &lt;strong&gt;Rewrite the entire service in Go, use Google's official &lt;a href="https://github.com/google/adk-go" rel="noopener noreferrer"&gt;ADK for Go&lt;/a&gt; (&lt;code&gt;google.golang.org/adk&lt;/code&gt; v1.9.0) for everything Agent-related, and deploy it back to the original Cloud Run service using the exact same environment variables.&lt;/strong&gt; I recorded the entire process in the original repo's &lt;a href="https://github.com/kkdai/line-research-bot/issues/1" rel="noopener noreferrer"&gt;Issue #1&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;After completing the rewrite, I separated the Go version into a new repo so that the Python version cited in the previous article remains unchanged. The two versions can be directly compared:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repo&lt;/th&gt;
&lt;th&gt;Content&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/kkdai/line-research-bot" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/line-research-bot&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Original Python 3.11 + FastAPI + Managed Agents version (example from the previous article), and the &lt;a href="https://github.com/kkdai/line-research-bot/issues/1" rel="noopener noreferrer"&gt;Issue #1&lt;/a&gt; record of this migration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/kkdai/line-research-bot-go" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/line-research-bot-go&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The protagonist of this post: Go 1.27 + ADK for Go v1.9.0, currently running on Cloud Run&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h1&gt;
  
  
  I. Clarification: It wasn't actually "Python ADK" before
&lt;/h1&gt;

&lt;p&gt;Many people (including myself initially) mix up Managed Agents and ADK, but they are two different things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Comparison Item&lt;/th&gt;
&lt;th&gt;Original Python Version&lt;/th&gt;
&lt;th&gt;New Go Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Agent Execution Location&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google-managed sandbox (Managed Agents API)&lt;/td&gt;
&lt;td&gt;Within your own Cloud Run container (ADK Runner)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;SDK&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;google-genai&lt;/code&gt; (&lt;code&gt;enterprise=True&lt;/code&gt;), &lt;strong&gt;not&lt;/strong&gt; using &lt;code&gt;google-adk&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;google.golang.org/adk v1.9.0&lt;/code&gt;&lt;/strong&gt; + &lt;code&gt;google.golang.org/genai&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Agent Definition&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;deploy/create-agent.sh&lt;/code&gt; uses curl to call REST API to create a &lt;code&gt;research-planner&lt;/code&gt; resource&lt;/td&gt;
&lt;td&gt;In-code &lt;code&gt;llmagent.New(llmagent.Config{...})&lt;/code&gt; × 3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;code_execution&lt;/code&gt;, &lt;code&gt;filesystem&lt;/code&gt;, &lt;code&gt;google_search&lt;/code&gt;, &lt;code&gt;url_context&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;geminitool.GoogleSearch{}&lt;/code&gt; + &lt;code&gt;url_context&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cross-turn State&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Sandbox filesystem (&lt;code&gt;environment_id&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;ADK Session State + Firestore &lt;code&gt;line_bot_workspaces&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Structured Output&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Prompt says "Return ONLY the JSON" + manual code fence stripping&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;OutputSchema&lt;/code&gt; (ADK automatically injects &lt;code&gt;set_model_response&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Invocation Method&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;interactions.create(background=True)&lt;/code&gt; + polling every 3s&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;runner.Run()&lt;/code&gt; returns Go 1.23+ iterator, synchronous event consumption&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So this time, rather than "Python ADK → Go ADK," it's more accurate to say: &lt;strong&gt;Replacing the hosted Managed Agent with a self-controlled ADK Agent&lt;/strong&gt;. The benefit is that you no longer have to worry about mocked &lt;code&gt;gsutil&lt;/code&gt; in the sandbox, background polling modes, or Pre-GA uncertainty.&lt;/p&gt;




&lt;h1&gt;
  
  
  II. Setting Rules Before Rewriting: One Issue, Feasibility Verification First
&lt;/h1&gt;

&lt;p&gt;This project is much smaller than the previous &lt;code&gt;linebot-helper&lt;/code&gt; (Python version ~1,400 lines, Go version ~2,000 lines, excluding tests), so instead of splitting it into 11 issues, I opened one &lt;a href="https://github.com/kkdai/line-research-bot/issues/1" rel="noopener noreferrer"&gt;Tracking Issue #1&lt;/a&gt;. I wrote a checklist covering "Current Status Audit → Design → Feasibility Verification → Rewriting → Testing → Deployment → Verification," commenting on the issue as each section was completed.&lt;/p&gt;

&lt;p&gt;Before writing a single line of Go, I did two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;gcloud run services describe&lt;/code&gt; to pull the online service configuration&lt;/strong&gt; (Service Account, 11 environment variables, Secret Manager bindings, memory, timeout) to ensure a 1:1 migration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrote a 30-line probe program&lt;/strong&gt; to confirm if ADK for Go on Vertex AI could enable " &lt;strong&gt;Google Search + URL Context + OutputSchema all at once&lt;/strong&gt; ". This was the key to whether the design would work; the results are discussed in Section IV.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once confirmed feasible, the Go version (module path &lt;code&gt;github.com/kkdai/line-research-bot-go&lt;/code&gt;) was split into the following modules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cmd/server/ main: Dependency injection, HTTP server, graceful shutdown
internal/research/ ADK for Go: Three llmagents + runner + session state
  prompts/ plan / search_compare / write_report (go:embed)
internal/worker/ PLAN → SEARCH_COMPARE → WRITE_REPORT → publish, deepen, retry
internal/server/ /webhook, /tasks/run-research, /readyz
internal/state/ Firestore (users / reports compatible + new workspaces)
internal/publisher/ goldmark (GFM + footnote) → HTML → GCS (including snapshots)
internal/linebot/ line-bot-sdk-go v8 + HMAC verification
internal/tasks/ Cloud Tasks (OIDC)
internal/intent, flex/ Intent classification, Flex cards
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The overall data flow is as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dot"&gt;&lt;code&gt;&lt;span class="nv"&gt;flowchart&lt;/span&gt; &lt;span class="nv"&gt;LR&lt;/span&gt;
    &lt;span class="nv"&gt;LINE&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"LINE App"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;|&lt;/span&gt;&lt;span class="s2"&gt;"POST /webhook&amp;lt;br/&amp;gt;(HMAC)"&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;WH&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"internal/server&amp;lt;br/&amp;gt;Intent classification + Locking"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;WH&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;|&lt;/span&gt;&lt;span class="s2"&gt;"enqueue"&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;CT&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Cloud Tasks&amp;lt;br/&amp;gt;research-jobs"&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;CT&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;|&lt;/span&gt;&lt;span class="s2"&gt;"POST /tasks/run-research&amp;lt;br/&amp;gt;(OIDC token)"&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;WK&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"internal/worker"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;WK&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;|&lt;/span&gt;&lt;span class="s2"&gt;"seed / save"&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;FS&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Firestore&amp;lt;br/&amp;gt;line_bot_workspaces"&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;WK&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;|&lt;/span&gt;&lt;span class="s2"&gt;"runner.Run()"&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;ADK&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ADK for Go&amp;lt;br/&amp;gt;plan / search_compare / write_report"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;ADK&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;|&lt;/span&gt;&lt;span class="s2"&gt;"Google Search + URL Context"&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;VX&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Vertex AI&amp;lt;br/&amp;gt;gemini-3.8-flash"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;WK&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;|&lt;/span&gt;&lt;span class="s2"&gt;"goldmark → HTML"&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;GCS&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Cloud Storage"&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;WK&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;|&lt;/span&gt;&lt;span class="s2"&gt;"Push Progress + Flex Card"&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;LINE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  III. From Managed Agents to ADK for Go: Four Core Rewrites
&lt;/h1&gt;

&lt;h2&gt;
  
  
  1. From "REST Agent Resource Creation" to "In-code &lt;code&gt;llmagent.New&lt;/code&gt;"
&lt;/h2&gt;

&lt;p&gt;In the Python version, the Agent was created via REST API using a shell script. The system instructions for the three stages were concatenated into one large block, relying on a stage tag (&lt;code&gt;PLAN&lt;/code&gt; / &lt;code&gt;SEARCH_COMPARE&lt;/code&gt; / &lt;code&gt;WRITE_REPORT&lt;/code&gt;) in the first line of every input for the Agent to find the correct section:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"research-planner"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"base_agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"antigravity-preview-05-2026"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"code_execution"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"filesystem"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"google_search"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"url_context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"system_instruction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"...concatenated instructions for three stages..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Go version uses three distinct &lt;code&gt;llmagent&lt;/code&gt; instances (&lt;a href="https://github.com/kkdai/line-research-bot-go/blob/main/internal/research/agents.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/research/agents.go&lt;/code&gt;&lt;/a&gt;). Prompts are stored in &lt;a href="https://github.com/kkdai/line-research-bot-go/tree/main/internal/research/prompts" rel="noopener noreferrer"&gt;&lt;code&gt;internal/research/prompts/&lt;/code&gt;&lt;/a&gt; using &lt;code&gt;go:embed&lt;/code&gt;, and tools are only provided to the stages that need them (the planning stage doesn't need internet access):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;urlContextTool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;geminitool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"url_context"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Reads the content of web pages by URL."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;URLContext&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URLContext&lt;/span&gt;&lt;span class="p"&gt;{}})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;ag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;llmagent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;llmagent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"research_planner_search_compare"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Searches the web and cross-compares sources."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;// gemini.NewModel(ctx, cfg.GeminiModel, &amp;amp;genai.ClientConfig{Backend: genai.BackendVertexAI, ...})&lt;/span&gt;
    &lt;span class="n"&gt;Instruction&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;searchPrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Tools&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;geminitool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GoogleSearch&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;urlContextTool&lt;/span&gt;&lt;span class="p"&gt;()},&lt;/span&gt;
    &lt;span class="n"&gt;OutputSchema&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;searchSchema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;OutputKey&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"search_output"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;AppName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;appName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SessionService&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ADK for Go currently only has &lt;code&gt;geminitool.GoogleSearch&lt;/code&gt; built-in. &lt;strong&gt;URL Context must be wrapped manually using &lt;code&gt;geminitool.New&lt;/code&gt; with a &lt;code&gt;genai.Tool&lt;/code&gt;&lt;/strong&gt;, which I discovered by digging through the &lt;code&gt;tool/geminitool&lt;/code&gt; source code. Additionally, I kept the &lt;code&gt;AGENT_ID&lt;/code&gt; environment variable but repurposed it as the ADK &lt;code&gt;AppName&lt;/code&gt; and agent name prefix, so the online environment variables didn't need to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. From &lt;code&gt;background=True&lt;/code&gt; + Polling to &lt;code&gt;runner.Run()&lt;/code&gt; Iterator
&lt;/h2&gt;

&lt;p&gt;The most tedious part of the Python version was this: Managed Agents didn't support synchronous mode, so you had to &lt;code&gt;create&lt;/code&gt; and then &lt;code&gt;get&lt;/code&gt; every 3 seconds until &lt;code&gt;completed&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interactions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_agent_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;instruction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;env_arg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;background&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;polled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interactions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;include_input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;polled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;AgentResponse&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_poll_interval&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ADK for Go's &lt;code&gt;runner.Run()&lt;/code&gt; directly returns a Go 1.23 &lt;code&gt;iter.Seq2[*session.Event, error]&lt;/code&gt;. The Agent runs in its own process, events flow out one by one, and timeout is handled by &lt;code&gt;context.WithTimeout&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewContentFromText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RoleUser&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RunConfig&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;invocationID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%w: %s: %v"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ErrAgent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;invocationID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvocationID&lt;/span&gt;
    &lt;span class="c"&gt;// Use final response text as fallback; read formal result from OutputKey&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I changed the &lt;code&gt;last_interaction_id&lt;/code&gt; stored in Firestore to ADK's &lt;code&gt;InvocationID&lt;/code&gt;. The field name remains the same, making it just as useful for log cross-referencing.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Sandbox &lt;code&gt;/workspace&lt;/code&gt; Files → ADK Session State + Firestore (The Most Critical Step)
&lt;/h2&gt;

&lt;p&gt;The biggest selling point of Managed Agents was the cross-turn persistent sandbox filesystem. ADK has no sandbox, so when a user asks to "go deeper into Chapter 2," where does the Agent find the previous sources and report?&lt;/p&gt;

&lt;p&gt;The answer is &lt;strong&gt;ADK Session State + Instruction Templates&lt;/strong&gt;. ADK's &lt;code&gt;Instruction&lt;/code&gt; replaces &lt;code&gt;{key}&lt;/code&gt; with the value from session state, and &lt;code&gt;{key?}&lt;/code&gt; means "it's okay if it doesn't exist":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Cross-compared sources (JSON)&lt;/span&gt;
{sources}

&lt;span class="gu"&gt;## Current report Markdown (empty for a new report)&lt;/span&gt;
{report_md?}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mapping becomes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sandbox File (Python)&lt;/th&gt;
&lt;th&gt;ADK Session State (Go)&lt;/th&gt;
&lt;th&gt;Persistence Location&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/workspace/plan.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{plan}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Firestore &lt;code&gt;line_bot_workspaces/{userId}.plan&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/workspace/sources.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{sources}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;...sources&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/workspace/report.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{report_md?}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;...report_md&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;However, Cloud Run scales horizontally and can scale to 0; items in &lt;code&gt;session.InMemoryService()&lt;/code&gt; can disappear at any time. My approach: &lt;strong&gt;Every time a stage is executed, create a new ADK session, seed the initial state from the Firestore workspace, write the results back to Firestore after running, and then delete the session.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;created&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sessions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CreateRequest&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;AppName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;appName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;UserID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;KeySources&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SourcesJSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;KeyReportMD&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;in&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReportMD&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the Cloud Run instance completely stateless; a deepening request hours later will work on any instance. Added benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Republishing" no longer requires the LLM&lt;/strong&gt;: The Python version's &lt;code&gt;retry_publish&lt;/code&gt; had to ask the Agent to read &lt;code&gt;report.md&lt;/code&gt; from the sandbox and return it; the Go version simply takes the Markdown from Firestore and re-renders/uploads it, saving a model call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handling sandbox expiration remains consistent&lt;/strong&gt;: Reports created in the old Managed Agents version cannot access their sandbox files in the Go version. I handle this as the original "environment expired" path: push a message saying "Previous research files have expired, restarting research from scratch 🔄" and rerun.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;⚠️ &lt;strong&gt;A small ADK template pitfall&lt;/strong&gt;: ADK treats all strings matching &lt;code&gt;{identifier}&lt;/code&gt; in the instruction as state variables and errors out if they aren't found. The original prompts had many JSON examples; these must be cleaned up. I wrote a unit test to scan the three prompts and only allow &lt;code&gt;{plan}&lt;/code&gt;, &lt;code&gt;{sources}&lt;/code&gt;, and &lt;code&gt;{report_md?}&lt;/code&gt; placeholders.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. "Return ONLY the JSON" → &lt;code&gt;OutputSchema&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;To get JSON in the Python version, the prompt ended with "Return ONLY the JSON. No prose, no markdown fences.", and the code required a &lt;code&gt;_parse_agent_json&lt;/code&gt; function to try the raw text,&lt;br&gt;
&lt;br&gt;
 ```&lt;code&gt;json&lt;/code&gt; fence content, or the first &lt;code&gt;{&lt;/code&gt; to the last &lt;code&gt;}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;ADK for Go's &lt;code&gt;OutputSchema&lt;/code&gt; is much cleaner:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;When OutputSchema is set, the framework transparently injects a &lt;code&gt;set_model_response&lt;/code&gt; tool so the model can still invoke other tools (function tools, RAG, agent transfer, etc.) while producing structured output.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In other words, &lt;strong&gt;ADK automatically injects a &lt;code&gt;set_model_response&lt;/code&gt; tool&lt;/strong&gt;. The model can use Google Search / URL Context to find data first, then call this tool to submit results matching the schema, which are then written back to session state via &lt;code&gt;OutputKey&lt;/code&gt;. I defined the output structures for all three stages using &lt;code&gt;genai.Schema&lt;/code&gt; in &lt;a href="https://github.com/kkdai/line-research-bot-go/blob/main/internal/research/schema.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/research/schema.go&lt;/code&gt;&lt;/a&gt;, keeping the original JSON parser as a fallback.&lt;/p&gt;




&lt;h1&gt;
  
  
  IV. Model Selection: Not every Gemini can do "Search + Read Web + Structured Output" simultaneously
&lt;/h1&gt;

&lt;p&gt;The probe program mentioned earlier was for this exact moment. Results from running the same program in my GCP project at location &lt;code&gt;global&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Google Search + URL Context + OutputSchema&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-2.5-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;&lt;code&gt;400 INVALID_ARGUMENT: controlled generation is not supported with URL Context tool&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-3-flash-preview&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;Used in the first live version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-3.1-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;404 NOT_FOUND&lt;/code&gt; (This project doesn't have this model)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemini-3.8-flash&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Current Default&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;gemini-3.8-pro&lt;/code&gt; / &lt;code&gt;-flash-preview&lt;/code&gt; / &lt;code&gt;-pro-preview&lt;/code&gt; / &lt;code&gt;-flash-lite&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;All &lt;code&gt;404 NOT_FOUND&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two takeaways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The model list must be verified by actually calling them from your project&lt;/strong&gt;. Just because a model name exists in documentation doesn't mean your project or location has access to it; &lt;code&gt;404 Publisher model ... was not found or your project does not have access to it&lt;/code&gt; is a common response.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model names must be environment variables&lt;/strong&gt;. The Go version adds an optional &lt;code&gt;GEMINI_MODEL&lt;/code&gt; variable; if not set, it uses the code default. This means the 11 original environment variables on the live service didn't need to change.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  A bug caught when switching to gemini-3.8-flash
&lt;/h3&gt;

&lt;p&gt;After upgrading to &lt;code&gt;gemini-3.8-flash&lt;/code&gt;, reports became significantly longer and deeper (approx. 5k → 11–12k characters), but integration tests caught a subtle issue: when asked to deepen a non-existent "Chapter 99," it returned:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
plaintext
error="chapter_not_found_chapter_not_found" available_chapters=[]


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The old code used &lt;code&gt;== "chapter_not_found"&lt;/code&gt; for validation; this value didn't match, so it would &lt;strong&gt;publish an empty &lt;code&gt;report_md&lt;/code&gt; as a new version&lt;/strong&gt;. I fixed this in three layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Changed &lt;code&gt;error&lt;/code&gt; in the schema to an &lt;strong&gt;enum&lt;/strong&gt; (only allowing &lt;code&gt;chapter_not_found&lt;/code&gt;) to restrict the model at the source.&lt;/li&gt;
&lt;li&gt;The worker now uses &lt;code&gt;strings.Contains&lt;/code&gt; for loose matching; if &lt;code&gt;available_chapters&lt;/code&gt; is empty, it &lt;strong&gt;extracts chapters from the &lt;code&gt;##&lt;/code&gt; headers in the report itself&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;report_md&lt;/code&gt; is empty, it never publishes and instead pushes "Deepening failed, please try again later."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After adding the enum and rerunning, &lt;code&gt;gemini-3.8-flash&lt;/code&gt; correctly returned &lt;code&gt;chapter_not_found&lt;/code&gt; and the full four chapters. &lt;strong&gt;Switching models isn't just about changing a string; edge cases for structured output must be re-verified with integration tests.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To be honest about the cost: the total time for the three stages increased from about 60 seconds to 150–190 seconds. It's still within timeout, but users have to wait a bit longer.&lt;/p&gt;




&lt;h1&gt;
  
  
  V. gcloud and Cloud Run Deployment: Ten Things to Watch Out For
&lt;/h1&gt;

&lt;p&gt;After finishing the code, passing &lt;code&gt;go test ./...&lt;/code&gt;, and running integration tests against the real Vertex AI, the real challenge began. This is a bot &lt;strong&gt;serving real LINE users live&lt;/strong&gt;. Below are the points I hit (or narrowly avoided) during deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Note 1: &lt;code&gt;describe&lt;/code&gt; the online settings first; don't trust the &lt;code&gt;deploy.sh&lt;/code&gt; in the repo
&lt;/h2&gt;

&lt;p&gt;I initially thought I could just follow the &lt;code&gt;deploy/deploy.sh&lt;/code&gt; in the repo, but a &lt;code&gt;describe&lt;/code&gt; of the live service revealed &lt;strong&gt;the two were already different&lt;/strong&gt; (I had tuned them online during previous debugging):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;deploy.sh&lt;/code&gt; in repo&lt;/th&gt;
&lt;th&gt;Actual Live Service&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GCS_BUCKET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;line-reports&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A different bucket&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1Gi&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2Gi&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Timeout&lt;/td&gt;
&lt;td&gt;&lt;code&gt;600&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;3600&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--min-instances&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0&lt;/code&gt; (not set)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If I had just run &lt;code&gt;deploy.sh&lt;/code&gt;, reports would have gone to the wrong bucket, and long tasks would have been killed at 600 seconds. &lt;strong&gt;When rewriting a service, the "actual online state" is the specification&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
shell
gcloud run services describe line-research-bot --region=asia-east1 --format=json \
  | jq '{sa: .spec.template.spec.serviceAccountName,
         env: [.spec.template.spec.containers[0].env[].name],
         resources: .spec.template.spec.containers[0].resources,
         timeout: .spec.template.spec.timeoutSeconds}'


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Note 2: To "reuse the same environment variables," the simplest way is to &lt;strong&gt;pass nothing&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The previous &lt;code&gt;linebot-helper&lt;/code&gt; was deployed as a &lt;strong&gt;new service&lt;/strong&gt;, so environment variables had to be converted to an &lt;code&gt;--env-vars-file&lt;/code&gt;. This time, deploying back to the &lt;strong&gt;same service&lt;/strong&gt;, there's a simpler way:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;gcloud run deploy --source=.&lt;/code&gt; &lt;strong&gt;without&lt;/strong&gt; &lt;code&gt;--set-env-vars&lt;/code&gt; / &lt;code&gt;--update-secrets&lt;/code&gt; will cause the new revision to fully inherit the environment variables and Secret Manager bindings from the previous revision.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This ensures the LINE Channel Secret / Access Token never appear in the command line or terminal output. After deploying, I described it again to confirm the list of 11 environment variables remained the same.&lt;/p&gt;
&lt;h2&gt;
  
  
  Note 3: Use &lt;code&gt;--no-traffic --tag&lt;/code&gt; for Canary; &lt;strong&gt;tags must be at least 3 characters&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Since it's the same service, I didn't want all LINE users hitting the new version immediately. The Cloud Run way:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
shell
gcloud run deploy line-research-bot --region=asia-east1 --source=. \
  --no-traffic --tag=golang


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The new revision gets 0% traffic but has a dedicated tag URL for direct testing. I hit a silly snag here: I initially tried &lt;code&gt;--tag=go&lt;/code&gt;, which was blocked:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
plaintext
ERROR: (gcloud.run.deploy) service.spec.traffic[1].tag: must be at least 3 characters long.


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;After performing smoke tests (listed below) on the tag URL, I switched traffic and recorded the previous revision name as a rollback point:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
shell
gcloud run services update-traffic line-research-bot --region=asia-east1 \
  --to-revisions=&amp;lt;new revision&amp;gt;=100

# Rollback is just one line
gcloud run services update-traffic line-research-bot --region=asia-east1 \
  --to-revisions=&amp;lt;previous revision&amp;gt;=100


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Smoke Test&lt;/th&gt;
&lt;th&gt;Expected&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /readyz&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;POST /webhook&lt;/code&gt; wrong signature&lt;/td&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;POST /tasks/run-research&lt;/code&gt; no OIDC token&lt;/td&gt;
&lt;td&gt;401&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model field in startup logs&lt;/td&gt;
&lt;td&gt;Expected model name&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Note 4: Cloud Tasks default dispatch deadline is only 10 minutes
&lt;/h2&gt;

&lt;p&gt;This was a potential issue I found while reviewing the Python code during the rewrite. Research tasks are executed via Cloud Tasks calling back to the &lt;code&gt;/tasks/run-research&lt;/code&gt; endpoint. While Cloud Run's timeout is 3600 seconds, &lt;strong&gt;Cloud Tasks' default dispatch deadline for HTTP targets is only 10 minutes&lt;/strong&gt;. If there's no response after 10 minutes, Cloud Tasks marks it as failed and &lt;strong&gt;retries&lt;/strong&gt; (queue setting &lt;code&gt;max-attempts=3&lt;/code&gt;), causing the research to rerun and doubling the Gemini bill.&lt;/p&gt;

&lt;p&gt;In the Go version, I set the task creation in &lt;a href="https://github.com/kkdai/line-research-bot-go/blob/main/internal/tasks/dispatcher.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/tasks/dispatcher.go&lt;/code&gt;&lt;/a&gt; to the maximum 30 minutes:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
go
Task: &amp;amp;cloudtaskspb.Task{
    DispatchDeadline: durationpb.New(30 * time.Minute),
    MessageType: &amp;amp;cloudtaskspb.Task_HttpRequest{HttpRequest: &amp;amp;cloudtaskspb.HttpRequest{ /* ... */ }},
},


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Additionally, the Python version's user lock TTL was only 300 seconds, meaning a user could send another query and grab the lock before the task finished, leading to two tasks modifying the same workspace. The Go version sets the &lt;strong&gt;lock TTL to 35 minutes (greater than the dispatch deadline)&lt;/strong&gt; and actively releases it when the task finishes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Note 5: Under &lt;code&gt;--allow-unauthenticated&lt;/code&gt;, Cloud Tasks endpoints must verify OIDC themselves
&lt;/h2&gt;

&lt;p&gt;The LINE Webhook must be accessible from outside, so the entire service is &lt;code&gt;--allow-unauthenticated&lt;/code&gt;. &lt;code&gt;/webhook&lt;/code&gt; is protected by &lt;code&gt;X-Line-Signature&lt;/code&gt;, but the Python version's &lt;code&gt;/tasks/run-research&lt;/code&gt; &lt;strong&gt;had no verification at all&lt;/strong&gt;: anyone who knew the URL could POST a JSON and trigger a full research task that burns Gemini credits.&lt;/p&gt;

&lt;p&gt;Cloud Tasks naturally carries an OIDC token when creating a task; the Python receiver just didn't verify it. The Go version adds this in &lt;a href="https://github.com/kkdai/line-research-bot-go/blob/main/cmd/server/main.go" rel="noopener noreferrer"&gt;&lt;code&gt;cmd/server/main.go&lt;/code&gt;&lt;/a&gt; using &lt;code&gt;google.golang.org/api/idtoken&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
go
func oidcVerifier(audience, saEmail string) server.TokenVerifier {
    return func(ctx context.Context, bearer string) error {
        p, err := idtoken.Validate(ctx, bearer, audience) // audience = service URL + /tasks/run-research
        if err != nil {
            return err
        }
        if email, _ := p.Claims["email"].(string); email != saEmail {
            return fmt.Errorf("unexpected token email %q", email)
        }
        return nil
    }
}


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Details to align:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Audience must match on both sides&lt;/strong&gt;: Explicitly specify &lt;code&gt;OidcToken.Audience&lt;/code&gt; when creating the task, and verify the same value on the receiving end (I use &lt;code&gt;CLOUD_RUN_SERVICE_URL + "/tasks/run-research"&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service Account must be able to actAs itself&lt;/strong&gt;: Cloud Tasks needs to sign the OIDC token using &lt;code&gt;line-bot-sa&lt;/code&gt;. The &lt;code&gt;line-bot-sa&lt;/code&gt; needs &lt;code&gt;roles/iam.serviceAccountUser&lt;/code&gt; on itself, plus &lt;code&gt;roles/cloudtasks.enqueuer&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CLOUD_RUN_SERVICE_URL&lt;/code&gt; is a chicken-and-egg problem&lt;/strong&gt;: You don't know the URL before the first deployment. The &lt;code&gt;deploy.sh&lt;/code&gt; strategy is to deploy first, then &lt;code&gt;gcloud run services update --update-env-vars=CLOUD_RUN_SERVICE_URL=...&lt;/code&gt;. When deploying back to the same service, this value is inherited and doesn't need resetting.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Note 6: &lt;code&gt;--no-cpu-throttling&lt;/code&gt; is &lt;strong&gt;NOT&lt;/strong&gt; needed this time; min-instances can be 0
&lt;/h2&gt;

&lt;p&gt;In the previous &lt;code&gt;linebot-helper&lt;/code&gt; post, I spent a whole section explaining how background goroutines get frozen without &lt;code&gt;--no-cpu-throttling&lt;/code&gt;. This architecture is the opposite:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/webhook&lt;/code&gt; only does signature verification, classification, locking, and enqueueing. &lt;strong&gt;It returns 200 only after finishing synchronously&lt;/strong&gt;, with no background goroutines.&lt;/li&gt;
&lt;li&gt;The truly time-consuming research runs within the &lt;strong&gt;HTTP request sent by Cloud Tasks&lt;/strong&gt;. The request stays open from start to finish, so Cloud Run won't throttle the CPU or scale down the instance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thus, the default "allocate CPU only during request processing" is perfectly sufficient, and &lt;code&gt;--min-instances&lt;/code&gt; can stay at 0. Go cold starts in less than 1 second, and the LINE reply token is valid for about 1 minute, which is plenty of time. &lt;strong&gt;Whether to use &lt;code&gt;--no-cpu-throttling&lt;/code&gt; depends on whether the work is done inside or outside the request.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The only thing to watch is releasing the lock: the &lt;code&gt;r.Context()&lt;/code&gt; might already be canceled when the handler ends, so I use &lt;code&gt;context.WithoutCancel(ctx)&lt;/code&gt; to release the Firestore lock, ensuring it always gets released.&lt;/p&gt;

&lt;h2&gt;
  
  
  Note 7: Deployment is possible without local Docker; &lt;code&gt;--source=.&lt;/code&gt; uses Cloud Build
&lt;/h2&gt;

&lt;p&gt;I don't have Docker installed on my laptop. &lt;code&gt;gcloud run deploy --source=.&lt;/code&gt; sees a &lt;code&gt;Dockerfile&lt;/code&gt; in the directory and hands it to Cloud Build. Since this Bot doesn't need Chromium or ffmpeg, I can safely use the minimal distroless image:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
docker
FROM golang:1.27 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY cmd/ ./cmd/
COPY internal/ ./internal/
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/server ./cmd/server

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /out/server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The Go version has no external executable dependencies, and prompts are compiled into the binary using &lt;code&gt;go:embed&lt;/code&gt;, so the final image is just a static executable. No extra data files need to be copied.&lt;/p&gt;

&lt;h2&gt;
  
  
  Note 8: Same service, no need to change LINE Webhook URL, but verify with API
&lt;/h2&gt;

&lt;p&gt;After deploying, I thought about going to the LINE Developers Console to change the Webhook URL, then realized: &lt;strong&gt;The new version is deployed to the same Cloud Run service; the URL hasn't changed&lt;/strong&gt;. Instead of visually comparing in the Console, it's better to ask LINE using the Channel Access Token:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
shell
TOKEN=$(gcloud secrets versions access latest --secret=LINE_CHANNEL_ACCESS_TOKEN)

# Current webhook setting and status
curl -s -H "Authorization: Bearer $TOKEN" \
  https://api.line.me/v2/bot/channel/webhook/endpoint

# Ask LINE to actually hit the webhook
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"endpoint":"&amp;lt;YOUR_SERVICE_URL&amp;gt;/webhook"}' \
  https://api.line.me/v2/bot/channel/webhook/test


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;In my script, I directly compare if &lt;code&gt;endpoint&lt;/code&gt; equals "Service URL + &lt;code&gt;/webhook&lt;/code&gt;", printing only &lt;code&gt;MATCH&lt;/code&gt; or &lt;code&gt;DIFFERENT&lt;/code&gt; so the URL itself doesn't appear in the terminal. The test API returning &lt;code&gt;{"success":true,"statusCode":200}&lt;/code&gt; means LINE traffic is already hitting the Go version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Note 9: When E2E testing with a fake user, don't let LINE push failures trigger a full pipeline rerun
&lt;/h2&gt;

&lt;p&gt;Smoke tests only prove the service is alive. I also wanted to verify the entire path: &lt;strong&gt;webhook → Cloud Tasks (OIDC) → ADK → Firestore → GCS&lt;/strong&gt; in the production environment without using a real user. The trick is to use the Channel Secret from Secret Manager to correctly sign a message for a &lt;strong&gt;fake userId&lt;/strong&gt; and hit &lt;code&gt;/webhook&lt;/code&gt; directly:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
shell
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -binary | base64)
curl -X POST -H "X-Line-Signature: $SIG" -d "$BODY" "$URL/webhook"


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;LINE's reply and push will inevitably return 400 because the userId/replyToken is fake. This is expected, but it highlighted a design issue: &lt;strong&gt;If the final Flex card push fails and the worker returns an error, the handler returns 500, causing Cloud Tasks to rerun the entire 3-minute, paid research process&lt;/strong&gt;. So, the Go version logs all LINE push failures but doesn't return an error; only infrastructure errors like Firestore failures trigger a Cloud Tasks retry.&lt;/p&gt;

&lt;p&gt;The E2E test in production took about 33 seconds. Firestore reports/workspaces were correctly written, the lock was released, and the &lt;code&gt;index.html&lt;/code&gt; on GCS had the correct title and version banner. Remember to clean up the fake user's Firestore documents and GCS objects after testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Note 10: Upgrade to the latest Go patch and use &lt;code&gt;govulncheck&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;My local machine was on Go 1.26.1 during the rewrite, but the latest version was already &lt;strong&gt;1.27.3&lt;/strong&gt;. Upgrading is easy; &lt;code&gt;go get&lt;/code&gt; modifies &lt;code&gt;go.mod&lt;/code&gt; and automatically downloads the toolchain:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
shell
go get go@1.27.3 toolchain@none
go run golang.org/x/vuln/cmd/govulncheck@latest ./...


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remember to update the &lt;code&gt;Dockerfile&lt;/code&gt; build image from &lt;code&gt;golang:1.26&lt;/code&gt; to &lt;code&gt;golang:1.27&lt;/code&gt;. &lt;code&gt;govulncheck&lt;/code&gt; reported "&lt;strong&gt;Vulnerabilities affecting your code: 0&lt;/strong&gt;", though there were 3 vulnerabilities in dependencies that weren't being called. Two in &lt;code&gt;golang.org/x/net&lt;/code&gt; were fixed by upgrading to v0.62.0; the one in &lt;code&gt;golang.org/x/crypto&lt;/code&gt; hasn't had an official fix released yet. &lt;strong&gt;&lt;code&gt;govulncheck&lt;/code&gt; distinguishes between "called" and "not called," which is much more practical than just scanning &lt;code&gt;go.mod&lt;/code&gt; version numbers.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  VI. Things Fixed Along the Way: Rewriting is a Great Time to Re-examine Design
&lt;/h1&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Python Version&lt;/th&gt;
&lt;th&gt;Go Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;/tasks/run-research&lt;/code&gt; Verification&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Cloud Tasks OIDC (audience + SA email)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Tasks Dispatch Deadline&lt;/td&gt;
&lt;td&gt;Default 10 mins (long tasks rerun)&lt;/td&gt;
&lt;td&gt;30 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;User Lock TTL&lt;/td&gt;
&lt;td&gt;300 seconds&lt;/td&gt;
&lt;td&gt;35 minutes, active release on finish&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;retry_publish&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Asks Agent to read &lt;code&gt;report.md&lt;/code&gt; again&lt;/td&gt;
&lt;td&gt;Uses Markdown from Firestore directly (no LLM)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deepen Version Number&lt;/td&gt;
&lt;td&gt;Relies on model's &lt;code&gt;new_version&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Calculated by code (previous + 1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Report HTML&lt;/td&gt;
&lt;td&gt;Raw HTML from model rendered; &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt; not escaped&lt;/td&gt;
&lt;td&gt;goldmark doesn't render raw HTML by default; title escaped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LINE Push Failure&lt;/td&gt;
&lt;td&gt;Exception bubbles up → 500 → Cloud Tasks rerun&lt;/td&gt;
&lt;td&gt;Logged only; doesn't trigger rerun&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution Environment&lt;/td&gt;
&lt;td&gt;Python 3.11 slim&lt;/td&gt;
&lt;td&gt;Go 1.27.3 distroless/static, nonroot&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Regarding testing, the Go version has two layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unit Tests&lt;/strong&gt; (&lt;code&gt;go test ./...&lt;/code&gt;, no GCP needed): Worker uses fake store / agents / LINE / publisher. Covers new research, stage failures, &lt;code&gt;retry_write&lt;/code&gt;, &lt;code&gt;retry_publish&lt;/code&gt;, deepening, &lt;code&gt;chapter_not_found&lt;/code&gt; (including model hallucination cases), and missing workspaces. Server covers signatures, locks, enqueue failure rollback, OIDC, recall / reset / small talk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration Tests&lt;/strong&gt; (&lt;code&gt;go test -tags integration&lt;/code&gt;): Hits the real Vertex AI to run through all three stages, then tests deepening a non-existent chapter. When switching models or upgrading ADK, this is the only way to catch the model behavior differences mentioned earlier.&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  VII. One Last Security Reminder: Don't let Cloud Run URLs appear in public Issues
&lt;/h1&gt;

&lt;p&gt;Both repos are public, as are the Issues and PRs. I deliberately did a few things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The first line of the Issue states: "This issue is a public record and &lt;strong&gt;will not&lt;/strong&gt; post any Cloud Run service URLs, Secret values, or internal identifiers."&lt;/li&gt;
&lt;li&gt;All &lt;code&gt;gcloud&lt;/code&gt; command outputs that would print URLs were masked using &lt;code&gt;sed -E 's#https?://[^]+#&amp;lt;url&amp;gt;#g'&lt;/code&gt;, or the URL was stored in a shell variable and only the comparison result was printed.&lt;/li&gt;
&lt;li&gt;Before sending a PR, I ran &lt;code&gt;git diff main | grep run.app&lt;/code&gt; to ensure no real URLs were in the new code or documentation (only a fake &lt;code&gt;https://x.run.app&lt;/code&gt; from a deleted Python test was caught).&lt;/li&gt;
&lt;li&gt;Before opening the new public repo, I scanned the &lt;strong&gt;entire git history&lt;/strong&gt; (&lt;code&gt;git log --all -p&lt;/code&gt;) to ensure the real service hostname never appeared once before pushing with &lt;code&gt;gh repo create --public --source=. --push&lt;/code&gt;. If a repo is public, everything in old commits is also public.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;/webhook&lt;/code&gt; has LINE signatures and &lt;code&gt;/tasks/run-research&lt;/code&gt; has OIDC, so even if the URL leaks, unauthorized access is blocked. However, a public &lt;code&gt;.run.app&lt;/code&gt; URL still attracts scanners and meaningless traffic. &lt;strong&gt;If you don't have to make it public, don't.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  VIII. Python Managed Agents → Go ADK Migration Checklist
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt; &lt;strong&gt;1. Audit online settings first&lt;/strong&gt;: Use &lt;code&gt;gcloud run services describe&lt;/code&gt; to get the SA, env vars, Secret bindings, memory, and timeout. Don't trust outdated deploy scripts in the repo.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;2. Feasibility verification for the model&lt;/strong&gt;: Call it from your project and location to confirm the model exists and supports your tool combination (e.g., Google Search + URL Context + OutputSchema).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;3. Sandbox Files → Session State&lt;/strong&gt;: Map the cross-turn files from the Managed Agents sandbox to ADK &lt;code&gt;{state}&lt;/code&gt; placeholders and persist them to external storage like Firestore to keep Cloud Run stateless.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;4. Check curly braces in instructions&lt;/strong&gt;: ADK treats &lt;code&gt;{identifier}&lt;/code&gt; as a state variable. Clean up JSON examples when moving prompts, and ideally use a test to catch them.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;5. Use &lt;code&gt;OutputSchema&lt;/code&gt; instead of prompt constraints&lt;/strong&gt;, but keep a loose JSON parser as a fallback. Use enums for error code fields.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;6. Use &lt;code&gt;--no-traffic --tag&lt;/code&gt; (tag ≥ 3 chars) for the same service&lt;/strong&gt;. Only &lt;code&gt;update-traffic&lt;/code&gt; after smoke tests pass, and record the revision for rollback.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;7. Omitting &lt;code&gt;--set-env-vars&lt;/code&gt; inherits env vars and Secrets from the previous version&lt;/strong&gt;. Describe again after deployment to confirm.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;8. Cloud Tasks&lt;/strong&gt;: Set dispatch deadline to the required length (up to 30 mins). Lock TTL must be longer than this. The receiver must verify the OIDC token.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;9. &lt;code&gt;--no-cpu-throttling&lt;/code&gt; is only needed if work happens outside the request&lt;/strong&gt;. If work is done within the Cloud Tasks request, min-instances 0 is sufficient.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;10. Verify webhook with LINE API&lt;/strong&gt;: &lt;code&gt;GET /v2/bot/channel/webhook/endpoint&lt;/code&gt; + &lt;code&gt;POST .../webhook/test&lt;/code&gt; is more reliable than checking the Console.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;11. External notification failures shouldn't trigger a full pipeline retry&lt;/strong&gt;. Only return 5xx for infrastructure errors.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;12. Upgrade to the latest Go patch and run &lt;code&gt;govulncheck&lt;/code&gt;&lt;/strong&gt;. Ensure no real service URLs remain in public repo Issues / PRs / READMEs.&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Summary and Open Source Project Links
&lt;/h1&gt;

&lt;p&gt;Migrating &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-research-bot" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/line-research-bot&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; from Python + Managed Agents to the Go + ADK for Go version &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-research-bot-go" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/line-research-bot-go&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; provided several key takeaways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Managed Agents and ADK are different trade-offs&lt;/strong&gt;: The former manages the sandbox and filesystem for you; the latter lets the Agent run within your own code. When the sandbox's value is just "remembering three files," ADK's Session State + Firestore is simpler and easier to test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;OutputSchema&lt;/code&gt; + Built-in Tools is a powerful combo in ADK for Go&lt;/strong&gt;: &lt;code&gt;set_model_response&lt;/code&gt; allows the model to search the web first and then submit strongly-typed results, provided your chosen model supports it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--no-traffic --tag&lt;/code&gt; is the safest way to switch when deploying to the same service&lt;/strong&gt;: Env vars, Secrets, and Webhook URLs don't need to change, and rollback is a single line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rewriting is the best time to re-examine design&lt;/strong&gt;: The OIDC verification, 30-minute dispatch deadline, and push failure handling added this time were all potential issues in the Python version.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The full Go code is at &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-research-bot-go" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/line-research-bot-go&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;. The step-by-step record of the rewrite is in &lt;a href="https://github.com/kkdai/line-research-bot/issues/1" rel="noopener noreferrer"&gt;Issue #1&lt;/a&gt;. Feel free to reference and discuss!&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/gde/geminiagent-google-managed-agents-api-4e43"&gt;Comprehensive Analysis of Google Managed Agents API (Python version of this Bot)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kkdai/line-research-bot-go" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/line-research-bot-go&lt;/code&gt;: Full Go code for this article&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/google/adk-go" rel="noopener noreferrer"&gt;ADK for Go on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://google.github.io/adk-docs/" rel="noopener noreferrer"&gt;Official ADK Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/tasks/docs/reference/rest/v2/projects.locations.queues.tasks#Task.FIELDS.dispatch_deadline" rel="noopener noreferrer"&gt;Cloud Tasks: Dispatch deadline for HTTP targets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/run/docs/rollouts-rollbacks-traffic-migration" rel="noopener noreferrer"&gt;Cloud Run: Testing new versions with revision tags&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>cloud</category>
      <category>go</category>
    </item>
    <item>
      <title>[AI in Practice] Migrating a LINE Research Bot from Go ADK to DeepSeek Harness (DSH): Architecture, Mapping, and Pitfalls</title>
      <dc:creator>Evan Lin</dc:creator>
      <pubDate>Tue, 03 Nov 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/gde/ai-in-practice-migrating-a-line-research-bot-from-go-adk-to-deepseek-harness-dsh-architecture-208e</link>
      <guid>https://dev.to/gde/ai-in-practice-migrating-a-line-research-bot-from-go-adk-to-deepseek-harness-dsh-architecture-208e</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNqNVMtu2zAQ_JWFTk5b1Sl6KGA0AeSXkkaJCstoDlFh0OTaIiyRAknVMZL8e5eSW9fOodFJWi1nhjNLPgVcCwwGEKxKveUFMw6SWa6AHtss14bVBQwn03Q2eciDIa60wQHEGt5DNL4B-vQfPSvVukTgWjkmFZqzPPjZgfhHSIPcSa1gPjxUh4urNL0h1OT6bgL3uCy03kAfRqVuBMyZ3VhCgTC8pNZ737jVZoMGerE-xve_fd9zHphGEf3HWaN6Z1-Xpn8pVVgbzdES2DN1kmpC8trLsmJrVA7y5vxcfIHPbX_auLpxGS-wYm0ho6VeeuaYwxNaD9PxxlqTARkywwvypjHlwpuBj66jjW-J9QcaqkB03QLHWEkl_yKiErk6MT6aziczL3fl0Axgvqsx40bWjijGiHWGuIErZhRphB6ra6pbKZCzNyUQvT2B6DiBg5JjnuiQhLBFaMUm5KUkk9sdWyekhm9ZehfOvo-8MdFinF09tM1A0TlZod_9PrFuEWF4oypW0u5q5njR1ZtlJd3iHend4nJh0HbmO61LeyKKSPaqIuUKo2vJof_rU78iEpoB20lJEm-FdJgkt39sPAFKkr0b_8_zlPlfkYN2wfrVzIwOI-MpXg1GdxDh4sIDpqrcgSsQuiku2Y6SoROsaEMtfptqH6aUvXV0buk9HmXH-UKjuiWiJfXzlqvgAwQVmopJQTfDU0AkVXtHCFyxpnTBy8tvgn5UHg%3D%3D%3Ftype%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNqNVMtu2zAQ_JWFTk5b1Sl6KGA0AeSXkkaJCstoDlFh0OTaIiyRAknVMZL8e5eSW9fOodFJWi1nhjNLPgVcCwwGEKxKveUFMw6SWa6AHtss14bVBQwn03Q2eciDIa60wQHEGt5DNL4B-vQfPSvVukTgWjkmFZqzPPjZgfhHSIPcSa1gPjxUh4urNL0h1OT6bgL3uCy03kAfRqVuBMyZ3VhCgTC8pNZ737jVZoMGerE-xve_fd9zHphGEf3HWaN6Z1-Xpn8pVVgbzdES2DN1kmpC8trLsmJrVA7y5vxcfIHPbX_auLpxGS-wYm0ho6VeeuaYwxNaD9PxxlqTARkywwvypjHlwpuBj66jjW-J9QcaqkB03QLHWEkl_yKiErk6MT6aziczL3fl0Axgvqsx40bWjijGiHWGuIErZhRphB6ra6pbKZCzNyUQvT2B6DiBg5JjnuiQhLBFaMUm5KUkk9sdWyekhm9ZehfOvo-8MdFinF09tM1A0TlZod_9PrFuEWF4oypW0u5q5njR1ZtlJd3iHend4nJh0HbmO61LeyKKSPaqIuUKo2vJof_rU78iEpoB20lJEm-FdJgkt39sPAFKkr0b_8_zlPlfkYN2wfrVzIwOI-MpXg1GdxDh4sIDpqrcgSsQuiku2Y6SoROsaEMtfptqH6aUvXV0buk9HmXH-UKjuiWiJfXzlqvgAwQVmopJQTfDU0AkVXtHCFyxpnTBy8tvgn5UHg%3D%3D%3Ftype%3Dpng" alt="Before / after architecture: Go ADK vs. DeepSeek Harness" width="1027" height="859"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Before (ADK for Go)&lt;/th&gt;
&lt;th&gt;After (DeepSeek Harness)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Where the agent runs&lt;/td&gt;
&lt;td&gt;Inside the same Go process&lt;/td&gt;
&lt;td&gt;One &lt;code&gt;dsh&lt;/code&gt; subprocess per stage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How it reaches Gemini&lt;/td&gt;
&lt;td&gt;Direct connection to Vertex AI&lt;/td&gt;
&lt;td&gt;DSH (Messages protocol) → LiteLLM sidecar → Vertex AI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured output&lt;/td&gt;
&lt;td&gt;&lt;code&gt;OutputSchema&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;submit_*&lt;/code&gt; tools + reading session events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Search&lt;/td&gt;
&lt;td&gt;ADK built-in &lt;code&gt;geminitool&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Custom &lt;code&gt;web_research&lt;/code&gt; tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud Run&lt;/td&gt;
&lt;td&gt;1 container&lt;/td&gt;
&lt;td&gt;2 containers (app + LiteLLM), still scales to zero&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h1&gt;
  
  
  Background
&lt;/h1&gt;

&lt;p&gt;In &lt;a href="https://dev.to/gde/ai-in-practice-rewriting-a-line-research-bot-from-python-managed-agents-to-go-adk-v190-from-4c50"&gt;my previous post&lt;/a&gt;, I rewrote my LINE research-planner bot from Python + Managed Agents to &lt;strong&gt;Go + &lt;a href="https://github.com/google/adk-go" rel="noopener noreferrer"&gt;ADK for Go&lt;/a&gt;&lt;/strong&gt;, resulting in &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-research-bot-go" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/line-research-bot-go&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The user experience is unchanged:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Send "&lt;strong&gt;Research how to choose a SOTA open-source vector database&lt;/strong&gt;" on LINE, and the bot runs three stages in sequence, &lt;strong&gt;PLAN → SEARCH_COMPARE → WRITE_REPORT&lt;/strong&gt;, then replies with a Flex card containing a summary, key sources, and a "Read full report" button.&lt;/li&gt;
&lt;li&gt;Follow up with "&lt;strong&gt;Go deeper on Chapter 2 and add Japanese sources&lt;/strong&gt;", and the same report is refined in place: the version number is bumped and the previous version is saved as a snapshot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Around the same time, DeepSeek open-sourced &lt;strong&gt;&lt;a href="https://github.com/deepseek-ai/dsh-superagent-prototype" rel="noopener noreferrer"&gt;&lt;code&gt;deepseek-ai/dsh-superagent-prototype&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;, a "super agent" prototype built on &lt;strong&gt;&lt;a href="https://www.deepseek.com/en/harness/" rel="noopener noreferrer"&gt;DeepSeek Harness (DSH)&lt;/a&gt;&lt;/strong&gt;. I was curious what this framework would look like behind a LINE bot, so I set myself a challenge:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Keep the LINE bot features and Cloud Run setup exactly as they are, and swap only the agent framework from ADK to the dsh-superagent stack.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The short answer: &lt;strong&gt;it works, and it is already running in production&lt;/strong&gt;, but not in the way I originally expected. This post covers three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What the DSH framework actually is&lt;/strong&gt; (with an architecture diagram), and what dsh-superagent builds on top of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How to migrate from ADK&lt;/strong&gt;: concept mapping and real code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What goes wrong&lt;/strong&gt;: the pitfalls I actually hit, collected into a checklist.&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repo&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/kkdai/line-research-bot-go" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/line-research-bot-go&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Previous post: Go 1.27 + ADK for Go v1.9.0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;a href="https://github.com/kkdai/line-research-bot-dsh" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/line-research-bot-dsh&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;This post: TypeScript (Node 24) + DeepSeek Harness 0.2.1-rc.5. The LINE webhook currently points here.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h1&gt;
  
  
  1. Understanding DeepSeek Harness (DSH)
&lt;/h1&gt;

&lt;p&gt;Many people assume dsh-superagent is an agent SDK. In fact, it consists of two separate layers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;On npm&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DeepSeek Harness (DSH)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;An "agent runtime" that handles model calls, tools, sessions, permissions, and sandboxing, all implemented as plugins&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@deepseek-ai/dsh&lt;/code&gt; (CLI), plus 100+ &lt;code&gt;@deepseek-ai/dsh-*&lt;/code&gt; packages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;dsh-superagent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A &lt;strong&gt;bundle (plugin pack)&lt;/strong&gt; running on DSH that turns it into a proactive agent that works on a GitHub repository&lt;/td&gt;
&lt;td&gt;A Node package installed into a DSH profile via &lt;code&gt;dsh plugin add&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  DSH's core design: everything is a Cordis plugin
&lt;/h2&gt;

&lt;p&gt;DSH is built on &lt;strong&gt;Cordis&lt;/strong&gt; (&lt;code&gt;@deepseek-ai/cordis&lt;/code&gt;), a plugin framework. Models, tools, session storage, sandboxing, even the JSON-RPC server: &lt;strong&gt;every piece is a plugin (a "plugin row")&lt;/strong&gt;, and plugins communicate through services exposed on &lt;code&gt;ctx&lt;/code&gt;, such as &lt;code&gt;ctx.llm&lt;/code&gt;, &lt;code&gt;ctx.tools&lt;/code&gt;, &lt;code&gt;ctx.fs&lt;/code&gt;, and &lt;code&gt;ctx.subprocess&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;How plugins are composed is determined by a &lt;strong&gt;Profile&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bundle&lt;/strong&gt;: an official, pre-assembled set of plugins, e.g. &lt;code&gt;dsh-sdk-minimal&lt;/code&gt;, &lt;code&gt;dsh-base&lt;/code&gt;, &lt;code&gt;web&lt;/code&gt;, &lt;code&gt;tui&lt;/code&gt;, &lt;code&gt;headless&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profile&lt;/strong&gt;: lives under &lt;code&gt;$DSH_HOME/profiles/&amp;lt;name&amp;gt;/&lt;/code&gt; and specifies which bundles to load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patch layers&lt;/strong&gt;: YAML patches layered on top of bundles. A patch can change a plugin's config, disable it with &lt;code&gt;disabled: true&lt;/code&gt;, or &lt;code&gt;insert&lt;/code&gt; your own plugins. Run &lt;code&gt;dsh --profile x --dump-config&lt;/code&gt; to see the fully resolved tree.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Example patch: tweak model settings, disable the shell, insert a custom plugin&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;llm-deepseek&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;thinking&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;disabled&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;persistent-bash&lt;/span&gt;
  &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;insert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;line-research-tools&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;file:///app/dsh/plugin/index.js'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  DSH architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNptVFFv0zAQ_iuniIcOcMP2gMQ0TZS1okC7Rk15mCiaXNttzBzbsp2Nsu2_c3ZSRLb5JXbuu-_uvjv7PmOGi-wUsq0yd6yiLsDq01oDLt9sdo7aCqaLcvVjnV2ZxgG1VklGgzQaBpXx4Wid_WzxcY2KApGXyAk5FPtQIQxdephy_A0xH7kQ1gtxQ6jMua-I5zeEKSl0ONu4_HyM5hLNU-q08H7oGj14FgsIOY987U-h-Vo_yX0ZM0d6QP8gaxEt1hmGlDAwWgAetlKJPjWXTrBU40GMuJbFRUuWcv3ljXaWES_crXApZx-4NPC1XFwSxPYYZ4vReLJE9wvjuPQwM5R3XptGcyXgDVgaWAWK7oXz_Uo_Ty67OgjdoUIobvoSZYxNJKFxGtbNyfGHE_BB2MM-GKOAUaV6hMVyMS8io98juCYoQm1b3S0GN5rCYFxOr8urcjWZX7fwvkSrxWJWdjnFICgnC7-HaXvU1SVVIFKfwoZiA3IQXMbM78Tm2gvqWIU5vjt534ZVzU5qfwpO7CTm5ASHW0mBi63UYoWsT7o_m8276ErVbWzctJFxQw7jBZRTGzqpRzpUzljJYI4DgBL62P9gmOnrU05iaR4xOAMkShJz0kwklldRmuliPsk7hM9fD-M49ElwBNJ8pu49nQQgQ-q9qDdK-OFzUDom71b7lyypAy8ZUJqXfmNRz-4J3h04Q-PDOpNaBkmV_BOv7n8DoU2Q2-7K41g-xLpaZ4wDrW-BLwTcY5vF9-XsMb89zutO3-gwKr6gmtTK4aEpQ2bqnB6akcIYfFv0_l9fCCLwPkjUJ-ZrjdQhqZu9hawWrqaS47N1n4VK1OkBw0mhjQrZ4-NfHpGC7A%3D%3D%3Ftype%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNptVFFv0zAQ_iuniIcOcMP2gMQ0TZS1okC7Rk15mCiaXNttzBzbsp2Nsu2_c3ZSRLb5JXbuu-_uvjv7PmOGi-wUsq0yd6yiLsDq01oDLt9sdo7aCqaLcvVjnV2ZxgG1VklGgzQaBpXx4Wid_WzxcY2KApGXyAk5FPtQIQxdephy_A0xH7kQ1gtxQ6jMua-I5zeEKSl0ONu4_HyM5hLNU-q08H7oGj14FgsIOY987U-h-Vo_yX0ZM0d6QP8gaxEt1hmGlDAwWgAetlKJPjWXTrBU40GMuJbFRUuWcv3ljXaWES_crXApZx-4NPC1XFwSxPYYZ4vReLJE9wvjuPQwM5R3XptGcyXgDVgaWAWK7oXz_Uo_Ty67OgjdoUIobvoSZYxNJKFxGtbNyfGHE_BB2MM-GKOAUaV6hMVyMS8io98juCYoQm1b3S0GN5rCYFxOr8urcjWZX7fwvkSrxWJWdjnFICgnC7-HaXvU1SVVIFKfwoZiA3IQXMbM78Tm2gvqWIU5vjt534ZVzU5qfwpO7CTm5ASHW0mBi63UYoWsT7o_m8276ErVbWzctJFxQw7jBZRTGzqpRzpUzljJYI4DgBL62P9gmOnrU05iaR4xOAMkShJz0kwklldRmuliPsk7hM9fD-M49ElwBNJ8pu49nQQgQ-q9qDdK-OFzUDom71b7lyypAy8ZUJqXfmNRz-4J3h04Q-PDOpNaBkmV_BOv7n8DoU2Q2-7K41g-xLpaZ4wDrW-BLwTcY5vF9-XsMb89zutO3-gwKr6gmtTK4aEpQ2bqnB6akcIYfFv0_l9fCCLwPkjUJ-ZrjdQhqZu9hawWrqaS47N1n4VK1OkBw0mhjQrZ4-NfHpGC7A%3D%3D%3Ftype%3Dpng" alt="DeepSeek Harness (DSH) architecture" width="1009" height="1448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few facts that follow from this diagram are critical for the migration:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The agent runs in a separate process.&lt;/strong&gt; Unlike ADK's &lt;code&gt;runner.Run()&lt;/code&gt;, which runs inside your program, with DSH you spawn a &lt;code&gt;dsh&lt;/code&gt; runtime subprocess and talk to it over &lt;strong&gt;stdio JSON-RPC&lt;/strong&gt;. The TypeScript client is &lt;code&gt;@deepseek-ai/dsh-sdk-client&lt;/code&gt;; a Python SDK is also available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model layer only speaks the Anthropic Messages protocol.&lt;/strong&gt; The official adapter, &lt;code&gt;llm-deepseek&lt;/code&gt;, targets &lt;code&gt;https://api.deepseek.com/anthropic&lt;/code&gt; by default. You can change &lt;code&gt;baseURL&lt;/code&gt;, but the other side must be a &lt;code&gt;/v1/messages&lt;/code&gt;-compatible endpoint. &lt;strong&gt;There is no Gemini adapter.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools are registered by plugins.&lt;/strong&gt; You write a Cordis plugin and call &lt;code&gt;ctx.tools.register(defineTool({...}))&lt;/code&gt; inside &lt;code&gt;apply(ctx)&lt;/code&gt;. DSH validates arguments against the parameter schema; on failure, the error is returned to the model so it can correct itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A session is an event stream.&lt;/strong&gt; Every &lt;code&gt;run()&lt;/code&gt; returns the full list of session events: &lt;code&gt;turn/start&lt;/code&gt;, &lt;code&gt;tool/call&lt;/code&gt;, &lt;code&gt;tool/result&lt;/code&gt;, &lt;code&gt;assistant/message&lt;/code&gt;, &lt;code&gt;turn/end&lt;/code&gt;, and so on. This is what you rely on later to extract structured results.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What dsh-superagent builds on DSH
&lt;/h2&gt;

&lt;p&gt;dsh-superagent is a prototype of a "proactive coding agent" used internally by the DeepSeek team. Its README is explicit: &lt;strong&gt;it is a research prototype, not a production product&lt;/strong&gt;. Its domain model looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNpdkE1LxDAQhv_KkKuWvcuyIFpdWL3YggfrYZpMm2CblOnED7b7302zKGiu75PnfZmj0sGQugLVDeFDW2SBh6fGQ3rVS6OqEFnTtuXN7t7JPrawgTtys42NeoWi2C01u74nXqBOeG2Z0KTobKgz0aJoS2aBMhHlOw5Zh2Z08-yChwsQdtjTv29VbGfNbpKV6dAXIcoCh7XF6TeSX7zMOGpNkyTZI3qDQgk9x4ccX6eu3o_kk-N53fFJOkrgvCV4gttqDzP9LDKOSaf4CyZikL-FZ-ONRe9pWKBqvLoENRKP6Ey65VGJpTFf1VCHcRB1On0DRol2Bg%3D%3D%3Ftype%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNpdkE1LxDAQhv_KkKuWvcuyIFpdWL3YggfrYZpMm2CblOnED7b7302zKGiu75PnfZmj0sGQugLVDeFDW2SBh6fGQ3rVS6OqEFnTtuXN7t7JPrawgTtys42NeoWi2C01u74nXqBOeG2Z0KTobKgz0aJoS2aBMhHlOw5Zh2Z08-yChwsQdtjTv29VbGfNbpKV6dAXIcoCh7XF6TeSX7zMOGpNkyTZI3qDQgk9x4ccX6eu3o_kk-N53fFJOkrgvCV4gttqDzP9LDKOSaf4CyZikL-FZ-ONRe9pWKBqvLoENRKP6Ey65VGJpTFf1VCHcRB1On0DRol2Bg%3D%3D%3Ftype%3Dpng" alt="dsh-superagent domain model" width="1437" height="232"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Source&lt;/strong&gt; converts GitHub notifications and Feishu messages into &lt;strong&gt;Triggers&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eval&lt;/strong&gt; decides whether to accept a job based on &lt;strong&gt;Policy&lt;/strong&gt; (who can issue commands, where, and with what authority). Accepted work becomes a &lt;strong&gt;Ticket&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Each Ticket is handled by an &lt;strong&gt;Executor Session&lt;/strong&gt; working in its own directory. Results flow back to the original conversation through a &lt;strong&gt;Channel&lt;/strong&gt;, e.g. by opening a PR.&lt;/li&gt;
&lt;li&gt;A web dashboard shows which items need a human decision.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The design is very thorough, and it exposes a &lt;code&gt;SourceKind&lt;/code&gt; extension point (&lt;code&gt;docs/008-cookbook.md&lt;/code&gt;), so in theory you could add a LINE Source.&lt;/p&gt;




&lt;h1&gt;
  
  
  2. Feasibility: why I did not simply fork dsh-superagent
&lt;/h1&gt;

&lt;p&gt;My first instinct was to fork dsh-superagent, add a LINE Source, and deploy it to Cloud Run. After reading through the docs and code, though, it became clear that its assumptions are almost the exact opposite of Cloud Run's:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;dsh-superagent&lt;/th&gt;
&lt;th&gt;Existing Cloud Run architecture&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Execution model&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;A single long-running process&lt;/strong&gt; (&lt;code&gt;scripts/dsh-superagent start&lt;/code&gt;, launched in tmux)&lt;/td&gt;
&lt;td&gt;Stateless, scales to zero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;State&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;SQLite&lt;/strong&gt; + lock files under &lt;code&gt;stateDir&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Firestore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Event source&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Actively polls&lt;/strong&gt; GitHub notifications; Feishu long-lived connection&lt;/td&gt;
&lt;td&gt;LINE &lt;strong&gt;pushes via webhook&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP&lt;/td&gt;
&lt;td&gt;Management API &lt;strong&gt;listens only on 127.0.0.1&lt;/strong&gt;, with same-origin checks&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/webhook&lt;/code&gt; must be publicly reachable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Required config&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;A GitHub repo and an agent account are mandatory&lt;/strong&gt;; otherwise the runtime stays in "awaiting configuration"&lt;/td&gt;
&lt;td&gt;Only a LINE channel&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agent permissions&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;danger-full-access&lt;/code&gt;, runs shell commands as the OS user&lt;/td&gt;
&lt;td&gt;Only needs web access for research&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model&lt;/td&gt;
&lt;td&gt;Anthropic Messages (DeepSeek)&lt;/td&gt;
&lt;td&gt;Vertex AI Gemini&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Forcing a fork would mean removing the mandatory GitHub configuration, building a public, signature-verified webhook, pinning Cloud Run to &lt;code&gt;min = max = 1&lt;/code&gt; instances, and mounting Filestore for SQLite. &lt;strong&gt;In other words: giving up scale-to-zero and exposing an alpha-stage process capable of running arbitrary shell commands directly to the internet.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I weighed two options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Option A: Fork dsh-superagent and add a LINE Source.&lt;/strong&gt; Keeps Eval / Ticket / dashboard, but is the most expensive and the most fragile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Option B: Keep the Cloud Run architecture and replace only the agent layer with DSH.&lt;/strong&gt; ✅ Chosen. The LINE webhook, Cloud Tasks, Firestore, and GCS stay untouched; each of the three research stages runs its own DSH session.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the model, I chose to &lt;strong&gt;keep using Gemini on Vertex AI&lt;/strong&gt;, with LiteLLM in between as a protocol translator. The consequences of that decision are covered in Section 3.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Takeaway&lt;/strong&gt;: Before "replacing framework A with framework B", be clear about whether you are replacing the &lt;strong&gt;runtime (the harness)&lt;/strong&gt; or &lt;strong&gt;the application built on top of it (the superagent)&lt;/strong&gt;. What I actually needed here was the former.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  3. Migrating from ADK to DSH: architecture and mapping
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Post-migration architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNptkk1v2kAQhv_KaE9EqTFJe2ijKhJyyodimiimzSGu0GIPeMt6191dA1XIf-_sQqBV6oN9mXnm8TvzzApdIrsCtpB6U1TcOEgfcgX0pOOvX55y5j_Qb5qc_YAout7l7P4um0K8wXml9Qo6o0k_OcvZDh5HVG7RrNF0nf08N_G1UA6Vg0Jya8VCFNwJreAcpC5WBNwPehwFMKpfLba4g2T61MlZInVbwpTb1R5l0CI3RRX91HObs7NDczL9x8r5-ti0Knqth87d-OYgeEuCG21WQfA0__aAsIglxGD5Gn35IPMeA0Ekpw0GCykUzubazTzGNrzAv1xOoIZvFDRowDq-DLCbzKdT2gpIzol6j7PlKqqFEjWXlMqrcrfhjt6_a3l0pPYDu69cZXQjCojXF3GN1tIE60ekqV-XcJimE7CixIKbvbQuuKy0dVcfer3ekZmmHgnDCbV9R-NwC_1xaFiil4redz9GC1pd9R8NWv_sGLHTWnqDYUaoYWim3xlqvZQIWagJ3HP49pBCoukqtu5t_jWn1ZSQt5cXny5hNJ2kAZpkp3vIaBMh0TeZNy1l2xi9JCtL0wcStyEVut9csXfAajQ1FyVd-zNzFdbh7ktc8FY69vLyB5dc-ak%3D%3Ftype%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNptkk1v2kAQhv_KaE9EqTFJe2ijKhJyyodimiimzSGu0GIPeMt6191dA1XIf-_sQqBV6oN9mXnm8TvzzApdIrsCtpB6U1TcOEgfcgX0pOOvX55y5j_Qb5qc_YAout7l7P4um0K8wXml9Qo6o0k_OcvZDh5HVG7RrNF0nf08N_G1UA6Vg0Jya8VCFNwJreAcpC5WBNwPehwFMKpfLba4g2T61MlZInVbwpTb1R5l0CI3RRX91HObs7NDczL9x8r5-ti0Knqth87d-OYgeEuCG21WQfA0__aAsIglxGD5Gn35IPMeA0Ekpw0GCykUzubazTzGNrzAv1xOoIZvFDRowDq-DLCbzKdT2gpIzol6j7PlKqqFEjWXlMqrcrfhjt6_a3l0pPYDu69cZXQjCojXF3GN1tIE60ekqV-XcJimE7CixIKbvbQuuKy0dVcfer3ekZmmHgnDCbV9R-NwC_1xaFiil4redz9GC1pd9R8NWv_sGLHTWnqDYUaoYWim3xlqvZQIWagJ3HP49pBCoukqtu5t_jWn1ZSQt5cXny5hNJ2kAZpkp3vIaBMh0TeZNy1l2xi9JCtL0wcStyEVut9csXfAajQ1FyVd-zNzFdbh7ktc8FY69vLyB5dc-ak%3D%3Ftype%3Dpng" alt="Post-migration architecture on Cloud Run" width="1904" height="320"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On Cloud Run this is &lt;strong&gt;one service with two containers&lt;/strong&gt;: &lt;code&gt;app&lt;/code&gt; (ingress) and a &lt;code&gt;litellm&lt;/code&gt; sidecar, communicating over localhost. The service still scales to zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  ADK → DSH mapping
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;ADK for Go (previous post)&lt;/th&gt;
&lt;th&gt;DeepSeek Harness (this post)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Where the agent runs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;In-process, &lt;code&gt;runner.Run()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;One &lt;code&gt;dsh&lt;/code&gt; runtime subprocess per stage, driven over stdio JSON-RPC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Agent definition&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;llmagent.New(...)&lt;/code&gt; × 3&lt;/td&gt;
&lt;td&gt;One profile (&lt;code&gt;sdk-minimal&lt;/code&gt; + patch) + persona; stage instructions go in the user message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;gemini.NewModel&lt;/code&gt;, direct to Vertex AI&lt;/td&gt;
&lt;td&gt;DSH Messages adapter → &lt;strong&gt;LiteLLM sidecar&lt;/strong&gt; → Vertex AI Gemini&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Google Search / URL Context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;geminitool.GoogleSearch{}&lt;/code&gt; + &lt;code&gt;url_context&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Custom plugin tool &lt;strong&gt;&lt;code&gt;web_research&lt;/code&gt;&lt;/strong&gt; (Gemini + googleSearch + urlContext)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Structured output&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;OutputSchema&lt;/code&gt; (auto-injects &lt;code&gt;set_model_response&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Custom &lt;strong&gt;&lt;code&gt;submit_plan&lt;/code&gt; / &lt;code&gt;submit_sources&lt;/code&gt; / &lt;code&gt;submit_report&lt;/code&gt;&lt;/strong&gt; tools; arguments read back from session events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stage handoff&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Session State &lt;code&gt;{plan}&lt;/code&gt;, &lt;code&gt;{sources}&lt;/code&gt;, &lt;code&gt;{report_md?}&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Firestore workspace, injected directly into the stage prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Trace ID&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ADK &lt;code&gt;InvocationID&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;DSH &lt;code&gt;sessionId&lt;/code&gt; (still stored in &lt;code&gt;last_interaction_id&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tool permissions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Only the tools each agent needs&lt;/td&gt;
&lt;td&gt;Patch disables bash, editor, and working-directory; only research tools remain&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Language&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Go 1.27&lt;/td&gt;
&lt;td&gt;TypeScript (Node 24 runs &lt;code&gt;.ts&lt;/code&gt; directly, no build step)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Everything on the LINE side — intent classification, Flex cards, Firestore document schema, GCS paths, the Cloud Tasks flow, deepen / retry — was &lt;strong&gt;ported 1:1&lt;/strong&gt; from Go, down to the exact message text. The real work in this migration comes down to four rewrites in the agent layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rewrite 1: &lt;code&gt;runner.Run()&lt;/code&gt; → &lt;code&gt;DeepSeekHarness.run()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;ADK runs the agent in-process; with DSH, each stage starts a runtime and shuts it down when done (&lt;a href="https://github.com/kkdai/line-research-bot-dsh/blob/main/src/research/agents.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/research/agents.ts&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;harness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DeepSeekHarness&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sdk-minimal&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;patches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// research.patch.yml, rendered at startup&lt;/span&gt;
  &lt;span class="na"&gt;dshHome&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dshHome&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// /tmp/dsh-home on Cloud Run&lt;/span&gt;
  &lt;span class="na"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;workdir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// an empty temp directory per stage&lt;/span&gt;
  &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;deepseek-official&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// routes through the llm-deepseek adapter&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// gemini-3.8-flash, an alias LiteLLM recognizes&lt;/span&gt;
  &lt;span class="na"&gt;maxTokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;runtimeEnv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// strips LINE secrets&lt;/span&gt;
    &lt;span class="na"&gt;DEEPSEEK_BASE_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;llmBaseURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// http://127.0.0.1:4000&lt;/span&gt;
    &lt;span class="na"&gt;DEEPSEEK_API_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;llmApiKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// LiteLLM master key&lt;/span&gt;
    &lt;span class="na"&gt;DSH_SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PERSONA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;harness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stagePrompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nf"&gt;assertTurnCompleted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;extractSubmission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit_plan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;harness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;provider: 'deepseek-official'&lt;/code&gt; looks odd — why say DeepSeek when the model is Gemini? It is simply &lt;strong&gt;the route name of the DSH adapter&lt;/strong&gt;, meaning "use the Messages protocol path". Where requests actually go is determined by &lt;code&gt;DEEPSEEK_BASE_URL&lt;/code&gt;, which points to LiteLLM.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;The SDK has no mid-run cancellation.&lt;/strong&gt; The way to enforce a timeout is to &lt;code&gt;close()&lt;/code&gt; the runtime. I wired this up with an &lt;code&gt;AbortSignal&lt;/code&gt;; each stage still has a 3 / 10 / 8-minute limit.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Rewrite 2: &lt;code&gt;OutputSchema&lt;/code&gt; → &lt;code&gt;submit_*&lt;/code&gt; tools
&lt;/h2&gt;

&lt;p&gt;ADK's &lt;code&gt;OutputSchema&lt;/code&gt; is convenient, and DSH has no equivalent. But under the hood, ADK simply &lt;strong&gt;auto-injects a &lt;code&gt;set_model_response&lt;/code&gt; tool&lt;/strong&gt;, so &lt;strong&gt;I did the same thing myself&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// dsh/plugin/index.js: a Cordis plugin&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;line-research-tools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tools&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;defineTool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit_plan&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Submit the research plan. Call exactly once when the plan is final.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;additionalProperties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;source_category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
                      &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;source_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;integer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;received&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DSH &lt;strong&gt;validates the arguments against the schema&lt;/strong&gt; first; if validation fails, the error goes back to the model so it can fix the call and retry. On the host side, all you need is to find "the last &lt;code&gt;submit_plan&lt;/code&gt; call that was not rejected" in the session events (&lt;a href="https://github.com/kkdai/line-research-bot-dsh/blob/main/src/research/submission.ts" rel="noopener noreferrer"&gt;&lt;code&gt;src/research/submission.ts&lt;/code&gt;&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tool/call&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;callId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tool/result&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolCallId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;accepted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is a nice side benefit: the issue from the previous post where the model returned malformed enum values like &lt;code&gt;chapter_not_found_chapter_not_found&lt;/code&gt; is now &lt;strong&gt;caught by DSH at the tool layer via &lt;code&gt;enum&lt;/code&gt; validation&lt;/strong&gt;, and the model is asked to resubmit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rewrite 3: a LiteLLM sidecar to connect DSH to Gemini
&lt;/h2&gt;

&lt;p&gt;DSH only speaks Anthropic Messages; Gemini on Vertex AI does not. &lt;strong&gt;&lt;a href="https://github.com/BerriAI/litellm" rel="noopener noreferrer"&gt;LiteLLM&lt;/a&gt;&lt;/strong&gt; fits neatly in between: it exposes &lt;code&gt;/v1/messages&lt;/code&gt; externally, calls &lt;code&gt;vertex_ai/gemini-3.8-flash&lt;/code&gt; internally, and authenticates with the Cloud Run service account (ADC).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;model_list&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;model_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gemini-3.8-flash&lt;/span&gt;
    &lt;span class="na"&gt;litellm_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vertex_ai/gemini-3.8-flash&lt;/span&gt;
      &lt;span class="na"&gt;vertex_project&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;your-project&amp;gt;&lt;/span&gt;
      &lt;span class="na"&gt;vertex_location&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;global&lt;/span&gt;
&lt;span class="na"&gt;litellm_settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;drop_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;general_settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;master_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;os.environ/LITELLM_MASTER_KEY&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The multi-container Cloud Run service is declared in &lt;code&gt;service.yaml&lt;/code&gt;, with &lt;code&gt;container-dependencies&lt;/code&gt; so the app waits for LiteLLM to come up first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;run.googleapis.com/container-dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{"app":["litellm"]}'&lt;/span&gt;
&lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt; &lt;span class="c1"&gt;# ingress, port 8080&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;litellm&lt;/span&gt; &lt;span class="c1"&gt;# sidecar, port 4000, startupProbe: /health/liveliness&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Rewrite 4: a &lt;code&gt;web_research&lt;/code&gt; tool to preserve Google Search grounding
&lt;/h2&gt;

&lt;p&gt;This was the part I cared about most. Much of the ADK version's quality came from &lt;strong&gt;Google Search + URL Context grounding&lt;/strong&gt;. Once requests are translated to the Messages protocol by LiteLLM, these Gemini-specific tools are effectively lost.&lt;/p&gt;

&lt;p&gt;The fix is to &lt;strong&gt;keep grounding out of the translation layer&lt;/strong&gt;: a &lt;code&gt;web_research&lt;/code&gt; tool calls Vertex AI directly via &lt;code&gt;@google/genai&lt;/code&gt;, with &lt;code&gt;googleSearch&lt;/code&gt; and &lt;code&gt;urlContext&lt;/code&gt; enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateContent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;researchModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;googleSearch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;urlContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="na"&gt;abortSignal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// groundingChunks URLs are vertexaisearch redirects; one HEAD request resolves the real URL&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The result: &lt;strong&gt;Gemini (via DSH) handles reasoning and planning, while Gemini (via the tool) handles web research.&lt;/strong&gt; DSH lets the model call &lt;code&gt;web_research&lt;/code&gt; in parallel (the tool is marked &lt;code&gt;isConcurrencySafe&lt;/code&gt;), so the search stage turned out faster than I expected.&lt;/p&gt;




&lt;h1&gt;
  
  
  4. Pitfalls: what actually went wrong
&lt;/h1&gt;

&lt;p&gt;Everything below is something I &lt;strong&gt;actually ran into&lt;/strong&gt; during this migration. DSH is currently at &lt;code&gt;0.2.1-rc.5&lt;/code&gt;, and a lot of behavior is either undocumented or documented for an older version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 1: SDK options were renamed, and the old ones are silently ignored
&lt;/h2&gt;

&lt;p&gt;The README for &lt;code&gt;@deepseek-ai/dsh-sdk-client&lt;/code&gt; shows this example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DeepSeekHarness&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lib/bin.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cordis.yml&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Copying it, everything appeared to work and the model responded. But when I asked it to list its tools, out came &lt;code&gt;bash&lt;/code&gt;, &lt;code&gt;subagent&lt;/code&gt;, &lt;code&gt;create_goal&lt;/code&gt;, &lt;code&gt;web_search&lt;/code&gt;… &lt;strong&gt;my patch had no effect at all.&lt;/strong&gt; Digging into the SDK source revealed that rc.5 had already switched to &lt;code&gt;profile&lt;/code&gt; / &lt;code&gt;patches&lt;/code&gt; / &lt;code&gt;dshHome&lt;/code&gt; / &lt;code&gt;env&lt;/code&gt;: &lt;strong&gt;&lt;code&gt;launch&lt;/code&gt; does not raise an error — it is simply ignored, and the SDK quietly falls back to the fully loaded &lt;code&gt;sdk&lt;/code&gt; profile.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;✅ &lt;strong&gt;How to verify&lt;/strong&gt;: print &lt;code&gt;tools[].name&lt;/code&gt; from the &lt;code&gt;request/header&lt;/code&gt; event to see the exact tool list the model receives. After the fix, only &lt;code&gt;read_image, web_research, submit_*&lt;/code&gt; remained.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Pitfall 2: plugin names are resolved relative to &lt;code&gt;$DSH_HOME/profiles/&amp;lt;profile&amp;gt;/&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The PoC worked fine, but the real integration test failed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AgentError: every submit_plan call was rejected: Error: unknown tool "submit_plan"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Launching the runtime directly and reading stderr revealed the cause:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;line-research-tools: failed to import: Cannot find package 'line-research-tools'
imported from /tmp/.../profiles/sdk-minimal/

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;DSH resolves plugins by walking up from the profile directory looking for &lt;code&gt;node_modules&lt;/code&gt;.&lt;/strong&gt; In the PoC, &lt;code&gt;DSH_HOME&lt;/code&gt; happened to live inside the project directory, so resolution worked by coincidence. On Cloud Run, &lt;code&gt;DSH_HOME=/tmp/dsh-home&lt;/code&gt;, so it is guaranteed to fail.&lt;/p&gt;

&lt;p&gt;Next I tried supplying the path dynamically with &lt;code&gt;name: !!js process.env.XXX&lt;/code&gt;, which was also rejected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;disabling profile plugin row "line-research-tools": its declared dsh requirements cannot be validated: name.startsWith is not a function

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;name&lt;/code&gt; is checked before &lt;code&gt;!!js&lt;/code&gt; is evaluated, so it must be a literal string.&lt;/strong&gt; The final solution: at startup, render the patch template into &lt;code&gt;DSH_HOME&lt;/code&gt; with the plugin's &lt;strong&gt;absolute &lt;code&gt;file://&lt;/code&gt; URL&lt;/strong&gt; hard-coded.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 This is the most important lesson of the whole migration: &lt;strong&gt;a single directory difference between PoC and production completely changed the outcome.&lt;/strong&gt; Integration tests must use the same &lt;code&gt;DSH_HOME&lt;/code&gt; location as production.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Pitfall 3: changing &lt;code&gt;apiKeyEnv&lt;/code&gt; in a patch has no effect
&lt;/h2&gt;

&lt;p&gt;I changed &lt;code&gt;apiKeyEnv&lt;/code&gt; for &lt;code&gt;llm-deepseek&lt;/code&gt; to &lt;code&gt;LITELLM_API_KEY&lt;/code&gt; in a patch, and &lt;code&gt;--dump-config&lt;/code&gt; confirmed the change, yet at runtime I still got:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MISSING_CREDENTIAL: no API key for provider route "deepseek-official"; ... export DEEPSEEK_API_KEY

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the end, simply passing LiteLLM's key and URL through the &lt;strong&gt;&lt;code&gt;DEEPSEEK_API_KEY&lt;/code&gt; / &lt;code&gt;DEEPSEEK_BASE_URL&lt;/code&gt; environment variables&lt;/strong&gt; worked. Also note that a patch's &lt;code&gt;config&lt;/code&gt; &lt;strong&gt;replaces the block entirely&lt;/strong&gt; rather than merging, so you need to restore defaults such as &lt;code&gt;defaultContextWindow&lt;/code&gt; and &lt;code&gt;streamIdleTimeoutMs&lt;/code&gt; yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 4: different profiles cannot share the same sessions directory
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;session artifact ".../session.v4.jsonl.zstd" uses .jsonl.zstd, but this backend is configured for compression "none"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sdk&lt;/code&gt; profile compresses sessions (zstd); &lt;code&gt;sdk-minimal&lt;/code&gt; does not. &lt;strong&gt;If two different profiles run against the same &lt;code&gt;DSH_HOME&lt;/code&gt;, they end up reading each other's files.&lt;/strong&gt; Production only uses one profile, so this is not an issue there, but remember to clear &lt;code&gt;$DSH_HOME/sessions&lt;/code&gt; when experimenting locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 5: Node's type stripping does not support constructor parameter properties
&lt;/h2&gt;

&lt;p&gt;Node 24 can run &lt;code&gt;node src/main.ts&lt;/code&gt; directly with no build step, which is great. But it only strips &lt;strong&gt;erasable syntax&lt;/strong&gt;, so code like this fails outright:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="c1"&gt;// ❌ TS1294&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable &lt;code&gt;erasableSyntaxOnly: true&lt;/code&gt; in &lt;code&gt;tsconfig.json&lt;/code&gt; so &lt;code&gt;tsc&lt;/code&gt; catches this early. Also, &lt;strong&gt;&lt;code&gt;.ts&lt;/code&gt; files under &lt;code&gt;node_modules&lt;/code&gt; are not type-stripped&lt;/strong&gt;, so DSH plugins must be written in plain JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 6: &lt;code&gt;npx node@24&lt;/code&gt; does not actually run Node 24
&lt;/h2&gt;

&lt;p&gt;I wanted to confirm the container's Node 24 could run everything, but &lt;code&gt;npx node@24 --version&lt;/code&gt; printed &lt;code&gt;v26.0.0&lt;/code&gt;: npx still used the &lt;code&gt;node&lt;/code&gt; on &lt;code&gt;PATH&lt;/code&gt;. I ended up downloading the Node 24 tarball directly from nodejs.org (verify it against SHASUMS256) to run the tests, which also confirmed the DSH runtime starts cleanly on Node 24.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 7: LiteLLM needs extra Vertex AI dependencies
&lt;/h2&gt;

&lt;p&gt;After &lt;code&gt;pip install 'litellm[proxy]'&lt;/code&gt;, the very first request failed with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;litellm.APIConnectionError: No module named 'google'

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Vertex AI path requires &lt;strong&gt;&lt;code&gt;google-auth&lt;/code&gt; + &lt;code&gt;google-cloud-aiplatform&lt;/code&gt;&lt;/strong&gt;, which you must add to the image yourself and pin (I use &lt;code&gt;litellm 1.106.6&lt;/code&gt;, &lt;code&gt;google-auth 2.62.1&lt;/code&gt;, &lt;code&gt;google-cloud-aiplatform 2.7.0&lt;/code&gt;). In addition, recent LiteLLM versions &lt;strong&gt;refuse to start without a master key&lt;/strong&gt;, and requests without a key return &lt;strong&gt;500 rather than 401&lt;/strong&gt; (due to missing &lt;code&gt;prisma&lt;/code&gt;), although they are still rejected. Fortunately the sidecar only listens on localhost inside the instance and is not reachable externally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 8: the agent subprocess does not need your secrets
&lt;/h2&gt;

&lt;p&gt;By default the DSH runtime inherits the entire &lt;code&gt;process.env&lt;/code&gt;, which means &lt;strong&gt;the LINE Channel Secret and Access Token end up inside the agent's process&lt;/strong&gt;. Even though the shell tools are disabled, I wrote a &lt;code&gt;runtimeEnv()&lt;/code&gt; helper to strip them out, with unit tests. This matches what the dsh-superagent docs say: &lt;strong&gt;wrappers and directories are defaults, not security boundaries&lt;/strong&gt; — don't hand over anything you don't have to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 9: it is slower than the ADK version
&lt;/h2&gt;

&lt;p&gt;The integration test (plan → search → write → deepen with a missing chapter) took &lt;strong&gt;about 7.5 minutes&lt;/strong&gt;, versus roughly 150–190 seconds for the ADK version. Contributing factors: an extra runtime startup per stage (about 1–2 seconds), the additional LiteLLM hop, and many &lt;code&gt;web_research&lt;/code&gt; calls. Everything still fits within Cloud Tasks' 30-minute dispatch deadline, but I relaxed the stage timeouts from ADK's 3 / 8 / 6 minutes to 3 / 10 / 8 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pitfall 10: pin pre-release dependencies
&lt;/h2&gt;

&lt;p&gt;DSH publishes three release lines on npm simultaneously: &lt;code&gt;latest&lt;/code&gt; (0.0.1-rc.1 or 0.1.x), &lt;code&gt;next&lt;/code&gt; (0.2.1-rc.5), and &lt;code&gt;alpha&lt;/code&gt; (0.2.2-alpha.3), and &lt;strong&gt;&lt;code&gt;latest&lt;/code&gt; points to different versions for different packages&lt;/strong&gt;. That is exactly how the SDK option rename in Pitfall 1 bit me. I pinned everything to the &lt;code&gt;next&lt;/code&gt; line: &lt;code&gt;@deepseek-ai/dsh&lt;/code&gt;, &lt;code&gt;dsh-sdk-client&lt;/code&gt;, and &lt;code&gt;dsh-tools&lt;/code&gt; are all at &lt;code&gt;0.2.1-rc.5&lt;/code&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  5. Deployment and cutover: reusing the existing service's configuration
&lt;/h1&gt;

&lt;p&gt;Deployment differs from the previous post: this time I &lt;strong&gt;created a new Cloud Run service&lt;/strong&gt;, since the sidecar changes the container layout. All configuration, however, was carried over from the original service:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;describe&lt;/code&gt; the original service first&lt;/strong&gt; to get the service account, bucket, queue, and Secret Manager bindings for the LINE secrets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reuse everything in the new service&lt;/strong&gt;: same SA, same bucket, same queue, same LINE secrets, and &lt;strong&gt;the same Firestore collections&lt;/strong&gt;. That way, after the cutover, users can keep "deepening" their existing reports. The only new secret is &lt;code&gt;LITELLM_MASTER_KEY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build two images with Cloud Build&lt;/strong&gt; (my laptop still has no Docker) and deploy with &lt;code&gt;gcloud run services replace service.yaml&lt;/code&gt;. &lt;code&gt;CLOUD_RUN_SERVICE_URL&lt;/code&gt; is precomputed using Cloud Run's deterministic &lt;code&gt;https://&amp;lt;service&amp;gt;-&amp;lt;project-number&amp;gt;.&amp;lt;region&amp;gt;.run.app&lt;/code&gt; format, so there is no need to deploy twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smoke tests&lt;/strong&gt;: &lt;code&gt;/readyz&lt;/code&gt; returns 200, an invalid signature returns 401, a missing OIDC token returns 401, a request signed with the real Channel Secret returns 200, and both containers pass their startup probes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switch the webhook via the LINE API&lt;/strong&gt; — again, no console required:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gcloud secrets versions access latest &lt;span class="nt"&gt;--secret&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;LINE_CHANNEL_ACCESS_TOKEN&lt;span class="si"&gt;)&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; PUT &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"endpoint":"&amp;lt;NEW_SERVICE_URL&amp;gt;/webhook"}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://api.line.me/v2/bot/channel/webhook/endpoint
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  https://api.line.me/v2/bot/channel/webhook/test

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Confirm traffic in the logs&lt;/strong&gt;: after the switch, the new service received &lt;code&gt;LineBotWebhook/2.0&lt;/code&gt; requests and the old service dropped to zero. The first real user's research job then ran on the new service from &lt;code&gt;job start&lt;/code&gt; to &lt;code&gt;job done&lt;/code&gt; in &lt;strong&gt;about 6 minutes 43 seconds&lt;/strong&gt; with no errors. All Gemini calls through the LiteLLM sidecar returned 200, and a ~29 KB &lt;code&gt;index.html&lt;/code&gt; report appeared in GCS.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rolling back is just a matter of pointing the webhook back at the original service. &lt;strong&gt;Because both services share Firestore, switching back and forth loses no data.&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  6. ADK → DSH migration checklist
&lt;/h1&gt;

&lt;ul&gt;
&lt;li&gt; &lt;strong&gt;1. Decide whether you are replacing the harness or the app on top of it&lt;/strong&gt;: dsh-superagent is a GitHub coding agent; a chat bot on Cloud Run needs DSH itself.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;2. Build a PoC first, using the production directory layout&lt;/strong&gt;: keep &lt;code&gt;DSH_HOME&lt;/code&gt; outside the project (e.g. &lt;code&gt;/tmp&lt;/code&gt;) so plugin resolution issues surface early.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;3. Print the tool list the model actually receives&lt;/strong&gt; (&lt;code&gt;request/header&lt;/code&gt; event) to confirm the patch is applied and dangerous tools are really disabled.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;4. Follow the type definitions for SDK options, not the README&lt;/strong&gt;: options get renamed between RCs, and old ones fail silently.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;5. Use a literal absolute &lt;code&gt;file://&lt;/code&gt; URL for the plugin &lt;code&gt;name&lt;/code&gt;&lt;/strong&gt;; &lt;code&gt;!!js&lt;/code&gt; does not work here.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;6. &lt;code&gt;OutputSchema&lt;/code&gt; → &lt;code&gt;submit_*&lt;/code&gt; tools&lt;/strong&gt;: let DSH validate the schema, and have the host pick the last accepted call from &lt;code&gt;tool/call&lt;/code&gt; / &lt;code&gt;tool/result&lt;/code&gt; events.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;7. Use a Messages-compatible gateway (LiteLLM) for non-DeepSeek models&lt;/strong&gt;, pass credentials via &lt;code&gt;DEEPSEEK_API_KEY&lt;/code&gt; / &lt;code&gt;DEEPSEEK_BASE_URL&lt;/code&gt;, and remember that patch configs replace rather than merge.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;8. Put vendor-specific capabilities (Google Search grounding) inside tools&lt;/strong&gt;; don't expect the translation layer to preserve them.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;9. Filter environment variables for the agent subprocess&lt;/strong&gt;; never hand it your Channel Secret.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;10. Pin all pre-release packages to the same dist-tag line&lt;/strong&gt;, and actually run the container's Node version at least once.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;11. Reuse the old service's SA / bucket / queue / secrets / Firestore in the new service&lt;/strong&gt;, switch the webhook via the LINE API, verify traffic in the logs, and roll back by reverting the webhook.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;12. Never expose real &lt;code&gt;.run.app&lt;/code&gt; URLs in public repos, issues, or posts.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  Summary and open-source links
&lt;/h1&gt;

&lt;p&gt;In this migration I replaced the agent layer of &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-research-bot-go" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/line-research-bot-go&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; — ADK for Go — with DeepSeek Harness, producing &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-research-bot-dsh" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/line-research-bot-dsh&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;. The key takeaways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;DSH is a composable agent runtime.&lt;/strong&gt; The Cordis plugin + profile layering design is very clean: disabling, swapping, or inserting any component takes just a few lines of YAML. The trade-off is that the agent runs in a separate process and must be driven over JSON-RPC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;dsh-superagent is an excellent reference design for proactive agents&lt;/strong&gt; (Source / Eval / Ticket / Mandate), but its execution model — a long-running process plus SQLite — is a poor fit for Cloud Run. Borrow its harness; don't force-port its app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Most of what ADK offers can be rebuilt on DSH as tools&lt;/strong&gt;: &lt;code&gt;OutputSchema&lt;/code&gt; becomes &lt;code&gt;submit_*&lt;/code&gt;, and Google Search grounding becomes &lt;code&gt;web_research&lt;/code&gt;. Schema validation at the tool layer is arguably more reliable than prompt-level constraints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The biggest risk with pre-release frameworks is not missing features but silent failures&lt;/strong&gt;: ignored options, and plugin load failures that surface as "unknown tool". Every layer needs a way to be observed and verified directly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The full source code is at &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-research-bot-dsh" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/line-research-bot-dsh&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;, and the design trade-offs are documented in the repo's &lt;a href="https://github.com/kkdai/line-research-bot-dsh/blob/main/docs/design.md" rel="noopener noreferrer"&gt;&lt;code&gt;docs/design.md&lt;/code&gt;&lt;/a&gt;. Feedback and discussion are welcome!&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/gde/ai-in-practice-rewriting-a-line-research-bot-from-python-managed-agents-to-go-adk-v190-from-4c50"&gt;Previous post: Rewriting a LINE Research Bot from Python Managed Agents to Go ADK&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kkdai/line-research-bot-dsh" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/line-research-bot-dsh&lt;/code&gt;: full source code for this post&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deepseek-ai/dsh-superagent-prototype" rel="noopener noreferrer"&gt;&lt;code&gt;deepseek-ai/dsh-superagent-prototype&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.deepseek.com/en/harness/" rel="noopener noreferrer"&gt;DeepSeek Harness&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.litellm.ai/" rel="noopener noreferrer"&gt;LiteLLM: Anthropic &lt;code&gt;/v1/messages&lt;/code&gt;-compatible endpoint&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/run/docs/deploying#sidecars" rel="noopener noreferrer"&gt;Cloud Run: multi-container (sidecar) deployments&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>backend</category>
      <category>go</category>
    </item>
    <item>
      <title>[AI in Practice] Same Name, Different Layer: DeepSeek Harness (DSH) Explained and Compared with Google Research Harness2</title>
      <dc:creator>Evan Lin</dc:creator>
      <pubDate>Tue, 03 Nov 2026 00:00:00 +0000</pubDate>
      <link>https://dev.to/gde/ai-in-practice-same-name-different-layer-deepseek-harness-dsh-explained-and-compared-with-4p7m</link>
      <guid>https://dev.to/gde/ai-in-practice-same-name-different-layer-deepseek-harness-dsh-explained-and-compared-with-4p7m</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNqNk0uP0zAQx7_KyKddQR-bA4IKVoLdQg-tWmVzWRGQHHuamDp2sJ0-9vHdGSdbSuGCT37O_Of3Hz8yYSWyCbC1tjtRcRdgnuYGaPi2KB1vKpglX3P2xdpSI6TokTtBm9wZ9D5vx-MimQBurd6ih13FA4QKgZdoAigP-LNVTYMSdipUOfvWB49DKociKGsg-3TaXa4ySpft7EDjFjXQndZ5tUWwTVC1ekAHF6tDqKy5fF-40fXK2cZ6hLxNrt4lQMvi9-LG1vGsuzfdN-gUGhGrWGtKbd2ZniydTim1uBoOxVuo-gJhrajs4LAP4g8-YN04ihtgBGXLnXRcaQ-j7nxuTZmhqxfTxTK9pxt-o7QmTMk4eXOWjeqEweD6KWcUTbaC4BncByBu6HiHZfb9YnOZs6dOWf8SjczNX_7c3s1I9i1ic4e4OTozAe491oWmwNxIcK3xJ2f-w4hVuvxMcYlnh-ADFK2RNHkF9x8Xc2h4EOd-ptE46auYipzqgfV9oK1tCIYI-2GwNtLq5lrXL9w8KY4CyHIT_FnYqOOF1I11klrqVFij21IZHxml2T-EIjYYDLunRzu15ZKasTgAP-r883WaHR8Ex390PaIwyvXCOuwSkXG5Ya-B1eQzV5J-zyMjsHX3jySueasDe37-BR-GFr8%3D%3Ftype%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNqNk0uP0zAQx7_KyKddQR-bA4IKVoLdQg-tWmVzWRGQHHuamDp2sJ0-9vHdGSdbSuGCT37O_Of3Hz8yYSWyCbC1tjtRcRdgnuYGaPi2KB1vKpglX3P2xdpSI6TokTtBm9wZ9D5vx-MimQBurd6ih13FA4QKgZdoAigP-LNVTYMSdipUOfvWB49DKociKGsg-3TaXa4ySpft7EDjFjXQndZ5tUWwTVC1ekAHF6tDqKy5fF-40fXK2cZ6hLxNrt4lQMvi9-LG1vGsuzfdN-gUGhGrWGtKbd2ZniydTim1uBoOxVuo-gJhrajs4LAP4g8-YN04ihtgBGXLnXRcaQ-j7nxuTZmhqxfTxTK9pxt-o7QmTMk4eXOWjeqEweD6KWcUTbaC4BncByBu6HiHZfb9YnOZs6dOWf8SjczNX_7c3s1I9i1ic4e4OTozAe491oWmwNxIcK3xJ2f-w4hVuvxMcYlnh-ADFK2RNHkF9x8Xc2h4EOd-ptE46auYipzqgfV9oK1tCIYI-2GwNtLq5lrXL9w8KY4CyHIT_FnYqOOF1I11klrqVFij21IZHxml2T-EIjYYDLunRzu15ZKasTgAP-r883WaHR8Ex390PaIwyvXCOuwSkXG5Ya-B1eQzV5J-zyMjsHX3jySueasDe37-BR-GFr8%3D%3Ftype%3Dpng" alt="DeepSeek Harness vs. Google Research Harness²: two different layers" width="1597" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;DeepSeek Harness (DSH)&lt;/th&gt;
&lt;th&gt;Google Research Harness²&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;In one sentence&lt;/td&gt;
&lt;td&gt;The agent's &lt;strong&gt;runtime&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;An &lt;strong&gt;automatic optimizer&lt;/strong&gt; for what the agent is equipped with&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What "harness" refers to&lt;/td&gt;
&lt;td&gt;The runtime that executes the agent&lt;/td&gt;
&lt;td&gt;The prompts, guardrails, memory, and skills wrapped around the model&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What you change&lt;/td&gt;
&lt;td&gt;Profiles, YAML patches, plugins&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;c1..c8&lt;/code&gt; file tree (though usually Harness² changes it for you)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where it's used in this series&lt;/td&gt;
&lt;td&gt;The agent layer of the LINE research bot&lt;/td&gt;
&lt;td&gt;Self-evolution of the LINE AI business-card assistant&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h1&gt;
  
  
  Introduction: two "harnesses" in one week
&lt;/h1&gt;

&lt;p&gt;In my recent &lt;strong&gt;Harness² trilogy&lt;/strong&gt; (&lt;a href="https://dev.to/gde/agent-platform-in-action-7-when-adk-go-graph-workflow-meets-google-research-harness-part-1-28c1"&gt;Part 1&lt;/a&gt;, &lt;a href="https://dev.to/gde/agent-platform-in-action-8-when-adk-go-graph-workflow-meets-google-research-harness-part-2-nc8"&gt;Part 2&lt;/a&gt;, &lt;a href="https://dev.to/gde/agent-platform-in-action-9-when-adk-go-graph-workflow-meets-google-research-harness-part-2-1olm"&gt;Part 3&lt;/a&gt;), I connected Google Research's &lt;strong&gt;&lt;a href="https://github.com/google-research/harness2" rel="noopener noreferrer"&gt;&lt;code&gt;google-research/harness2&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt; to my &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant" rel="noopener noreferrer"&gt;LINE AI business-card assistant&lt;/a&gt;&lt;/strong&gt;, letting it read OpenTelemetry traces and grow its own skill files. Accuracy climbed from &lt;code&gt;62.5%&lt;/code&gt; all the way to &lt;code&gt;100.0%&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This week I also came across DeepSeek's open-source &lt;strong&gt;&lt;a href="https://github.com/deepseek-ai/dsh-superagent-prototype" rel="noopener noreferrer"&gt;&lt;code&gt;deepseek-ai/dsh-superagent-prototype&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;, which is built on &lt;strong&gt;&lt;a href="https://www.deepseek.com/en/harness/" rel="noopener noreferrer"&gt;DeepSeek Harness (DSH)&lt;/a&gt;&lt;/strong&gt;. I migrated the agent layer of my &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-research-bot-go" rel="noopener noreferrer"&gt;LINE research bot&lt;/a&gt;&lt;/strong&gt; from ADK for Go to DSH, producing &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-research-bot-dsh" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/line-research-bot-dsh&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;, which is now running in production.&lt;/p&gt;

&lt;p&gt;Both are called &lt;strong&gt;Harness&lt;/strong&gt;, and at first I assumed they were the same kind of thing. After actually using both, it turns out:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;DSH is about how the harness is assembled and strapped onto the horse so it can run; Harness² is about automatically adjusting every strap on that harness.&lt;/strong&gt; They live at different layers and can even be combined.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because there is a lot to cover, I split this into two posts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;This post&lt;/strong&gt;: what the DSH framework actually is (with an architecture diagram), and a point-by-point comparison with Google Harness².&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/gde/ai-in-practice-migrating-a-line-research-bot-from-go-adk-to-deepseek-harness-dsh-architecture-208e"&gt;The next post&lt;/a&gt;&lt;/strong&gt;: how I actually migrated the LINE research bot from ADK to DSH, plus a list of pitfalls.&lt;/li&gt;
&lt;/ol&gt;




&lt;h1&gt;
  
  
  1. Understanding DeepSeek Harness (DSH)
&lt;/h1&gt;

&lt;p&gt;Many people assume dsh-superagent is an agent SDK. In fact, it consists of two separate layers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;On npm&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DeepSeek Harness (DSH)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;An "agent runtime" that handles model calls, tools, sessions, permissions, and sandboxing, all implemented as plugins&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;@deepseek-ai/dsh&lt;/code&gt; (CLI), plus 100+ &lt;code&gt;@deepseek-ai/dsh-*&lt;/code&gt; packages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;dsh-superagent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A &lt;strong&gt;bundle (plugin pack)&lt;/strong&gt; running on DSH that turns it into a proactive agent that works on a GitHub repository&lt;/td&gt;
&lt;td&gt;A Node package installed into a DSH profile via &lt;code&gt;dsh plugin add&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  1.1 Core design: everything is a Cordis plugin
&lt;/h2&gt;

&lt;p&gt;DSH is built on &lt;strong&gt;Cordis&lt;/strong&gt; (&lt;code&gt;@deepseek-ai/cordis&lt;/code&gt;), a plugin framework. Models, tools, session storage, sandboxing, even the JSON-RPC server: &lt;strong&gt;every piece is a plugin (a "plugin row")&lt;/strong&gt;, and plugins communicate through services exposed on &lt;code&gt;ctx&lt;/code&gt;, such as &lt;code&gt;ctx.llm&lt;/code&gt;, &lt;code&gt;ctx.tools&lt;/code&gt;, &lt;code&gt;ctx.fs&lt;/code&gt;, and &lt;code&gt;ctx.subprocess&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;How plugins are composed is determined by a &lt;strong&gt;Profile&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bundle&lt;/strong&gt;: an official, pre-assembled set of plugins, e.g. &lt;code&gt;dsh-sdk-minimal&lt;/code&gt;, &lt;code&gt;dsh-base&lt;/code&gt;, &lt;code&gt;web&lt;/code&gt;, &lt;code&gt;tui&lt;/code&gt;, &lt;code&gt;headless&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Profile&lt;/strong&gt;: lives under &lt;code&gt;$DSH_HOME/profiles/&amp;lt;name&amp;gt;/&lt;/code&gt; and specifies which bundles to load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patch layers&lt;/strong&gt;: YAML patches layered on top of bundles. A patch can change a plugin's config, disable it with &lt;code&gt;disabled: true&lt;/code&gt;, or &lt;code&gt;insert&lt;/code&gt; your own plugins. Run &lt;code&gt;dsh --profile x --dump-config&lt;/code&gt; to see the fully resolved tree.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Example patch: tweak model settings, disable the shell, insert a custom plugin&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;llm-deepseek&lt;/span&gt;
  &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;thinking&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;disabled&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;persistent-bash&lt;/span&gt;
  &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;insert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;line-research-tools&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;file:///app/dsh/plugin/index.js'&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1.2 DSH architecture
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNptVFFv0zAQ_iuniIcOcMP2gMQ0TZS1okC7Rk15mCiaXNttzBzbsp2Nsu2_c3ZSRLb5JXbuu-_uvjv7PmOGi-wUsq0yd6yiLsDq01oDLt9sdo7aCqaLcvVjnV2ZxgG1VklGgzQaBpXx4Wid_WzxcY2KApGXyAk5FPtQIQxdephy_A0xH7kQ1gtxQ6jMua-I5zeEKSl0ONu4_HyM5hLNU-q08H7oGj14FgsIOY987U-h-Vo_yX0ZM0d6QP8gaxEt1hmGlDAwWgAetlKJPjWXTrBU40GMuJbFRUuWcv3ljXaWES_crXApZx-4NPC1XFwSxPYYZ4vReLJE9wvjuPQwM5R3XptGcyXgDVgaWAWK7oXz_Uo_Ty67OgjdoUIobvoSZYxNJKFxGtbNyfGHE_BB2MM-GKOAUaV6hMVyMS8io98juCYoQm1b3S0GN5rCYFxOr8urcjWZX7fwvkSrxWJWdjnFICgnC7-HaXvU1SVVIFKfwoZiA3IQXMbM78Tm2gvqWIU5vjt534ZVzU5qfwpO7CTm5ASHW0mBi63UYoWsT7o_m8276ErVbWzctJFxQw7jBZRTGzqpRzpUzljJYI4DgBL62P9gmOnrU05iaR4xOAMkShJz0kwklldRmuliPsk7hM9fD-M49ElwBNJ8pu49nQQgQ-q9qDdK-OFzUDom71b7lyypAy8ZUJqXfmNRz-4J3h04Q-PDOpNaBkmV_BOv7n8DoU2Q2-7K41g-xLpaZ4wDrW-BLwTcY5vF9-XsMb89zutO3-gwKr6gmtTK4aEpQ2bqnB6akcIYfFv0_l9fCCLwPkjUJ-ZrjdQhqZu9hawWrqaS47N1n4VK1OkBw0mhjQrZ4-NfHpGC7A%3D%3D%3Ftype%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNptVFFv0zAQ_iuniIcOcMP2gMQ0TZS1okC7Rk15mCiaXNttzBzbsp2Nsu2_c3ZSRLb5JXbuu-_uvjv7PmOGi-wUsq0yd6yiLsDq01oDLt9sdo7aCqaLcvVjnV2ZxgG1VklGgzQaBpXx4Wid_WzxcY2KApGXyAk5FPtQIQxdephy_A0xH7kQ1gtxQ6jMua-I5zeEKSl0ONu4_HyM5hLNU-q08H7oGj14FgsIOY987U-h-Vo_yX0ZM0d6QP8gaxEt1hmGlDAwWgAetlKJPjWXTrBU40GMuJbFRUuWcv3ljXaWES_crXApZx-4NPC1XFwSxPYYZ4vReLJE9wvjuPQwM5R3XptGcyXgDVgaWAWK7oXz_Uo_Ty67OgjdoUIobvoSZYxNJKFxGtbNyfGHE_BB2MM-GKOAUaV6hMVyMS8io98juCYoQm1b3S0GN5rCYFxOr8urcjWZX7fwvkSrxWJWdjnFICgnC7-HaXvU1SVVIFKfwoZiA3IQXMbM78Tm2gvqWIU5vjt534ZVzU5qfwpO7CTm5ASHW0mBi63UYoWsT7o_m8276ErVbWzctJFxQw7jBZRTGzqpRzpUzljJYI4DgBL62P9gmOnrU05iaR4xOAMkShJz0kwklldRmuliPsk7hM9fD-M49ElwBNJ8pu49nQQgQ-q9qDdK-OFzUDom71b7lyypAy8ZUJqXfmNRz-4J3h04Q-PDOpNaBkmV_BOv7n8DoU2Q2-7K41g-xLpaZ4wDrW-BLwTcY5vF9-XsMb89zutO3-gwKr6gmtTK4aEpQ2bqnB6akcIYfFv0_l9fCCLwPkjUJ-ZrjdQhqZu9hawWrqaS47N1n4VK1OkBw0mhjQrZ4-NfHpGC7A%3D%3D%3Ftype%3Dpng" alt="DeepSeek Harness (DSH) architecture" width="1009" height="1448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Four key facts follow from this diagram:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The agent runs in a separate process.&lt;/strong&gt; Unlike ADK's &lt;code&gt;runner.Run()&lt;/code&gt;, which runs inside your program, with DSH you spawn a &lt;code&gt;dsh&lt;/code&gt; runtime subprocess and talk to it over &lt;strong&gt;stdio JSON-RPC&lt;/strong&gt;. The TypeScript client is &lt;code&gt;@deepseek-ai/dsh-sdk-client&lt;/code&gt;; a Python SDK is also available.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model layer only speaks the Anthropic Messages protocol.&lt;/strong&gt; The official adapter, &lt;code&gt;llm-deepseek&lt;/code&gt;, targets &lt;code&gt;https://api.deepseek.com/anthropic&lt;/code&gt; by default. You can change &lt;code&gt;baseURL&lt;/code&gt;, but the other side must be a &lt;code&gt;/v1/messages&lt;/code&gt;-compatible endpoint. &lt;strong&gt;There is no Gemini adapter.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools are registered by plugins.&lt;/strong&gt; You write a Cordis plugin and call &lt;code&gt;ctx.tools.register(defineTool({...}))&lt;/code&gt; inside &lt;code&gt;apply(ctx)&lt;/code&gt;. DSH validates arguments against the parameter schema; on failure, the error is returned to the model so it can correct itself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A session is an event stream.&lt;/strong&gt; Every &lt;code&gt;run()&lt;/code&gt; returns the full list of session events: &lt;code&gt;turn/start&lt;/code&gt;, &lt;code&gt;tool/call&lt;/code&gt;, &lt;code&gt;tool/result&lt;/code&gt;, &lt;code&gt;assistant/message&lt;/code&gt;, &lt;code&gt;turn/end&lt;/code&gt;, and so on. Both structured output extraction and debugging rely on these events.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1.3 What dsh-superagent builds on DSH
&lt;/h2&gt;

&lt;p&gt;dsh-superagent is a prototype of a "proactive coding agent" used internally by the DeepSeek team. Its README is explicit: &lt;strong&gt;it is a research prototype, not a production product&lt;/strong&gt;. Its domain model looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNpdkE1LxDAQhv_KkKuWvcuyIFpdWL3YggfrYZpMm2CblOnED7b7302zKGiu75PnfZmj0sGQugLVDeFDW2SBh6fGQ3rVS6OqEFnTtuXN7t7JPrawgTtys42NeoWi2C01u74nXqBOeG2Z0KTobKgz0aJoS2aBMhHlOw5Zh2Z08-yChwsQdtjTv29VbGfNbpKV6dAXIcoCh7XF6TeSX7zMOGpNkyTZI3qDQgk9x4ccX6eu3o_kk-N53fFJOkrgvCV4gttqDzP9LDKOSaf4CyZikL-FZ-ONRe9pWKBqvLoENRKP6Ey65VGJpTFf1VCHcRB1On0DRol2Bg%3D%3D%3Ftype%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNpdkE1LxDAQhv_KkKuWvcuyIFpdWL3YggfrYZpMm2CblOnED7b7302zKGiu75PnfZmj0sGQugLVDeFDW2SBh6fGQ3rVS6OqEFnTtuXN7t7JPrawgTtys42NeoWi2C01u74nXqBOeG2Z0KTobKgz0aJoS2aBMhHlOw5Zh2Z08-yChwsQdtjTv29VbGfNbpKV6dAXIcoCh7XF6TeSX7zMOGpNkyTZI3qDQgk9x4ccX6eu3o_kk-N53fFJOkrgvCV4gttqDzP9LDKOSaf4CyZikL-FZ-ONRe9pWKBqvLoENRKP6Ey65VGJpTFf1VCHcRB1On0DRol2Bg%3D%3D%3Ftype%3Dpng" alt="dsh-superagent domain model" width="1437" height="232"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;Source&lt;/strong&gt; converts GitHub notifications and Feishu messages into &lt;strong&gt;Triggers&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Eval&lt;/strong&gt; decides whether to accept a job based on &lt;strong&gt;Policy&lt;/strong&gt; (who can issue commands, where, and with what authority). Accepted work becomes a &lt;strong&gt;Ticket&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Each Ticket is handled by an &lt;strong&gt;Executor Session&lt;/strong&gt; working in its own directory. Results flow back to the original conversation through a &lt;strong&gt;Channel&lt;/strong&gt;, e.g. by opening a PR.&lt;/li&gt;
&lt;li&gt;A web dashboard shows which items need a human decision.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Its execution model is &lt;strong&gt;a single long-running process + SQLite + a management API that listens only on 127.0.0.1&lt;/strong&gt;, and it requires a GitHub repo and an agent account. That is why, when migrating the LINE bot, I borrowed only DSH and did not fork dsh-superagent; details are in &lt;a href="https://dev.to/gde/ai-in-practice-migrating-a-line-research-bot-from-go-adk-to-deepseek-harness-dsh-architecture-208e"&gt;the next post&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Takeaway&lt;/strong&gt;: When talking about DSH, be clear whether you mean the &lt;strong&gt;runtime (the harness)&lt;/strong&gt; or &lt;strong&gt;the application built on top of it (the superagent)&lt;/strong&gt;. What most people actually want to borrow is the former.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h1&gt;
  
  
  2. Quick recap: what Google Research Harness² does
&lt;/h1&gt;

&lt;p&gt;For the full details, see &lt;a href="https://dev.to/gde/agent-platform-in-action-7-when-adk-go-graph-workflow-meets-google-research-harness-part-1-28c1"&gt;Harness² Part 1&lt;/a&gt;. Here are the highlights:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It defines the "harness" as an editable file tree&lt;/strong&gt;: &lt;code&gt;c1..c4&lt;/code&gt; are always-loaded components (&lt;code&gt;systemprompt.md&lt;/code&gt;, &lt;code&gt;guardrails.md&lt;/code&gt;, &lt;code&gt;LongTermMEMORY.md&lt;/code&gt;, &lt;code&gt;tool_descriptions/&lt;/code&gt;) with a combined budget of &lt;strong&gt;24,000 bytes&lt;/strong&gt;; &lt;code&gt;c5..c8&lt;/code&gt; are on-demand components (&lt;code&gt;sub_agents/&lt;/code&gt;, &lt;code&gt;skills/&lt;/code&gt;, &lt;code&gt;scripts/&lt;/code&gt;, &lt;code&gt;plugin/&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It edits this tree automatically via two-level recursion&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;Inner loop, &lt;strong&gt;Propose → Probe → Compose&lt;/strong&gt;: read the execution trajectories (&lt;code&gt;trajectory.md&lt;/code&gt;) of failed tasks, propose multiple candidate patches, run each once in a sandbox, and compose the ones with positive gains into the next generation $H^{(k)}$.&lt;/li&gt;
&lt;li&gt;Outer loop, &lt;strong&gt;Experience Reflector&lt;/strong&gt;: distill lessons across rounds and write them into &lt;code&gt;c3: LongTermMEMORY.md&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It connects to your agent through a &lt;code&gt;BenchmarkAdapter&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;run()&lt;/code&gt; must answer "with this version of the harness loaded, what did the agent produce for this task, and what happened along the way?" For the business-card assistant, I wrote a Go &lt;code&gt;cmd/harness-runner&lt;/code&gt; subprocess CLI that the Python Harness² calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-leakage barrier&lt;/strong&gt;: gold answers are hidden from both the agent and the Harvester LLM, forcing it to infer general rules from trajectories.&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  3. DSH vs. Harness²: a point-by-point comparison
&lt;/h1&gt;

&lt;h2&gt;
  
  
  3.1 "Harness" means different things on each side
&lt;/h2&gt;

&lt;p&gt;This is the single most important point of the post:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Harness in DSH&lt;/th&gt;
&lt;th&gt;Harness in Harness²&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Refers to&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;The program that runs the agent&lt;/strong&gt;: agent loop, tool execution, model calls, sessions, permissions&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;What gets fed to the agent&lt;/strong&gt;: prompts, guardrails, memory, tool descriptions, skills&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form&lt;/td&gt;
&lt;td&gt;A Node process (runtime)&lt;/td&gt;
&lt;td&gt;A folder (&lt;code&gt;c1..c8&lt;/code&gt; files)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analogy&lt;/td&gt;
&lt;td&gt;How the buckles and reins of a harness are assembled&lt;/td&gt;
&lt;td&gt;How long each strap on the harness is adjusted&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who changes it&lt;/td&gt;
&lt;td&gt;Engineers write profiles / patches / plugins&lt;/td&gt;
&lt;td&gt;Harness²'s optimizer changes it itself&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the two are not competing alternatives; they are &lt;strong&gt;an upper and a lower layer&lt;/strong&gt;. The file tree Harness² optimizes must eventually be loaded and executed by some runtime, and DSH is exactly such a runtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  3.2 Full comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Aspect&lt;/th&gt;
&lt;th&gt;DeepSeek Harness (DSH)&lt;/th&gt;
&lt;th&gt;Google Research Harness²&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Origin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open-sourced by DeepSeek; the foundation of dsh-superagent&lt;/td&gt;
&lt;td&gt;Open-sourced alongside a Google Research paper (arXiv:2610.14224)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Language&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TypeScript / Node (plus a Python SDK)&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Role&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Agent runtime&lt;/td&gt;
&lt;td&gt;Offline optimizer for the agent's harness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Core abstractions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cordis plugins, Profile, Bundle, Patch&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;c1..c8&lt;/code&gt; components, &lt;code&gt;BenchmarkAdapter&lt;/code&gt;, Recursion&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Primary output&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The result of one agent run + session events&lt;/td&gt;
&lt;td&gt;A better harness file tree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;When it runs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Online, on every request&lt;/td&gt;
&lt;td&gt;Offline, over multiple rounds against an eval set&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Requires an eval set&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; (plus rubrics and trajectories)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Anthropic Messages-compatible endpoint (DeepSeek by default)&lt;/td&gt;
&lt;td&gt;The optimized agent can use any model; the Harvester is configured separately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Intrusiveness for existing agents&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High: the agent must run inside DSH&lt;/td&gt;
&lt;td&gt;Low: write an adapter and externalize config into files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;How you observe the agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Session events such as &lt;code&gt;tool/call&lt;/code&gt;, &lt;code&gt;tool/result&lt;/code&gt;, &lt;code&gt;turn/end&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;trajectory.md&lt;/code&gt; (generated from OpenTelemetry spans in the business-card assistant)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Maturity&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0.2.1-rc.5&lt;/code&gt;, pre-release; options get renamed&lt;/td&gt;
&lt;td&gt;Research code; interfaces oriented around the paper's experiments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;In this series&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Agent layer of the LINE research bot's online service&lt;/td&gt;
&lt;td&gt;Offline self-evolution of the LINE AI business-card assistant&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  3.3 Where do Harness²'s &lt;code&gt;c1..c8&lt;/code&gt; map to in DSH?
&lt;/h2&gt;

&lt;p&gt;Since DSH also "assembles a bunch of things into an agent", we can try mapping Harness²'s eight components onto it. The table below reflects my understanding from actually using DSH; &lt;strong&gt;where there is no counterpart in my usage, I say so&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Harness² component&lt;/th&gt;
&lt;th&gt;Where it lives in DSH&lt;/th&gt;
&lt;th&gt;How I used it in the LINE research bot&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;c1&lt;/code&gt; systemprompt.md&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;system-prompt&lt;/code&gt; plugin; the persona can be passed via &lt;code&gt;DSH_SYSTEM_PROMPT&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;prompts/persona.md&lt;/code&gt; as the persona; each of the three stages' instructions goes in the user message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;c2&lt;/code&gt; guardrails.md&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Two places: &lt;strong&gt;schema validation&lt;/strong&gt; of tool arguments (&lt;code&gt;required&lt;/code&gt;, &lt;code&gt;enum&lt;/code&gt;), and patches that set dangerous tools to &lt;code&gt;disabled: true&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;enum&lt;/code&gt; on &lt;code&gt;submit_report&lt;/code&gt;'s &lt;code&gt;error&lt;/code&gt; field rejects invalid values; bash and editor are disabled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;c3&lt;/code&gt; LongTermMEMORY.md&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;A DSH session is &lt;strong&gt;an event stream for a single run&lt;/strong&gt;, not cross-round memory&lt;/td&gt;
&lt;td&gt;Not used; cross-stage data lives in Firestore&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;c4&lt;/code&gt; tool_descriptions/&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;description&lt;/code&gt; and parameter schema in &lt;code&gt;defineTool()&lt;/code&gt;, which the model sees directly&lt;/td&gt;
&lt;td&gt;Descriptions for &lt;code&gt;web_research&lt;/code&gt; and &lt;code&gt;submit_*&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;c5&lt;/code&gt; sub_agents/&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;DSH has a built-in &lt;code&gt;subagent&lt;/code&gt; tool (visible in the full &lt;code&gt;sdk&lt;/code&gt; profile)&lt;/td&gt;
&lt;td&gt;Not used; also disabled in &lt;code&gt;sdk-minimal&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;c6&lt;/code&gt; skills/&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;I did not investigate an equivalent mechanism this time&lt;/td&gt;
&lt;td&gt;Not used&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;c7&lt;/code&gt; scripts/&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Equivalent to letting the agent run a shell (&lt;code&gt;persistent-bash&lt;/code&gt; and similar tools)&lt;/td&gt;
&lt;td&gt;Deliberately disabled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;c8&lt;/code&gt; plugin/&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;This is the core of DSH itself&lt;/strong&gt;: Cordis plugins&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;dsh/plugin/index.js&lt;/code&gt; registers the research tools&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most interesting finding from this mapping: &lt;strong&gt;Harness²'s &lt;code&gt;c8&lt;/code&gt; is the star in DSH&lt;/strong&gt;, while &lt;code&gt;c1..c3&lt;/code&gt;, which Harness² cares about most, get only basic attachment points in DSH. This mirrors their respective roles: DSH cares whether the &lt;em&gt;execution&lt;/em&gt; side can be assembled, while Harness² cares whether the &lt;em&gt;content&lt;/em&gt; side can evolve.&lt;/p&gt;

&lt;h2&gt;
  
  
  3.4 Common ground: both move the agent's "equipment" out of the code
&lt;/h2&gt;

&lt;p&gt;Despite operating at different layers, the two share several design instincts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Externalize configuration into files.&lt;/strong&gt; The first step in bringing Harness² to the business-card assistant was extracting the prompts hard-coded in &lt;code&gt;.go&lt;/code&gt; files into &lt;code&gt;harness/base/&lt;/code&gt;; DSH externalizes "which plugins to load and which tools to disable" into profiles + YAML patches. Neither wants you to recompile just to change agent behavior.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolate via subprocesses.&lt;/strong&gt; In the business-card assistant, Harness² calls a Go &lt;code&gt;harness-runner&lt;/code&gt; subprocess; DSH itself is a subprocess spawned by the SDK. Either way, the host can be written in any language.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both need visibility into the agent's process, not just its result.&lt;/strong&gt; Harness² relies on &lt;code&gt;trajectory.md&lt;/code&gt; for diagnosis; on DSH, I rely on session events to extract structured output and to verify that patches took effect. &lt;strong&gt;You can only optimize or debug what you can observe.&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  3.5 Differences: one governs &lt;em&gt;how it runs&lt;/em&gt;, the other &lt;em&gt;what it runs&lt;/em&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DSH's challenges are engineering problems&lt;/strong&gt;: plugin path resolution, renamed SDK options, a model layer limited to the Messages protocol. These are all about "getting it to run"; I cover ten pitfalls in &lt;a href="https://dev.to/gde/ai-in-practice-migrating-a-line-research-bot-from-go-adk-to-deepseek-harness-dsh-architecture-208e"&gt;the next post&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Harness²'s challenges are data problems&lt;/strong&gt;: a good enough eval set, rubrics, zero-leakage design, and high-resolution trajectories. These are all about "making it better".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DSH runs on every request&lt;/strong&gt;, so latency and cost matter; &lt;strong&gt;Harness² runs offline&lt;/strong&gt;, so it can spend more tokens to produce a better file tree, at zero cost once deployed.&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  4. Can they be combined?
&lt;/h1&gt;

&lt;p&gt;Yes, and conceptually it is quite natural. Taking the LINE research bot as an example, if I wanted it to self-evolve like the business-card assistant, it would look roughly like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNptUl1LwzAU_SuXPE1cu48nFRmMbSgiUrr5INaHtL213dok3KSTse2_e9tOZNM8JOHee07OOWQvEp2iuAORlforySU5eA4jBbxsHX-SNDkEb--ReJSk0NqoHg7jMfSCncu16oPOsrJQeBWJjw7VrHAxY0SISU220Oo-psEkIG20RRgA3-LmnOmqqZwhp_OAkXObT1NpHJJPter9IQfPmzSjXRFVGqkLyasl0xBalJTkwCQKCXqrncFlQoVx55QPixcen2m1RfafjHw_uYFCOd0qN0hWKwnX8EPoG-l431XlubLXhmaOaJaIm1Ng_zhYhU88Z7nJ4QBuUTkLUT0e3Y7BkVxj4jTt_Cq9FNn65lfak1n--OdMmt4hEp6Xd-97aUEctuc5aTfemp1E4tCQdQhmOSFSLAv2L-MS2eqvjma8zVr0QVRIlSxS_jB74XKs2q-TYibr0onj8RtudrY_%3Ftype%3Dpng" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmermaid.ink%2Fimg%2Fpako%3AeNptUl1LwzAU_SuXPE1cu48nFRmMbSgiUrr5INaHtL213dok3KSTse2_e9tOZNM8JOHee07OOWQvEp2iuAORlforySU5eA4jBbxsHX-SNDkEb--ReJSk0NqoHg7jMfSCncu16oPOsrJQeBWJjw7VrHAxY0SISU220Oo-psEkIG20RRgA3-LmnOmqqZwhp_OAkXObT1NpHJJPter9IQfPmzSjXRFVGqkLyasl0xBalJTkwCQKCXqrncFlQoVx55QPixcen2m1RfafjHw_uYFCOd0qN0hWKwnX8EPoG-l431XlubLXhmaOaJaIm1Ng_zhYhU88Z7nJ4QBuUTkLUT0e3Y7BkVxj4jTt_Cq9FNn65lfak1n--OdMmt4hEp6Xd-97aUEctuc5aTfemp1E4tCQdQhmOSFSLAv2L-MS2eqvjma8zVr0QVRIlSxS_jB74XKs2q-TYibr0onj8RtudrY_%3Ftype%3Dpng" alt="Combining Harness² with DSH via an adapter" width="1388" height="229"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Modeled on the business-card assistant's &lt;code&gt;cmd/harness-runner&lt;/code&gt;, write a TypeScript runner CLI that accepts &lt;code&gt;--harness-dir&lt;/code&gt; and &lt;code&gt;--task-json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The runner converts &lt;code&gt;c1&lt;/code&gt; into &lt;code&gt;DSH_SYSTEM_PROMPT&lt;/code&gt;, writes &lt;code&gt;c4&lt;/code&gt; into the tool descriptions, and turns part of &lt;code&gt;c2&lt;/code&gt; into a patch (e.g. which tools to disable).&lt;/li&gt;
&lt;li&gt;After the run, it renders session events such as &lt;code&gt;tool/call&lt;/code&gt; / &lt;code&gt;tool/result&lt;/code&gt; into &lt;code&gt;trajectory.md&lt;/code&gt; and returns it, together with the report, to the Python-side adapter.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I &lt;strong&gt;have not implemented this yet&lt;/strong&gt;; it simply seems feasible after putting the experience from both projects together. DSH's session events are already closer to "agent decisions" than OpenTelemetry spans, so they should work well as trajectories.&lt;/p&gt;




&lt;h1&gt;
  
  
  5. When to use which?
&lt;/h1&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your situation&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;You want an agent runtime that is composable, lets you disable dangerous tools, and lets you swap model endpoints&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;DSH&lt;/strong&gt; (note: it is pre-release, and the model layer only speaks Messages)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your agent is already in production and you want prompts, skills, and memory to evolve automatically based on evaluation results&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Harness²&lt;/strong&gt; (requires an eval set and execution trajectories)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You already run well on ADK and just want to improve quality&lt;/td&gt;
&lt;td&gt;Start with &lt;strong&gt;Harness²&lt;/strong&gt;; no need to switch frameworks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;You want both&lt;/td&gt;
&lt;td&gt;Use DSH as the runtime and Harness² as the offline optimizer, connected via an adapter + subprocess&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h1&gt;
  
  
  Summary
&lt;/h1&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Despite the shared name, DSH is a runtime, while Harness² is the content a runtime loads plus an optimizer for it.&lt;/strong&gt; Once this is clear, there is no point asking "which one is better".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DSH's design centers on composability&lt;/strong&gt;: Cordis plugins + profile layering mean disabling, swapping, or inserting any component takes just a few lines of YAML. The trade-off is that the agent runs in a separate process and must be driven over JSON-RPC.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Harness²'s design centers on evolvability&lt;/strong&gt;: it splits the agent's equipment into budget-constrained &lt;code&gt;c1..c8&lt;/code&gt; components and uses trajectory-driven, two-level recursion to improve them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Both tackle different facets of the same goal&lt;/strong&gt;: freeing agent behavior from code. One frees &lt;em&gt;how it runs&lt;/em&gt;; the other frees &lt;em&gt;what it runs&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The next post gets hands-on: &lt;strong&gt;&lt;a href="https://dev.to/gde/ai-in-practice-migrating-a-line-research-bot-from-go-adk-to-deepseek-harness-dsh-architecture-208e"&gt;Migrating a LINE Research Bot from Go ADK to DeepSeek Harness: Migration Mapping and Pitfalls&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/gde/agent-platform-in-action-7-when-adk-go-graph-workflow-meets-google-research-harness-part-1-28c1"&gt;Harness² Part 1: The two-level recursive agent harness framework and decoupling into c1..c8&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/gde/agent-platform-in-action-8-when-adk-go-graph-workflow-meets-google-research-harness-part-2-nc8"&gt;Harness² Part 2: LineCardAdapter, the zero-leakage barrier, and behavioral trajectories&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/gde/agent-platform-in-action-9-when-adk-go-graph-workflow-meets-google-research-harness-part-2-1olm"&gt;Harness² Part 3: From 62.5% to 100% with two-level recursion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/deepseek-ai/dsh-superagent-prototype" rel="noopener noreferrer"&gt;&lt;code&gt;deepseek-ai/dsh-superagent-prototype&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.deepseek.com/en/harness/" rel="noopener noreferrer"&gt;DeepSeek Harness&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/google-research/harness2" rel="noopener noreferrer"&gt;&lt;code&gt;google-research/harness2&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/kkdai/line-research-bot-dsh" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/line-research-bot-dsh&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>llm</category>
    </item>
    <item>
      <title>[Agent Platform in Action (7)] When ADK Go Graph Workflow Meets Google Research Harness (Part 1): Two-Layer Recursive Agent Fram</title>
      <dc:creator>Evan Lin</dc:creator>
      <pubDate>Sun, 01 Nov 2026 14:37:47 +0000</pubDate>
      <link>https://dev.to/gde/agent-platform-in-action-7-when-adk-go-graph-workflow-meets-google-research-harness-part-1-28c1</link>
      <guid>https://dev.to/gde/agent-platform-in-action-7-when-adk-go-graph-workflow-meets-google-research-harness-part-1-28c1</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftgngihh3qblqv1ey9xpn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftgngihh3qblqv1ey9xpn.png" alt="Harness² Part 1 Architecture: Current Architecture vs c1..c8 Editable Harness Tree" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Foreword: After the AI Agent Goes Online, What is the Next Bottleneck?
&lt;/h1&gt;

&lt;p&gt;Welcome back to the &lt;strong&gt;"Agent Platform in Action"&lt;/strong&gt; series! In the previous six articles, we took the &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant" rel="noopener noreferrer"&gt;LINE AI Card Assistant (&lt;code&gt;kkdai/line-card-assistant&lt;/code&gt;)&lt;/a&gt;&lt;/strong&gt; through the complete five stages from a standalone prototype to production-grade cloud deployment:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/2026/10/16/adk-graph-workflow-go/"&gt;[Agent Platform in Action (1)] Building LINE AI Card Assistant with Go and Google ADK 2.0 Graph Workflow&lt;/a&gt;&lt;/strong&gt;: Using &lt;code&gt;google.golang.org/adk/v2/workflow&lt;/code&gt; to build a 13-node directed graph featuring Vision OCR, &lt;code&gt;AddFanOut&lt;/code&gt; / &lt;code&gt;AddFanIn&lt;/code&gt; parallel background investigation, and &lt;code&gt;NewRequestInputEvent&lt;/code&gt; HITL interaction loops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/2026/10/17/adk-graph-workflow-go-2/"&gt;[Agent Platform in Action (2)] From Monolithic Cloud Run to Google Cloud Agent Runtime&lt;/a&gt;&lt;/strong&gt;: Completed &lt;strong&gt;Phase 1 (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/10" rel="noopener noreferrer"&gt;PR #10&lt;/a&gt;)&lt;/strong&gt;, standardizing the project into an &lt;code&gt;agents-cli&lt;/code&gt; (&lt;code&gt;adk_go&lt;/code&gt;) dual-channel architecture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/2026/10/20/adk-graph-workflow-go-3/"&gt;[Agent Platform in Action (3)] Implementing Cross-Instance Business Card Recognition and Pause/Resume with ADK 2.0 GCS Artifact Service and Agent Platform Sessions&lt;/a&gt;&lt;/strong&gt;: Completed the &lt;strong&gt;first half of Phase 2 (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/11" rel="noopener noreferrer"&gt;PR #11&lt;/a&gt;)&lt;/strong&gt;, solving the HITL "amnesia" of stateless containers and the issue of binary image data explosion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/2026/10/21/adk-graph-workflow-go-4/"&gt;[Agent Platform in Action (4)] Completing the Architecture Puzzle with Cloud Run LINE Webhook Gateway, Agent Runtime, and Cloud SQL PostgreSQL&lt;/a&gt;&lt;/strong&gt;: Completed the &lt;strong&gt;second half of Phase 2 &amp;amp; Phase 3 (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/12" rel="noopener noreferrer"&gt;PR #12&lt;/a&gt;)&lt;/strong&gt;, upgrading the business card database to Cloud SQL PostgreSQL and achieving two-layer stateless decoupling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/2026/10/22/adk-graph-workflow-go-5/"&gt;[Agent Platform in Action (5)] Establishing Automated Evaluation Benchmarks with agents-cli eval and ADK 2.0 Go&lt;/a&gt;&lt;/strong&gt;: Completed &lt;strong&gt;Phase 4 (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/13" rel="noopener noreferrer"&gt;PR #13&lt;/a&gt;)&lt;/strong&gt;, establishing a golden test set and a quality flywheel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/2026/10/24/adk-graph-workflow-go-6/"&gt;[Agent Platform in Action (6)] Building a Full-Stack Observable LINE AI Card Assistant with agents-cli deploy, Terraform IaC, GitHub Actions CI/CD, and Cloud Trace&lt;/a&gt;&lt;/strong&gt;: Completed &lt;strong&gt;Phase 5 (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/14" rel="noopener noreferrer"&gt;PR #14&lt;/a&gt;)&lt;/strong&gt;, adding OpenTelemetry Spans (&lt;code&gt;workflow.node.*&lt;/code&gt;) to all 13 Graph Nodes and deploying to the cloud via Terraform and GitHub Actions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Additionally, we have planned advanced Graph Workflow primitives for future ADK v2.6+ (such as &lt;code&gt;Parallel&lt;/code&gt; dynamic dispatch, &lt;code&gt;MultiRoute&lt;/code&gt; multi-way fan-out, reflection-repair loops, and strongly typed nodes) into the &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/issues/15" rel="noopener noreferrer"&gt;Future Roadmap Issue #15&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;However, now that our Agent possesses &lt;strong&gt;Phase 4 Automated Evaluation&lt;/strong&gt; and &lt;strong&gt;Phase 5 Full-Node Tracing (OpenTelemetry Spans)&lt;/strong&gt;, a deeper question emerges:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;When evaluations find that the Agent performs poorly in certain real-world cases (e.g., Japanese-English bilingual cards, searching for colloquial nicknames in the Taiwan tech industry, multi-clause colloquial HITL modifications, or financial compliance icebreaker letters), must engineers manually check logs, manually modify Prompt strings in the Go source code, and then recompile and redeploy every single time?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Can we let a &lt;strong&gt;Meta-Agent (Engineer Agent)&lt;/strong&gt; automatically read our evaluation results and OpenTelemetry execution trajectories, diagnose the reasons for lost points, write new Skills and Long-Term Memory itself, and complete a closed-loop self-evolution?&lt;/p&gt;

&lt;p&gt;This is precisely the core proposition addressed by Google Research's recently published and open-sourced &lt;strong&gt;&lt;a href="https://github.com/google-research/harness2" rel="noopener noreferrer"&gt;&lt;code&gt;google-research/harness2&lt;/code&gt; (&lt;em&gt;Harness²: Recursive Harnessing for LLM Agents&lt;/em&gt;, arXiv:2610.14224)&lt;/a&gt;&lt;/strong&gt;!&lt;/p&gt;

&lt;p&gt;This series will consist of three articles (corresponding to &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/issues/16" rel="noopener noreferrer"&gt;Issue #16&lt;/a&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/issues/17" rel="noopener noreferrer"&gt;Issue #17&lt;/a&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/issues/18" rel="noopener noreferrer"&gt;Issue #18&lt;/a&gt;&lt;/strong&gt;, and &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/pull/19" rel="noopener noreferrer"&gt;PR #19&lt;/a&gt;&lt;/strong&gt;), guiding you through the complete integration of &lt;strong&gt;Google Research &lt;code&gt;Harness²&lt;/code&gt;&lt;/strong&gt; into our Go ADK 2.0 project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Part 1 (This article, &lt;a href="https://github.com/kkdai/line-card-assistant/issues/16" rel="noopener noreferrer"&gt;Issue #16&lt;/a&gt;)&lt;/strong&gt;: What is Google &lt;code&gt;Harness²&lt;/code&gt;? What can it do? Inventorying the existing architecture before modification and decoupling hardcoded configurations in the Go binary into a &lt;code&gt;c1..c8&lt;/code&gt; editable Harness file tree.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 2 (&lt;a href="https://github.com/kkdai/line-card-assistant/issues/17" rel="noopener noreferrer"&gt;Issue #17&lt;/a&gt;)&lt;/strong&gt;: Encapsulating Phase 4's &lt;code&gt;eval/datasets/&lt;/code&gt; and Phase 5's OpenTelemetry Spans into a &lt;code&gt;LineCardAdapter&lt;/code&gt; for &lt;code&gt;Harness²&lt;/code&gt;, and analyzing why "Zero-Leakage Barriers" and "High-Resolution Behavioral Trajectories" are both indispensable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Part 3 (&lt;a href="https://github.com/kkdai/line-card-assistant/issues/18" rel="noopener noreferrer"&gt;Issue #18&lt;/a&gt;)&lt;/strong&gt;: Complete testing tutorial, disclosure of &lt;code&gt;Parallel&lt;/code&gt; vs &lt;code&gt;Sequential + LongTermMEMORY&lt;/code&gt; two-layer recursive experimental data (accuracy jumping from &lt;code&gt;62.5%&lt;/code&gt; to &lt;code&gt;100.0%&lt;/code&gt;), and outlook on the future development of Agents.&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  1. What is Google Research &lt;code&gt;Harness²&lt;/code&gt;? What can it do?
&lt;/h1&gt;

&lt;h2&gt;
  
  
  1. From "Fine-tuning Model Weights" to "Optimizing Agent Harnessing"
&lt;/h2&gt;

&lt;p&gt;In the era of Large Language Models (LLMs), the success of an AI Agent is often determined not just by the underlying model (like Gemini 2.5 Flash / Pro) itself, but by the &lt;strong&gt;"Agent Harness"&lt;/strong&gt; layer wrapped around the model—namely, System Prompts, formatting and safety Guardrails, Long-Term Memory, Tool Descriptions, Sub-Agent settings, and On-Demand Skills.&lt;/p&gt;

&lt;p&gt;Past automated prompt optimizations (like PromptBreeder, TextGrad, or DSPy) mostly targeted mutations of a &lt;strong&gt;single Prompt string&lt;/strong&gt;. However, in a real enterprise-grade Agent, if you stuff all edge-case patching rules into the same System Prompt, it quickly leads to &lt;strong&gt;"Context Bloat"&lt;/strong&gt;: the number of Always-On Tokens increases, which not only squeezes the model's Output Token Budget but also causes different rules to interfere with each other!&lt;/p&gt;

&lt;p&gt;Google Research's &lt;strong&gt;&lt;code&gt;Harness²&lt;/code&gt; (&lt;em&gt;Recursive Harnessing for LLM Agents&lt;/em&gt;, arXiv:2610.14224)&lt;/strong&gt; breaks this limitation. It defines the entire Agent's equipment as a structured &lt;strong&gt;&lt;code&gt;c1..c8&lt;/code&gt; Editable Component Tree&lt;/strong&gt; and automatically evolves this file tree through &lt;strong&gt;Two-Level Recursion&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dot"&gt;&lt;code&gt;&lt;span class="nv"&gt;flowchart&lt;/span&gt; &lt;span class="nv"&gt;TB&lt;/span&gt;
    &lt;span class="k"&gt;subgraph&lt;/span&gt; &lt;span class="nv"&gt;Outer&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Outer Recursion: Harnessing-Level Recursion (Experience accumulation across rounds k=1..K)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;Reflector&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Experience Reflector\n(ReflectorPrompt)\nDistilling success and failure patterns across rounds"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;Memory&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"c3: LongTermMEMORY.md\nPersisting domain experience across iterations"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;Reflector&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;|&lt;/span&gt;&lt;span class="nv"&gt;Write&lt;/span&gt; &lt;span class="nv"&gt;long&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;term&lt;/span&gt; &lt;span class="nv"&gt;rules&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Memory&lt;/span&gt;
    &lt;span class="nv"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;subgraph&lt;/span&gt; &lt;span class="nv"&gt;Inner&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Inner Recursion: Task-Level Recursion (Closed-loop diagnosis and patching within a single round)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;Propose&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"1. Propose (Diagnostic Proposal)\nRead trajectory.md\nLocate behavioral defects and propose N candidate Patches"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;Probe&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"2. Probe (Sandbox Probing)\nExecute N candidate Harnesses in parallel\nCompare trajectory and score changes before and after modification"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;Compose&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"3. Compose (Conflict Resolution and Synthesis)\nRetain positive-gain Patches, discard negative interference\nSynthesize the next-generation best Harness H^(k)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;Propose&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Probe&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Compose&lt;/span&gt;
    &lt;span class="nv"&gt;end&lt;/span&gt;

    &lt;span class="nv"&gt;Memory&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Propose&lt;/span&gt;
    &lt;span class="nv"&gt;Compose&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Reflector&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. The &lt;code&gt;c1..c8&lt;/code&gt; Eight Harness Components and the 24KB Budget Limit
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://github.com/google-research/harness2/blob/main/harness2/harness.py" rel="noopener noreferrer"&gt;&lt;code&gt;google-research/harness2/harness.py&lt;/code&gt;&lt;/a&gt;, a standard Agent Harness consists of &lt;strong&gt;4 Always-On Components (&lt;code&gt;c1..c4&lt;/code&gt;)&lt;/strong&gt; and &lt;strong&gt;4 On-Demand Components (&lt;code&gt;c5..c8&lt;/code&gt;)&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component Code&lt;/th&gt;
&lt;th&gt;File Path Rule&lt;/th&gt;
&lt;th&gt;Loading Mode&lt;/th&gt;
&lt;th&gt;Responsibility in &lt;code&gt;Harness²&lt;/code&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;c1&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;systemprompt.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Always-On&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Defines core roles, node responsibilities, and indexing rules for triggering &lt;code&gt;c6&lt;/code&gt; skills (Skill Trigger Lines).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;c2&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;guardrails.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Always-On&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Defines non-violable output formats (e.g., pure JSON constraints), E.164 normalization invariants, and security/PII guardrails.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;c3&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;LongTermMEMORY.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Always-On&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Domain lessons and industry compliance memories written across rounds by the outer &lt;code&gt;Experience Reflector&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;c4&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tool_descriptions/*.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Always-On&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tool parameter descriptions, search strategies, and normalization details (e.g., Japan long-distance prefix &lt;code&gt;(0)&lt;/code&gt; cleaning rules).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;c5&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sub_agents/&amp;lt;name&amp;gt;/AGENT.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;On-Demand&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Exclusive Instructions and YAML Frontmatter (&lt;code&gt;mode: subagent&lt;/code&gt;) for delegated sub-agents (e.g., &lt;code&gt;company_enrich&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;c6&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;On-Demand&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Professional skills dynamically injected only for specific domains or tasks (e.g., Japanese card OCR, Hsinchu Science Park colloquial search expansion).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;c7&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;scripts/*.py&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;On-Demand&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Auxiliary verification or data conversion scripts.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;c8&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;plugin/*.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;On-Demand&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Runtime lifecycle and OpenTelemetry diagnostic hooks.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Important&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why distinguish between &lt;code&gt;c1..c4&lt;/code&gt; (Always-On) and &lt;code&gt;c5..c8&lt;/code&gt; (On-Demand)?&lt;/strong&gt; &lt;code&gt;Harness²&lt;/code&gt; sets an iron rule in &lt;code&gt;OPENCODE_SURFACE&lt;/code&gt;: &lt;strong&gt;The total byte count of &lt;code&gt;c1..c4&lt;/code&gt; always-on files must never exceed &lt;code&gt;24,000 bytes&lt;/code&gt; (&lt;code&gt;AlwaysOnMaxBytes = 24000&lt;/code&gt;)&lt;/strong&gt;! When &lt;code&gt;Harness²&lt;/code&gt; finds that a failure case requires lengthy domain knowledge (e.g., a department title mapping table for Japanese corporate cards or a colloquial abbreviation dictionary for Taiwan semiconductor parks), it won't force these rules into &lt;code&gt;systemprompt.md&lt;/code&gt; causing Context explosion. Instead, it will automatically create an on-demand &lt;strong&gt;&lt;code&gt;c6&lt;/code&gt; skill file (&lt;code&gt;skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt;)&lt;/strong&gt; and leave only a lightweight Trigger Line in &lt;code&gt;systemprompt.md&lt;/code&gt;!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  3. How Does Two-Level Recursion Work?
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Harness²&lt;/code&gt; evolution engine (&lt;a href="https://github.com/google-research/harness2/blob/main/harness2/core/recursion.py" rel="noopener noreferrer"&gt;&lt;code&gt;harness2/core/recursion.py&lt;/code&gt;&lt;/a&gt;) includes two levels of recursion:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Level 1 (Inner): Task-Level Recursion (&lt;code&gt;Propose&lt;/code&gt; $\rightarrow$ &lt;code&gt;Probe&lt;/code&gt; $\rightarrow$ &lt;code&gt;Compose&lt;/code&gt;)&lt;/strong&gt;: 

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Propose&lt;/code&gt; (Diagnostic Proposal)&lt;/strong&gt;: The optimizer (Harvester LLM) observes tasks that lost points in the benchmark, the final output (Deliverable), and the &lt;strong&gt;execution trajectory (&lt;code&gt;trajectory.md&lt;/code&gt;)&lt;/strong&gt;. It diagnoses which step the Agent made a wrong decision on and proposes multiple independent candidate patches (Candidate Patches, each being a set of additions or modifications to &lt;code&gt;c1..c8&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Probe&lt;/code&gt; (Sandbox Probing)&lt;/strong&gt;: The system mounts each candidate Patch into a sandbox and runs the task once, collecting new scores and new execution trajectories to form a "Before vs. After" Behavioral Contrast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Compose&lt;/code&gt; (Conflict Resolution and Synthesis)&lt;/strong&gt;: The optimizer compares the actual performance of all candidate Patches, merges modifications that truly bring positive score improvements into the new generation Harness $H^{(k)}$, and discards ineffective or side-effect-inducing modifications.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Level 2 (Outer): Harnessing-Level Recursion (&lt;code&gt;Experience Reflector&lt;/code&gt;)&lt;/strong&gt;: 

&lt;ul&gt;
&lt;li&gt;Between multiple iterations ($k = 1 \dots K$), the outer &lt;code&gt;ReflectorPrompt&lt;/code&gt; reviews the evolution history from $H^{(0)} \to H^{(1)} \to \dots \to H^{(k)}$: Which types of modifications succeeded? Which cross-domain constraints (e.g., financial industry compliance headers and personal data protection regulations) need to be remembered at a global level?&lt;/li&gt;
&lt;li&gt;Distilled high-level experiences are written into &lt;strong&gt;&lt;code&gt;c3: LongTermMEMORY.md&lt;/code&gt;&lt;/strong&gt;, guiding the next round of &lt;code&gt;Propose&lt;/code&gt; and Agent execution.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h1&gt;
  
  
  2. Inventorying the Current Architecture: Why Phase 1~5 Go Implementation Cannot Be Directly Optimized by &lt;code&gt;Harness²&lt;/code&gt;?
&lt;/h1&gt;

&lt;p&gt;Let's compare the &lt;strong&gt;left side (Before: Hardcoded Go Binary Architecture)&lt;/strong&gt; of the architecture diagram at the beginning of the article and review the system architecture we completed in Phase 1 to Phase 5:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dot"&gt;&lt;code&gt;&lt;span class="nv"&gt;flowchart&lt;/span&gt; &lt;span class="nv"&gt;LR&lt;/span&gt;
    &lt;span class="k"&gt;subgraph&lt;/span&gt; &lt;span class="nv"&gt;Before&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Before: Phase 1~5 Solid Compilation Architecture (Hardcoded Go Binary)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;direction&lt;/span&gt; &lt;span class="nv"&gt;TB&lt;/span&gt;
        &lt;span class="nv"&gt;N1&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"nodes_ingest.go\nHardcoded VisionOCR / CleanAndDedup /\nCompanyEnrich / ApplyEdit Prompts"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;N2&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"nodes_search.go\nHardcoded QueryIntent /\nEmailDraft Prompts"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;N3&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"graph.go\nFixed SubAgent Instruction"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;E&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"eval/datasets/basic-dataset.json\nPhase 4 Golden Test Set"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;T&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"telemetry.go\nPhase 5 OpenTelemetry 13-Node Spans"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;end&lt;/span&gt;

    &lt;span class="nv"&gt;Problem&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Pain Points:\n1. Prompts hardcoded in .go string constants; re-go build required for every adjustment\n2. Lack of on-demand c6 Skills mechanism\n3. Phase 4 Eval and Phase 5 OTel Spans not yet connected into an automated optimization loop"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;Before&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Problem&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the code before modification:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;All Prompt templates for &lt;strong&gt;&lt;code&gt;VisionOCRNode&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;CompanyEnrichNode&lt;/code&gt;&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;ApplyEditNode&lt;/code&gt;&lt;/strong&gt; (located in &lt;code&gt;internal/workflow/nodes_ingest.go&lt;/code&gt;), as well as &lt;strong&gt;&lt;code&gt;QueryIntentNode&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;EmailDraftNode&lt;/code&gt;&lt;/strong&gt; (located in &lt;code&gt;internal/workflow/nodes_search.go&lt;/code&gt;), are hardcoded as &lt;code&gt;fmt.Sprintf(...)&lt;/code&gt; string constants in the Go code.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;llmagent.Config{Instruction: ...}&lt;/code&gt; for &lt;strong&gt;&lt;code&gt;CompanyEnrichAgent&lt;/code&gt;&lt;/strong&gt; (located in &lt;code&gt;internal/workflow/nodes_ingest.go&lt;/code&gt;) is also a fixed string hardcoded in a Go function.&lt;/li&gt;
&lt;li&gt;When we want the &lt;code&gt;Harness²&lt;/code&gt; Python evolution loop to modify system prompts, add a Japanese business card recognition skill (&lt;code&gt;skills/jp_card_ocr/SKILL.md&lt;/code&gt;), or adjust phone number normalization guidelines (&lt;code&gt;tool_descriptions/tool_guidance.md&lt;/code&gt;), &lt;strong&gt;it is impossible for &lt;code&gt;Harness²&lt;/code&gt; to rewrite our &lt;code&gt;.go&lt;/code&gt; source code and &lt;code&gt;go build&lt;/code&gt; again for every single task&lt;/strong&gt;!&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Therefore, the first step in introducing &lt;code&gt;Harness²&lt;/code&gt; (&lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/issues/16" rel="noopener noreferrer"&gt;Issue #16&lt;/a&gt;&lt;/strong&gt;) is to completely decouple the Go compilation phase from the Harness equipment phase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Go Binary is responsible for "Graph Topology and Dual-Channel Execution Engine"&lt;/strong&gt;: &lt;code&gt;RouterNode&lt;/code&gt;, parallel Fan-Out/Fan-In, &lt;code&gt;HITLConfirmNode&lt;/code&gt; pause/resume, Cloud SQL and GCS Artifact access, OpenTelemetry instrumentation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External &lt;code&gt;c1..c8&lt;/code&gt; Harness file tree is responsible for the "Evolvable Brain and Skill Equipment"&lt;/strong&gt;: Dynamically hot-reloaded at runtime via the environment variable &lt;code&gt;LINE_CARD_HARNESS_DIR&lt;/code&gt;!&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  3. Implementation Part 1: Building &lt;code&gt;internal/harness&lt;/code&gt; Loader and &lt;code&gt;harness/base/&lt;/code&gt; Initial Equipment Tree
&lt;/h1&gt;

&lt;p&gt;To make the Go ADK 2.0 Graph Workflow natively understand the &lt;code&gt;Harness²&lt;/code&gt; &lt;code&gt;c1..c8&lt;/code&gt; specification, we added two core directories to the project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/kkdai/line-card-assistant/tree/main/harness/base" rel="noopener noreferrer"&gt;&lt;code&gt;harness/base/&lt;/code&gt;&lt;/a&gt;: Stores the initial unevolved ($k=0$) &lt;code&gt;c1..c8&lt;/code&gt; baseline file tree.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/harness/harness.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/harness/harness.go&lt;/code&gt;&lt;/a&gt;: Responsible for loading the &lt;code&gt;c1..c8&lt;/code&gt; file tree, validating &lt;code&gt;OPENCODE_SURFACE&lt;/code&gt; 24KB limits and YAML Frontmatter rules, and dynamically combining always-on context with on-demand skills for each Graph Node.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. Establishing the &lt;code&gt;harness/base/&lt;/code&gt; Baseline &lt;code&gt;c1..c8&lt;/code&gt; File Tree
&lt;/h2&gt;

&lt;p&gt;We extracted the basic settings previously scattered in &lt;code&gt;nodes_ingest.go&lt;/code&gt; and &lt;code&gt;nodes_search.go&lt;/code&gt; into the &lt;code&gt;harness/base/&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;harness/base/
├── systemprompt.md # c1: Core roles and overview of Graph Node responsibilities
├── guardrails.md # c2: JSON output format and phone normalization guardrails
├── LongTermMEMORY.md # c3: Cross-round long-term memory (initially empty, to be written by Reflector)
├── tool_descriptions/
│ └── tool_guidance.md # c4: Guidance for google_search and format_company_profile
├── sub_agents/
│ └── company_enrich/
│ └── AGENT.md # c5: CompanyEnrichAgent delegated sub-agent definition
├── skills/ # c6: On-demand skills directory (to be generated by Propose-Probe-Compose evolution)
├── scripts/ # c7: Auxiliary scripts directory
└── plugin/ # c8: Diagnostic plugin directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, &lt;code&gt;harness/base/sub_agents/company_enrich/AGENT.md&lt;/code&gt; uses a YAML Frontmatter format fully compatible with &lt;code&gt;google-research/harness2&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;subagent&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Searches Google to summarize company background and industry tags.&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="s"&gt;You are a professional business analysis assistant. Please use the google_search tool to query the company name provided by the user, then call the format_company_profile tool to return a two-sentence Traditional Chinese company profile and 3 to 5 technical or industry tags (e.g., AI, Cloud, SaaS, FinTech, Semiconductor).&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Implementing &lt;code&gt;internal/harness/harness.go&lt;/code&gt;: 24KB Always-On Budget Validation and On-Demand Skill Injection
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/harness/harness.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/harness/harness.go&lt;/code&gt;&lt;/a&gt;, we implemented the validator &lt;code&gt;Validate()&lt;/code&gt; and node prompt synthesizer &lt;code&gt;ComposeNodePrompt()&lt;/code&gt; fully aligned with the Python version of &lt;code&gt;OPENCODE_SURFACE.validate()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// AlwaysOnMaxBytes matches google-research/harness2 OPENCODE_SURFACE.always_on_max (24,000 bytes).&lt;/span&gt;
&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;AlwaysOnMaxBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;24000&lt;/span&gt;

&lt;span class="c"&gt;// AlwaysOnFiles lists the four c1..c4 always-on files mounted into the agent context.&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;AlwaysOnFiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"systemprompt.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"guardrails.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"LongTermMEMORY.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"tool_descriptions/tool_guidance.md"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// Validate enforces Google Research Harness² OPENCODE_SURFACE invariants on the c1..c8 tree.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Harness&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Validate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reqFile&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;AlwaysOnFiles&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Files&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;reqFile&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"missing or empty always-on component %q"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reqFile&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AlwaysOnBytes&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;AlwaysOnMaxBytes&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"always-on c1..c4 byte count %d exceeds cap %d (every token here competes with the agent's output budget)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AlwaysOnMaxBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;combinedAlwaysOn&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SystemPrompt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Guardrails&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;dirName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skill&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Skills&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;skill&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;dirName&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%s frontmatter name %q must match directory name %q"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skill&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RelPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skill&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dirName&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skill&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%s must include a non-empty description in YAML frontmatter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skill&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RelPath&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;combinedAlwaysOn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dirName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"skill %q is never referenced in systemprompt.md or guardrails.md; add a trigger line naming `%s`"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dirName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dirName&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are two very critical engineering details here:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Mandatory Skill Trigger Reference Check&lt;/strong&gt;: If &lt;code&gt;Harness²&lt;/code&gt; adds a skill in &lt;code&gt;skills/jp_card_ocr/SKILL.md&lt;/code&gt; but forgets to write when to trigger &lt;code&gt;jp_card_ocr&lt;/code&gt; in &lt;code&gt;systemprompt.md&lt;/code&gt; or &lt;code&gt;guardrails.md&lt;/code&gt;, &lt;code&gt;Validate()&lt;/code&gt; will directly report an error and intercept it, avoiding the creation of "Orphan Skills" that will never be called.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node-Level Dynamic Skill Matching (&lt;code&gt;MatchSkills&lt;/code&gt; + &lt;code&gt;ComposeNodePrompt&lt;/code&gt;)&lt;/strong&gt;: When the Graph Workflow executes &lt;code&gt;VisionOCRNode&lt;/code&gt;, &lt;code&gt;CompanyEnrichNode&lt;/code&gt;, &lt;code&gt;ApplyEditNode&lt;/code&gt;, &lt;code&gt;QueryIntentNode&lt;/code&gt;, or &lt;code&gt;EmailDraftNode&lt;/code&gt;, it only injects &lt;code&gt;c6&lt;/code&gt; skills related to that node and automatically attaches &lt;code&gt;adk.harness.skills&lt;/code&gt; and &lt;code&gt;adk.harness.always_on_bytes&lt;/code&gt; attributes to the OpenTelemetry Span:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// composeHarnessPrompt enriches a node's base prompt with the active Harness² c1..c8 tree&lt;/span&gt;
&lt;span class="c"&gt;// and records the active harness metadata onto the node's OpenTelemetry span.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;composeHarnessPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Span&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nodeName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;basePrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;taskInput&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;harness&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Harness&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;harness&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoadActive&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;composed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;skills&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ComposeNodePrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nodeName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;basePrompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;taskInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetAttributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;AttrHarnessRoot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RootDir&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;AttrHarnessAlwaysOnBytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AlwaysOnBytes&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skills&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetAttributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AttrHarnessSkills&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StringSlice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skills&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;composed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can directly use the CLI to verify the always-on byte count and structural validity of &lt;code&gt;harness/base&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;go run ./cmd/harness-runner &lt;span class="nt"&gt;--validate-only&lt;/span&gt; &lt;span class="nt"&gt;--harness-dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;harness/base
OK &lt;span class="o"&gt;(&lt;/span&gt;1599 always-on bytes&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  4. Summary and Next Article Preview
&lt;/h1&gt;

&lt;p&gt;In this article (Part 1), we completed the goals of &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/issues/16" rel="noopener noreferrer"&gt;Issue #16&lt;/a&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Gained a deep understanding of how Google Research &lt;strong&gt;&lt;code&gt;Harness²&lt;/code&gt; (arXiv:2610.14224)&lt;/strong&gt; automatically evolves Agent capabilities through a &lt;strong&gt;&lt;code&gt;c1..c8&lt;/code&gt; editable file tree&lt;/strong&gt; and &lt;strong&gt;two-level recursion (Task-level &lt;code&gt;Propose-Probe-Compose&lt;/code&gt; + Harnessing-level &lt;code&gt;Experience Reflector&lt;/code&gt;)&lt;/strong&gt;, without recompiling the main program or bloating the always-on context.&lt;/li&gt;
&lt;li&gt;Inventoried the architectural bottlenecks of Phase 1~5 where prompts and normalization logic were hardcoded in the Go binary.&lt;/li&gt;
&lt;li&gt;Implemented &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/harness/harness.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/harness/harness.go&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/kkdai/line-card-assistant/tree/main/harness/base" rel="noopener noreferrer"&gt;&lt;code&gt;harness/base/&lt;/code&gt;&lt;/a&gt;, allowing the 13 nodes of the ADK 2.0 Go Graph Workflow to support dynamic mounting of the &lt;code&gt;c1..c8&lt;/code&gt; equipment tree via &lt;code&gt;LINE_CARD_HARNESS_DIR&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;With a hot-reloadable &lt;code&gt;c1..c8&lt;/code&gt; equipment tree in place, the next article &lt;strong&gt;&lt;a href="https://dev.to/2026/10/26/adk-graph-workflow-go-8/"&gt;[Agent Platform in Action (8)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 2): Turning Phase 4 Eval Datasets and Phase 5 OpenTelemetry Spans into LineCardAdapter&lt;/a&gt;&lt;/strong&gt; will enter &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/issues/17" rel="noopener noreferrer"&gt;Issue #17&lt;/a&gt;&lt;/strong&gt;: revealing why we need to encapsulate Phase 4 test sets and Phase 5 OpenTelemetry Spans into a &lt;code&gt;LineCardAdapter&lt;/code&gt;, and how the "Zero-Leakage Barrier" prevents the AI optimizer from cheating!&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>architecture</category>
      <category>go</category>
    </item>
    <item>
      <title>[Agent Platform in Action (8)] When ADK Go Graph Workflow Meets Google Research Harness (Part 2): Integrating Phase 4 Eval Datas</title>
      <dc:creator>Evan Lin</dc:creator>
      <pubDate>Sun, 01 Nov 2026 14:37:19 +0000</pubDate>
      <link>https://dev.to/gde/agent-platform-in-action-8-when-adk-go-graph-workflow-meets-google-research-harness-part-2-nc8</link>
      <guid>https://dev.to/gde/agent-platform-in-action-8-when-adk-go-graph-workflow-meets-google-research-harness-part-2-nc8</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyxdjol1nvvsed1qzztso.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyxdjol1nvvsed1qzztso.png" alt="Harness² Part 2: LineCardAdapter Bridging Phase 4 Eval &amp;amp; Phase 5 OpenTelemetry Spans" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Introduction: Why Decoupling Prompts Isn't Enough?
&lt;/h1&gt;

&lt;p&gt;In the previous post &lt;strong&gt;&lt;a href="https://dev.to/2026/10/25/adk-graph-workflow-go-7/"&gt;[Agent Platform in Practice (7)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 1): Understanding the Recursive Agent Framework and c1..c8 Decoupling&lt;/a&gt;&lt;/strong&gt; (corresponding to &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/issues/16" rel="noopener noreferrer"&gt;Issue #16&lt;/a&gt;&lt;/strong&gt;), we introduced Google Research's &lt;strong&gt;&lt;a href="https://github.com/google-research/harness2" rel="noopener noreferrer"&gt;&lt;code&gt;google-research/harness2&lt;/code&gt; (arXiv:2610.14224)&lt;/a&gt;&lt;/strong&gt; and decoupled the prompts, guardrails, long-term memory, and on-demand skills of the &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant" rel="noopener noreferrer"&gt;LINE AI Business Card Assistant (&lt;code&gt;kkdai/line-card-assistant&lt;/code&gt;)&lt;/a&gt;&lt;/strong&gt; into an &lt;strong&gt;editable &lt;code&gt;c1..c8&lt;/code&gt; Harness file tree&lt;/strong&gt; that can be dynamically mounted via &lt;code&gt;LINE_CARD_HARNESS_DIR&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, simply having a hot-reloadable &lt;code&gt;c1..c8&lt;/code&gt; file tree isn't enough; the &lt;code&gt;Harness²&lt;/code&gt; dual-layer recursive optimization engine (&lt;a href="https://github.com/google-research/harness2/blob/main/harness2/core/recursion.py" rel="noopener noreferrer"&gt;&lt;code&gt;harness2/core/recursion.py&lt;/code&gt;&lt;/a&gt;) still cannot directly optimize our Go Agent. This is because &lt;code&gt;Harness²&lt;/code&gt; requires a standardized bridge — the &lt;strong&gt;&lt;code&gt;BenchmarkAdapter&lt;/code&gt;&lt;/strong&gt; — to answer three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;&lt;code&gt;list_tasks()&lt;/code&gt; (What are the test questions?)&lt;/strong&gt;: What test cases should the Agent challenge? What are the public inputs (Prompt / Image) for each case, and what are the hidden scoring criteria (Rubrics)?&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;&lt;code&gt;run()&lt;/code&gt; (How does the Agent think and execute?)&lt;/strong&gt;: When a candidate Harness $H’$ is mounted to execute a specific task, what is the Agent's final output (&lt;code&gt;deliverable&lt;/code&gt;)? More importantly, &lt;strong&gt;what decision trajectory did it follow across the 13 nodes of the Graph Workflow (&lt;code&gt;trajectory.md&lt;/code&gt;)&lt;/strong&gt;?&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;&lt;code&gt;score()&lt;/code&gt; (How was the performance?)&lt;/strong&gt;: Which independent scoring items (&lt;code&gt;per_rubric&lt;/code&gt;) did the final output and execution trajectory pass, and where did it lose points?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Conveniently, in &lt;strong&gt;Phase 4 (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/13" rel="noopener noreferrer"&gt;PR #13&lt;/a&gt;)&lt;/strong&gt;, we already established the &lt;code&gt;eval/datasets/&lt;/code&gt; automated evaluation benchmark, and in &lt;strong&gt;Phase 5 (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/14" rel="noopener noreferrer"&gt;PR #14&lt;/a&gt;)&lt;/strong&gt;, we added &lt;code&gt;workflow.node.*&lt;/code&gt; OpenTelemetry Spans to all 13 Graph Nodes!&lt;/p&gt;

&lt;p&gt;In this post (Part 2, corresponding to &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/issues/17" rel="noopener noreferrer"&gt;Issue #17&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/pull/19" rel="noopener noreferrer"&gt;PR #19&lt;/a&gt;&lt;/strong&gt;), we will merge these two existing assets into the &lt;code&gt;Harness²&lt;/code&gt; &lt;strong&gt;&lt;code&gt;LineCardAdapter&lt;/code&gt;&lt;/strong&gt; and explain in depth &lt;strong&gt;"Why must it be designed this way?"&lt;/strong&gt;.&lt;/p&gt;




&lt;h1&gt;
  
  
  I. Why This Change? Two Core Design Philosophies
&lt;/h1&gt;

&lt;p&gt;Before diving into the code, many engineers often ask two questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;em&gt;"Why not just feed the entire &lt;code&gt;eval/datasets/*.json&lt;/code&gt; file to the optimizer LLM and let it modify the Prompt itself?"&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt; &lt;em&gt;"Why do we need to convert Phase 5 OpenTelemetry Spans into &lt;code&gt;trajectory.md&lt;/code&gt;? Isn't the final LINE reply text and total score enough?"&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These two questions touch upon the most core engineering designs in the &lt;code&gt;Harness²&lt;/code&gt; paper: the &lt;strong&gt;Zero-Leakage Barrier&lt;/strong&gt; and &lt;strong&gt;Behavioral Trajectory Contrast&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Why the "Zero-Leakage Barrier"? Preventing the Optimizer from "Cheating by Memorizing Answers"
&lt;/h2&gt;

&lt;p&gt;Imagine this: if we expose the golden answers in &lt;code&gt;eval/datasets/harness2-challenge-dataset.json&lt;/code&gt; (e.g., &lt;code&gt;expected_contains: ["+81-3-1234-5678", "Kenta Sato"]&lt;/code&gt;) directly to the Agent or the &lt;code&gt;Harness²&lt;/code&gt; Harvester LLM, what happens?&lt;/p&gt;

&lt;p&gt;The laziest and easiest way for the optimizer to get a perfect score is to write a single line in &lt;code&gt;systemprompt.md&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"If the input contains Sato, please directly output &lt;code&gt;+81-3-1234-5678&lt;/code&gt; and &lt;code&gt;Kenta Sato&lt;/code&gt;."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In machine learning, this is known as &lt;strong&gt;Test Set Leakage / Overfitting to Rubrics&lt;/strong&gt;. The Agent appears to score 100 on the benchmark, but it crashes immediately when it encounters a new Japanese business card in the real world!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dot"&gt;&lt;code&gt;&lt;span class="nv"&gt;flowchart&lt;/span&gt; &lt;span class="nv"&gt;LR&lt;/span&gt;
    &lt;span class="nv"&gt;Dataset&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"eval/datasets/*.json\nComplete Test Cases\n(incl. expected_contains / expected_route)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;subgraph&lt;/span&gt; &lt;span class="nv"&gt;Barrier&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Zero-Leakage Barrier"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;Brief&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"TaskBrief (Public Task Sheet)\nContains only task_id, prompt, session_input\n❌ Never contains expected_* golden answers"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;Scorer&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Isolated Scorer (Black-box Grader)\nHolds independent hidden r1..r4 Rubrics"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;end&lt;/span&gt;

    &lt;span class="nv"&gt;Agent&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Go ADK Graph Workflow\n(cmd/harness-runner)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;Harvester&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Harness² Harvester\n(Propose / Probe / Compose)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;

    &lt;span class="nv"&gt;Dataset&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Brief&lt;/span&gt;
    &lt;span class="nv"&gt;Dataset&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Scorer&lt;/span&gt;
    &lt;span class="nv"&gt;Brief&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;|&lt;/span&gt;&lt;span class="s2"&gt;"Public Input"&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Agent&lt;/span&gt;
    &lt;span class="nv"&gt;Agent&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;|&lt;/span&gt;&lt;span class="s2"&gt;"Deliverable + OTel trajectory.md"&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Scorer&lt;/span&gt;
    &lt;span class="nv"&gt;Scorer&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;|&lt;/span&gt;&lt;span class="s2"&gt;"Returns only binary scores {r1: 1.0, r2: 0.0}\nand OTel execution trajectory"&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;Harvester&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;Harness²&lt;/code&gt; &lt;code&gt;BenchmarkAdapter&lt;/code&gt; architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;TaskBrief&lt;/code&gt; (Public Task Sheet)&lt;/strong&gt;: Can only contain user input (&lt;code&gt;prompt&lt;/code&gt; and simulated images/context). It is &lt;strong&gt;strictly forbidden from containing any &lt;code&gt;expected_contains&lt;/code&gt;, &lt;code&gt;expected_not_contains&lt;/code&gt;, or correct answer strings&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;score()&lt;/code&gt; (Black-box Grader)&lt;/strong&gt;: After comparing answers in an isolated environment, it only returns "which Rubric received a &lt;code&gt;1.0&lt;/code&gt; or &lt;code&gt;0.0&lt;/code&gt;" (e.g., &lt;code&gt;r3_e164_jp_phone_normalization: 0.0&lt;/code&gt;), without leaking the expected string to the optimizer.&lt;/li&gt;
&lt;li&gt;  As a result, the &lt;code&gt;Harness²&lt;/code&gt; optimizer must truly reason out general domain rules (e.g., "When converting Japanese phone numbers &lt;code&gt;+81 (0)3-...&lt;/code&gt; to E.164, the domestic trunk prefix &lt;code&gt;(0)&lt;/code&gt; must be removed") by &lt;strong&gt;reading the Agent's execution trajectory (&lt;code&gt;trajectory.md&lt;/code&gt;)&lt;/strong&gt;, rather than hard-coding answers into the Prompt!&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Why Convert Phase 5 OpenTelemetry Spans into &lt;code&gt;trajectory.md&lt;/code&gt;? Opening the Black Box of the 13-Node Graph
&lt;/h2&gt;

&lt;p&gt;Our LINE AI Business Card Assistant is not a simple "single LLM call," but an &lt;strong&gt;ADK 2.0 Directed Acyclic Graph (Graph Workflow) consisting of 13 nodes&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Business Card Ingestion Pipeline: &lt;code&gt;RouterNode&lt;/code&gt; $\rightarrow$ &lt;code&gt;VisionOCRNode&lt;/code&gt; $\rightarrow$ Parallel &lt;code&gt;AddFanOut&lt;/code&gt; (&lt;code&gt;CleanAndDedupNode&lt;/code&gt;, &lt;code&gt;CompanyEnrichNode&lt;/code&gt;, &lt;code&gt;ContextMatchNode&lt;/code&gt;) $\rightarrow$ &lt;code&gt;AddFanIn&lt;/code&gt; (&lt;code&gt;JoinPreviewNode&lt;/code&gt;) $\rightarrow$ &lt;code&gt;BuildPreviewNode&lt;/code&gt; $\rightarrow$ &lt;code&gt;HITLConfirmNode&lt;/code&gt; $\rightarrow$ &lt;code&gt;ApplyEditNode&lt;/code&gt; / &lt;code&gt;SaveCardNode&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  Smart Search and Icebreaker Pipeline: &lt;code&gt;RouterNode&lt;/code&gt; $\rightarrow$ &lt;code&gt;QueryIntentNode&lt;/code&gt; $\rightarrow$ &lt;code&gt;HybridSearchNode&lt;/code&gt; / &lt;code&gt;BroadSearchNode&lt;/code&gt; $\rightarrow$ &lt;code&gt;FormatCarouselNode&lt;/code&gt; / &lt;code&gt;EmailDraftNode&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without Phase 5's OpenTelemetry Spans, when a user searches for &lt;em&gt;"Help me find a Sales Director in Hsinchu Science Park working on AI chips"&lt;/em&gt; but gets &lt;em&gt;"No matching business cards found"&lt;/em&gt; (score &lt;code&gt;0.25&lt;/code&gt;), the optimizer only sees this final failure message and cannot determine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Did the &lt;strong&gt;&lt;code&gt;RouterNode&lt;/code&gt;&lt;/strong&gt; misjudge the search intent as &lt;code&gt;chat&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;  Did the &lt;strong&gt;&lt;code&gt;QueryIntentNode&lt;/code&gt;&lt;/strong&gt; fail to expand the colloquial "Hsinchu Science Park" into the database's address &lt;code&gt;Hsinchu&lt;/code&gt;, or fail to expand "Sales Director" into &lt;code&gt;Sales Director&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;  Was there an error in the SQL query logic of the &lt;strong&gt;&lt;code&gt;HybridSearchNode&lt;/code&gt;&lt;/strong&gt;?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When we collect and render Phase 5's OpenTelemetry &lt;code&gt;workflow.node.*&lt;/code&gt; Spans into a structured &lt;strong&gt;&lt;code&gt;trajectory.md&lt;/code&gt;&lt;/strong&gt;, &lt;code&gt;Harness²&lt;/code&gt; can clearly see the input, output, duration, routing decisions, and loaded skills of every Graph Node during the &lt;code&gt;Propose&lt;/code&gt; and &lt;code&gt;Probe&lt;/code&gt; stages!&lt;/p&gt;




&lt;h1&gt;
  
  
  II. Implementation Part 2: Building the &lt;code&gt;LineCardAdapter&lt;/code&gt; in Three Steps
&lt;/h1&gt;

&lt;p&gt;Referring to the architecture diagram at the beginning of this article, we completed the implementation of &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/issues/17" rel="noopener noreferrer"&gt;Issue #17&lt;/a&gt;&lt;/strong&gt; through three core components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/harness/trajectory.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/harness/trajectory.go&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;: Mounts an in-memory OpenTelemetry &lt;code&gt;SpanRecorder&lt;/code&gt; on the Go side to export the execution details of the 13 Graph Nodes as &lt;code&gt;trajectory.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/cmd/harness-runner/main.go" rel="noopener noreferrer"&gt;&lt;code&gt;cmd/harness-runner/main.go&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;: Provides a deterministic subprocess CLI for the Python &lt;code&gt;Harness²&lt;/code&gt; to call, receiving &lt;code&gt;--harness-dir&lt;/code&gt; and &lt;code&gt;--task-json&lt;/code&gt;, and outputting a JSON containing &lt;code&gt;deliverable&lt;/code&gt;, &lt;code&gt;trajectory_markdown&lt;/code&gt;, and &lt;code&gt;spans&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/harness/line_card_adapter.py" rel="noopener noreferrer"&gt;&lt;code&gt;harness/line_card_adapter.py&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;: Implements the &lt;code&gt;google-research/harness2&lt;/code&gt; &lt;code&gt;BenchmarkAdapter&lt;/code&gt; interface, connecting the Phase 4 Dataset with the Go &lt;code&gt;harness-runner&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. Implementing &lt;code&gt;internal/harness/trajectory.go&lt;/code&gt;: Rendering OpenTelemetry Spans as &lt;code&gt;trajectory.md&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;In Phase 5, our &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/workflow/telemetry.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/workflow/telemetry.go&lt;/code&gt;&lt;/a&gt; already calls &lt;code&gt;startNodeSpan(ctx, "VisionOCRNode")&lt;/code&gt; in every Graph Node. Now, in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/harness/trajectory.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/harness/trajectory.go&lt;/code&gt;&lt;/a&gt;, we use &lt;code&gt;go.opentelemetry.io/otel/sdk/trace/tracetest.NewSpanRecorder()&lt;/code&gt; to intercept all completed node Spans and transform them into a Markdown trajectory report that the &lt;code&gt;Harness²&lt;/code&gt; Harvester LLM is best at reading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// InstallRecorder registers a new in-memory SpanRecorder as the global OTel TracerProvider&lt;/span&gt;
&lt;span class="c"&gt;// so every workflow.node.* span emitted by internal/workflow/telemetry.go is captured.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;InstallRecorder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;TrajectoryRecorder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;sr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;tracetest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewSpanRecorder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;tp&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sdktrace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewTracerProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sdktrace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithSpanProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sr&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;otel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetTracerProvider&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;otel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetTracerProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;TrajectoryRecorder&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;recorder&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tp&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;restore&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;otel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetTracerProvider&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;restore&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the same time, we added three OpenTelemetry Attributes to &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/workflow/telemetry.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/workflow/telemetry.go&lt;/code&gt;&lt;/a&gt;: &lt;code&gt;AttrNodeDecision&lt;/code&gt; (&lt;code&gt;adk.node.decision&lt;/code&gt;), &lt;code&gt;AttrNodeDiagnostic&lt;/code&gt; (&lt;code&gt;adk.node.diagnostic&lt;/code&gt;), and &lt;code&gt;AttrHarnessSkills&lt;/code&gt; (&lt;code&gt;adk.harness.skills&lt;/code&gt;). When &lt;code&gt;TrajectoryRecorder.RenderMarkdown()&lt;/code&gt; executes, it produces a high-resolution &lt;code&gt;trajectory.md&lt;/code&gt; like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# ADK 2.0 Go Graph Workflow Execution Trajectory&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Task ID**&lt;/span&gt; : &lt;span class="sb"&gt;`challenge_colloquial_tw_search`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Harness Root**&lt;/span&gt; : &lt;span class="sb"&gt;`harness/base`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Always-On Bytes (`c1..c4`)**&lt;/span&gt;: &lt;span class="sb"&gt;`1599 / 24000`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Final Score**&lt;/span&gt; : &lt;span class="sb"&gt;`0.25`&lt;/span&gt;

&lt;span class="gu"&gt;## OpenTelemetry Node Spans (`workflow.node.*`)&lt;/span&gt;
&lt;span class="gu"&gt;### 1. `workflow.node.RouterNode`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Node**&lt;/span&gt; : &lt;span class="sb"&gt;`RouterNode`&lt;/span&gt; | &lt;span class="gs"&gt;**Status**&lt;/span&gt; : &lt;span class="sb"&gt;`OK`&lt;/span&gt; | &lt;span class="gs"&gt;**Duration**&lt;/span&gt; : &lt;span class="sb"&gt;`0.02 ms`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Decision**&lt;/span&gt; : &lt;span class="sb"&gt;`intent=search (colloquial_query=Help me find a Sales Director in Hsinchu Science Park working on AI chips, I need to contact Taiwan semiconductor clients)`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Diagnostic**&lt;/span&gt; : &lt;span class="sb"&gt;`routed search query to QueryIntentNode`&lt;/span&gt;

&lt;span class="gu"&gt;### 2. `workflow.node.QueryIntentNode`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Node**&lt;/span&gt; : &lt;span class="sb"&gt;`QueryIntentNode`&lt;/span&gt; | &lt;span class="gs"&gt;**Status**&lt;/span&gt; : &lt;span class="sb"&gt;`OK`&lt;/span&gt; | &lt;span class="gs"&gt;**Duration**&lt;/span&gt; : &lt;span class="sb"&gt;`0.04 ms`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Decision**&lt;/span&gt; : &lt;span class="sb"&gt;`parsed_filters={"location":"Hsinchu Science Park","role":"Sales Director","industry":"AI Chips"}`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Diagnostic**&lt;/span&gt; : &lt;span class="sb"&gt;`UNEXPANDED_COLLOQUIAL_FILTERS: colloquial Taiwan science park alias 'Hsinchu Science Park' and Mandarin title 'Sales Director' were passed literally without bilingual synonym expansion; add a c6 skill 'tw_semicon_search' with BILINGUAL_SYNONYM_MAP`&lt;/span&gt;

&lt;span class="gu"&gt;### 3. `workflow.node.HybridSearchNode`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Node**&lt;/span&gt; : &lt;span class="sb"&gt;`HybridSearchNode`&lt;/span&gt; | &lt;span class="gs"&gt;**Status**&lt;/span&gt; : &lt;span class="sb"&gt;`OK`&lt;/span&gt; | &lt;span class="gs"&gt;**Duration**&lt;/span&gt; : &lt;span class="sb"&gt;`0.03 ms`&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="gs"&gt;**Decision**&lt;/span&gt; : &lt;span class="sb"&gt;`matched_cards=0 (strict literal filter on location='Hsinchu Science Park' failed against DB address='Hsinchu Science Park (竹科)')`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please pay attention to the Span records of the 2nd node &lt;code&gt;workflow.node.QueryIntentNode&lt;/code&gt; and the 3rd node &lt;code&gt;workflow.node.HybridSearchNode&lt;/code&gt;! Through this trajectory, the &lt;code&gt;Propose&lt;/code&gt; stage of &lt;code&gt;Harness²&lt;/code&gt; can see at a glance: &lt;strong&gt;&lt;code&gt;RouterNode&lt;/code&gt; was correct, the error was in &lt;code&gt;QueryIntentNode&lt;/code&gt; treating "Hsinchu Science Park" as a rigid string to query the English address field&lt;/strong&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Expanding the Phase 4 Evaluation Set: &lt;code&gt;eval/datasets/harness2-challenge-dataset.json&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;To fully test &lt;code&gt;Harness²&lt;/code&gt;'s ability to retain existing capabilities (preventing catastrophic forgetting) and conquer difficult edge cases, we established &lt;strong&gt;8 tasks (totaling 32 fine-grained Rubrics)&lt;/strong&gt; in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/eval/datasets/harness2-challenge-dataset.json" rel="noopener noreferrer"&gt;&lt;code&gt;eval/datasets/harness2-challenge-dataset.json&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Task Category&lt;/th&gt;
&lt;th&gt;Task ID&lt;/th&gt;
&lt;th&gt;Scenario Description&lt;/th&gt;
&lt;th&gt;4 Independent Scoring Rubrics (&lt;code&gt;r1..r4&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Baseline (Regression)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;baseline_ingest_ming_wang&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Standard Traditional Chinese card ingestion (Wang Xiaoming / Taiwan Cloud Tech)&lt;/td&gt;
&lt;td&gt;Intent routing, Name/Title extraction, E.164 phone normalization, Industry tag enrichment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Baseline (Regression)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;baseline_dedup_appier_promotion&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Duplicate detection for promotion (Lin Yating Senior $\rightarrow$ VP)&lt;/td&gt;
&lt;td&gt;Dedup detection, Title update, Keep new mobile, Company consistency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Baseline (Regression)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;baseline_search_cloud_summit_ai&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Standard keyword search (Cloud Summit + AI)&lt;/td&gt;
&lt;td&gt;Search routing, Multiple hits, Tag display, Interactive buttons&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Baseline (Regression)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;baseline_email_google_cloud&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;General tech follow-up icebreaker (Chen Jianhong / Google Cloud)&lt;/td&gt;
&lt;td&gt;Email routing, Recipient/Event mention, Agenda link, Professional closing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Challenge (Evolution 1)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;challenge_jp_bilingual_ingest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Bilingual JP/EN card OCR &amp;amp; JP phone normalization&lt;/strong&gt; (Kenta Sato / CTO / &lt;code&gt;+81 (0)3-1234-5678&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;r1&lt;/code&gt; Routing, &lt;code&gt;r2&lt;/code&gt; Bilingual name retention, &lt;code&gt;r3&lt;/code&gt; &lt;strong&gt;JP &lt;code&gt;+81&lt;/code&gt; remove &lt;code&gt;(0)&lt;/code&gt; normalization&lt;/strong&gt;, &lt;code&gt;r4&lt;/code&gt; &lt;strong&gt;JP title bilingual mapping&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Challenge (Evolution 2)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;challenge_colloquial_tw_search&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Colloquial Taiwan tech alias search&lt;/strong&gt; ("Help me find a Sales Director in Hsinchu Science Park")&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;r1&lt;/code&gt; Routing, &lt;code&gt;r2&lt;/code&gt; &lt;strong&gt;Hsinchu Science Park $\leftrightarrow$ Hsinchu bilingual expansion hit&lt;/strong&gt;, &lt;code&gt;r3&lt;/code&gt; &lt;strong&gt;Sales Director $\leftrightarrow$ Sales Director title expansion&lt;/strong&gt;, &lt;code&gt;r4&lt;/code&gt; Tag completeness&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Challenge (Evolution 3)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;challenge_noisy_multi_edit_hitl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Multi-clause colloquial HITL continuous correction&lt;/strong&gt; ("Sorry, let me correct that, the title should be Group Senior VP, also change the end of the mobile to 8899, and add GDG DevFest speaker to notes")&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;r1&lt;/code&gt; HITL Edit routing, &lt;code&gt;r2&lt;/code&gt; &lt;strong&gt;Multi-field atomic update (Title)&lt;/strong&gt;, &lt;code&gt;r3&lt;/code&gt; &lt;strong&gt;Mobile suffix partial replacement &amp;amp; Note append&lt;/strong&gt;, &lt;code&gt;r4&lt;/code&gt; Retain original company/email&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Challenge (Evolution 4)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;challenge_pii_safe_executive_followup&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Financial executive compliance &amp;amp; PII masking icebreaker&lt;/strong&gt; (Zhang Yazhu / Cathay Financial, Digital Risk Officer, requires privacy protection and compliance headers)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;r1&lt;/code&gt; Email routing, &lt;code&gt;r2&lt;/code&gt; Executive title/scenario, &lt;code&gt;r3&lt;/code&gt; &lt;strong&gt;Mobile/Private Email PII Redaction&lt;/strong&gt;, &lt;code&gt;r4&lt;/code&gt; &lt;strong&gt;Financial compliance subject header &lt;code&gt;[Confidential / FinTech Compliance Verified]&lt;/code&gt;&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  3. Implementing &lt;code&gt;harness/line_card_adapter.py&lt;/code&gt;: Connecting to &lt;code&gt;google-research/harness2&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Finally, we implemented the &lt;code&gt;LineCardAdapter&lt;/code&gt; in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/harness/line_card_adapter.py" rel="noopener noreferrer"&gt;&lt;code&gt;harness/line_card_adapter.py&lt;/code&gt;&lt;/a&gt;, connecting all the above components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LineCardAdapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Adapts `line-card-assistant` to the `google-research/harness2` BenchmarkAdapter protocol.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_tasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TaskBrief&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return public task briefs with zero ground-truth rubric leakage.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;briefs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;TaskBrief&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_raw_cases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Strip `rubrics` and `expected_*` so the agent/harvester never sees ground truth.
&lt;/span&gt;            &lt;span class="n"&gt;briefs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nc"&gt;TaskBrief&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;general&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{}),&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;briefs&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;harness&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Harness&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TaskBrief&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;RunArtifact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Mount `harness` in a temporary directory and execute the Go Graph Workflow runner.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;problems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;harness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;diag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;# Harness Validation Failed&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;- &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;problems&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RunArtifact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deliverable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trajectory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;diag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;line_card_harness_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;tmp_dir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;harness_root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;harness&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;materialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;harness_root&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;# Invoke the compiled Go binary with --harness-dir pointing to the materialized c1..c8 tree
&lt;/span&gt;            &lt;span class="n"&gt;proc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;binary&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--harness-dir=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;harness_root&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--task-json=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;task_payload_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RunArtifact&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;deliverable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deliverable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;trajectory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trajectory_markdown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TaskBrief&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;artifact&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RunArtifact&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ScoreResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Evaluate the deliverable against isolated per-rubric criteria.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_cases_by_id&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;rubrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rubrics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
        &lt;span class="n"&gt;per_rubric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;rubric_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rubrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;per_rubric&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;rubric_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_evaluate_rubric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;artifact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deliverable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;per_rubric&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;per_rubric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;per_rubric&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ScoreResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;per_rubric&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;per_rubric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Through this &lt;code&gt;LineCardAdapter&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Whenever &lt;code&gt;Harness²&lt;/code&gt; proposes a new candidate Harness $H’$, &lt;code&gt;adapter.run(harness, task)&lt;/code&gt; first calls &lt;code&gt;harness.validate()&lt;/code&gt; to check the 24KB &lt;code&gt;OPENCODE_SURFACE&lt;/code&gt; limit and Skill Frontmatter rules.&lt;/li&gt;
&lt;li&gt;  Once validated, the &lt;code&gt;c1..c8&lt;/code&gt; files of $H’$ are materialized into a temporary directory and passed to Go's &lt;code&gt;harness-runner&lt;/code&gt; for execution.&lt;/li&gt;
&lt;li&gt;  After the Go 13-node Graph Workflow finishes, it returns the final LINE message (&lt;code&gt;deliverable&lt;/code&gt;) and OpenTelemetry trajectory (&lt;code&gt;trajectory_markdown&lt;/code&gt;), which are then passed to &lt;code&gt;adapter.score()&lt;/code&gt; for leakage-free independent scoring!&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  III. Summary and Teaser for the Next Part
&lt;/h1&gt;

&lt;p&gt;In this post (Part 2), we completed the core engineering for &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/issues/17" rel="noopener noreferrer"&gt;Issue #17&lt;/a&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Clarified why the golden test set cannot be fed directly to the optimizer — through the &lt;strong&gt;&lt;code&gt;TaskBrief&lt;/code&gt; Zero-Leakage Barrier&lt;/strong&gt;, we force &lt;code&gt;Harness²&lt;/code&gt; to reason out general skills from behavioral trajectories rather than memorizing answers.&lt;/li&gt;
&lt;li&gt; Through &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/harness/trajectory.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/harness/trajectory.go&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/cmd/harness-runner/main.go" rel="noopener noreferrer"&gt;&lt;code&gt;cmd/harness-runner/main.go&lt;/code&gt;&lt;/a&gt;, we transformed &lt;strong&gt;Phase 5's OpenTelemetry &lt;code&gt;workflow.node.*&lt;/code&gt; Spans&lt;/strong&gt; into high-resolution &lt;code&gt;trajectory.md&lt;/code&gt;, opening the black box of the 13-node Graph Workflow.&lt;/li&gt;
&lt;li&gt; Established &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/eval/datasets/harness2-challenge-dataset.json" rel="noopener noreferrer"&gt;&lt;code&gt;eval/datasets/harness2-challenge-dataset.json&lt;/code&gt;&lt;/a&gt; (8 major tasks, 32 fine-grained Rubrics) and &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/harness/line_card_adapter.py" rel="noopener noreferrer"&gt;&lt;code&gt;harness/line_card_adapter.py&lt;/code&gt;&lt;/a&gt;, perfectly implementing the &lt;code&gt;Harness²&lt;/code&gt; &lt;code&gt;BenchmarkAdapter&lt;/code&gt; protocol.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything is ready! In the final post &lt;strong&gt;&lt;a href="https://dev.to/2026/10/27/adk-graph-workflow-go-9/"&gt;[Agent Platform in Practice (9)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 3): Dual-Layer Recursive Testing Guide, 62.5% to 100% Performance Results, and the Future of AgentOps&lt;/a&gt;&lt;/strong&gt; (corresponding to &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/issues/18" rel="noopener noreferrer"&gt;Issue #18&lt;/a&gt;&lt;/strong&gt;), we will actually launch the dual-layer recursive optimization experiment, reveal the complete quantitative data evolving from &lt;code&gt;62.5%&lt;/code&gt; to &lt;code&gt;100.0%&lt;/code&gt;, and discuss the profound impact of &lt;code&gt;Harness²&lt;/code&gt; on future enterprise-grade Agent development!&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>go</category>
      <category>llm</category>
    </item>
    <item>
      <title>[Agent Platform in Action (9)] When ADK Go Graph Workflow Meets Google Research Harness (Part 2): Two-layer Recursive Testing Gu</title>
      <dc:creator>Evan Lin</dc:creator>
      <pubDate>Sun, 01 Nov 2026 14:36:51 +0000</pubDate>
      <link>https://dev.to/gde/agent-platform-in-action-9-when-adk-go-graph-workflow-meets-google-research-harness-part-2-1olm</link>
      <guid>https://dev.to/gde/agent-platform-in-action-9-when-adk-go-graph-workflow-meets-google-research-harness-part-2-1olm</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frmuecj0ixueufuhe5l1s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frmuecj0ixueufuhe5l1s.png" alt="Harness² Part 3: Recursive Optimization Benchmark &amp;amp; Future AgentOps Outlook" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Introduction: From "Manual Prompt Tuning" to "Two-Layer Recursive Autonomous Evolution"
&lt;/h1&gt;

&lt;p&gt;Welcome to the final part of the &lt;strong&gt;Agent Platform in Practice&lt;/strong&gt; Harness² trilogy! In the previous two articles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/2026/10/25/adk-graph-workflow-go-7/"&gt;[Agent Platform in Practice (7)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 1): Understanding the Two-Layer Recursive Agent Harnessing Framework and c1..c8 Decoupling Practice&lt;/a&gt;&lt;/strong&gt; (&lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/issues/16" rel="noopener noreferrer"&gt;Issue #16&lt;/a&gt;&lt;/strong&gt;): We introduced Google Research's &lt;strong&gt;&lt;a href="https://github.com/google-research/harness2" rel="noopener noreferrer"&gt;&lt;code&gt;google-research/harness2&lt;/code&gt; (arXiv:2610.14224)&lt;/a&gt;&lt;/strong&gt; and decoupled the 13-node Graph Workflow of &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant" rel="noopener noreferrer"&gt;LINE AI Card Master (&lt;code&gt;kkdai/line-card-assistant&lt;/code&gt;)&lt;/a&gt;&lt;/strong&gt; into an &lt;strong&gt;editable &lt;code&gt;c1..c8&lt;/code&gt; file tree&lt;/strong&gt; supporting dynamic mounting via &lt;code&gt;LINE_CARD_HARNESS_DIR&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/2026/10/26/adk-graph-workflow-go-8/"&gt;[Agent Platform in Practice (8)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 2): Turning Phase 4 Eval Datasets and Phase 5 OpenTelemetry Spans into LineCardAdapter&lt;/a&gt;&lt;/strong&gt; (&lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/issues/17" rel="noopener noreferrer"&gt;Issue #17&lt;/a&gt;&lt;/strong&gt;): We encapsulated Phase 4's &lt;code&gt;eval/datasets/&lt;/code&gt; and Phase 5's OpenTelemetry &lt;code&gt;workflow.node.*&lt;/code&gt; Spans into a &lt;code&gt;LineCardAdapter&lt;/code&gt;, establishing a &lt;strong&gt;Zero-Leakage Barrier&lt;/strong&gt; and &lt;strong&gt;High-Resolution Behavior Trajectories (&lt;code&gt;trajectory.md&lt;/code&gt;)&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this post (the final part, corresponding to &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/issues/18" rel="noopener noreferrer"&gt;Issue #18&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/pull/19" rel="noopener noreferrer"&gt;PR #19&lt;/a&gt;&lt;/strong&gt;), we will answer the last three key questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;How to test after modifications?&lt;/strong&gt; (A complete testing guide from Go unit tests, CLI single-point diagnosis, Python two-layer recursive experiments to production environment mounting)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What is the overall performance after modifications?&lt;/strong&gt; (Publicly disclosing quantitative data and evolutionary details from &lt;code&gt;Base 62.5%&lt;/code&gt; $\rightarrow$ &lt;code&gt;k=1 84.4%&lt;/code&gt; $\rightarrow$ &lt;code&gt;k=2 Parallel 96.9%&lt;/code&gt; $\rightarrow$ &lt;code&gt;k=2 Sequential + Memory 100.0%&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What are the future outlooks and application scenarios for &lt;code&gt;Harness²&lt;/code&gt; and Agents?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h1&gt;
  
  
  1. How to test after modifications? (Step-by-Step Testing and Verification Guide)
&lt;/h1&gt;

&lt;p&gt;After introducing &lt;code&gt;Harness²&lt;/code&gt;, the entire project features four levels of testing and verification: &lt;strong&gt;Go native unit testing&lt;/strong&gt;, &lt;strong&gt;single-task OpenTelemetry trajectory diagnosis&lt;/strong&gt;, &lt;strong&gt;Python two-layer recursive evolution experiments&lt;/strong&gt;, and &lt;strong&gt;seamless production environment switching&lt;/strong&gt;:&lt;/p&gt;

&lt;h2&gt;
  
  
  Level 1: Execute Go &lt;code&gt;c1..c8&lt;/code&gt; Surface Constraints and Project-wide Unit Tests
&lt;/h2&gt;

&lt;p&gt;We have written comprehensive unit tests in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/harness/harness_test.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/harness/harness_test.go&lt;/code&gt;&lt;/a&gt; to verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All &lt;code&gt;c1..c8&lt;/code&gt; file trees under &lt;code&gt;harness/base/&lt;/code&gt; and &lt;code&gt;harness/evolved/&lt;/code&gt; comply with the &lt;code&gt;OPENCODE_SURFACE&lt;/code&gt; 24KB resident byte limit (&lt;code&gt;AlwaysOnMaxBytes = 24000&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Validate()&lt;/code&gt; accurately intercepts and reports errors when &lt;code&gt;c1..c4&lt;/code&gt; exceeds 24,000 bytes.&lt;/li&gt;
&lt;li&gt;When a new &lt;code&gt;c6&lt;/code&gt; skill (&lt;code&gt;skills/&amp;lt;name&amp;gt;/SKILL.md&lt;/code&gt;) is added but no trigger index (Trigger Line) is added to &lt;code&gt;systemprompt.md&lt;/code&gt; or &lt;code&gt;guardrails.md&lt;/code&gt;, &lt;code&gt;Validate()&lt;/code&gt; immediately intercepts the orphan skill.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TrajectoryRecorder&lt;/code&gt; correctly intercepts OpenTelemetry &lt;code&gt;workflow.node.*&lt;/code&gt; Spans and renders a Markdown report containing node decisions and diagnostics.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Execution command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Execute Harness² c1..c8 loader and OpenTelemetry trajectory tests
$ go test -v ./internal/harness/...
=== RUN TestLoadBaseHarnessAndValidate
--- PASS: TestLoadBaseHarnessAndValidate (0.00s)
=== RUN TestValidateRejectsOversizedAlwaysOnAndUnreferencedSkill
--- PASS: TestValidateRejectsOversizedAlwaysOnAndUnreferencedSkill (0.00s)
=== RUN TestEvolvedHarnessesValidateAndMatchSkills
--- PASS: TestEvolvedHarnessesValidateAndMatchSkills (0.00s)
=== RUN TestTrajectoryRecorderRendersWorkflowNodeSpans
--- PASS: TestTrajectoryRecorderRendersWorkflowNodeSpans (0.00s)
PASS
ok github.com/kkdai/line-card-assistant/internal/harness

# Execute project-wide regression tests (ensuring 100% compatibility with all existing Phase 1~5 features)
$ go test ./...
ok github.com/kkdai/line-card-assistant/app
ok github.com/kkdai/line-card-assistant/cmd/harness-runner
ok github.com/kkdai/line-card-assistant/cmd/line-gateway
ok github.com/kkdai/line-card-assistant/eval
ok github.com/kkdai/line-card-assistant/internal/harness
ok github.com/kkdai/line-card-assistant/internal/storage
ok github.com/kkdai/line-card-assistant/internal/webhook
ok github.com/kkdai/line-card-assistant/internal/workflow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Level 2: Use &lt;code&gt;cmd/harness-runner&lt;/code&gt; to Validate Any &lt;code&gt;c1..c8&lt;/code&gt; File Tree
&lt;/h2&gt;

&lt;p&gt;Before mounting any custom or evolved Harness online, you can use &lt;code&gt;--validate-only&lt;/code&gt; to quickly check its &lt;code&gt;c1..c4&lt;/code&gt; resident byte count and YAML Frontmatter compliance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;go run ./cmd/harness-runner &lt;span class="nt"&gt;--validate-only&lt;/span&gt; &lt;span class="nt"&gt;--harness-dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;harness/base
OK &lt;span class="o"&gt;(&lt;/span&gt;1599 always-on bytes&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;$ &lt;/span&gt;go run ./cmd/harness-runner &lt;span class="nt"&gt;--validate-only&lt;/span&gt; &lt;span class="nt"&gt;--harness-dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;harness/evolved/k2_sequential_memory
OK &lt;span class="o"&gt;(&lt;/span&gt;4661 always-on bytes&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Level 3: Execute the Four-Stage &lt;code&gt;Harness²&lt;/code&gt; Two-Layer Recursive Evolution Benchmark Experiment
&lt;/h2&gt;

&lt;p&gt;We implemented a complete two-layer recursive experiment script in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/harness/run_harness2_experiment.py" rel="noopener noreferrer"&gt;&lt;code&gt;harness/run_harness2_experiment.py&lt;/code&gt;&lt;/a&gt;. It automatically compiles the Go &lt;code&gt;harness-runner&lt;/code&gt; and sequentially evaluates four configurations against the 8 major tasks (32 independent Rubrics) in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/eval/datasets/harness2-challenge-dataset.json" rel="noopener noreferrer"&gt;&lt;code&gt;eval/datasets/harness2-challenge-dataset.json&lt;/code&gt;&lt;/a&gt;, finally outputting a Markdown comparison table and &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/harness/experiment_report.json" rel="noopener noreferrer"&gt;&lt;code&gt;harness/experiment_report.json&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 harness/run_harness2_experiment.py
### Overall Configuration Comparison

| Stage | Configuration | Task Score (Max 8.0) | Rubric Pass Rate | Always-On Bytes (Cap 24,000) | Active `c6` Skills | `LongTermMEMORY.md` |
| :--- | :--- | :---: | :---: | :---: | :--- | :---: |
| `base_k0` | **Base Harness ($k=0$)** | `5.00 / 8.00` | `20/32 (62.5%)` | `1599 B` | `(none)` | `No` |
| `k1_single_step` | **Single-Step Propose-Probe-Compose ($k=1$)** | `6.75 / 8.00` | `27/32 (84.4%)` | `2505 B` | `jp_card_ocr, tw_semicon_search` | `No` |
| `k2_parallel` | **Multi-Step Parallel Recursion ($k=2$, No Memory)** | `7.75 / 8.00` | `31/32 (96.9%)` | `2998 B` | `executive_email_drafter, hitl_multi_edit, jp_card_ocr, tw_semicon_search` | `No` |
| `k2_sequential_memory` | **Multi-Step Sequential Recursion + `LongTermMEMORY.md` ($k=2$)** | `8.00 / 8.00` | `32/32 (100.0%)` | `4661 B` | `executive_email_drafter, hitl_multi_edit, jp_card_ocr, tw_semicon_search` | `Yes` |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Level 4: Directly Mount the Best Evolved Harness in Production or Local Servers
&lt;/h2&gt;

&lt;p&gt;Once &lt;code&gt;Harness²&lt;/code&gt; evolves the optimal &lt;code&gt;harness/evolved/k2_sequential_memory&lt;/code&gt;, &lt;strong&gt;no modification to any line of Go code is required&lt;/strong&gt;. Simply specify the directory via the environment variable &lt;code&gt;LINE_CARD_HARNESS_DIR&lt;/code&gt;, and both the dual-channel main program (&lt;code&gt;main.go&lt;/code&gt;) and the evaluator (&lt;code&gt;eval/&lt;/code&gt;) will immediately load the evolved harness tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ LINE_CARD_HARNESS_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;harness/evolved/k2_sequential_memory go run &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  2. Performance Summary: Evolutionary Analysis from &lt;code&gt;62.5%&lt;/code&gt; to &lt;code&gt;100.0%&lt;/code&gt;
&lt;/h1&gt;

&lt;p&gt;Let's take a deep dive into the four-stage quantitative experimental data recorded in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/harness/experiment_report.json" rel="noopener noreferrer"&gt;&lt;code&gt;harness/experiment_report.json&lt;/code&gt;&lt;/a&gt; to see how &lt;code&gt;Harness²&lt;/code&gt; diagnosed and patched the Go Agent's flaws step-by-step!&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Four-Stage Overall and Single-Task Score Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Evaluation Metric / Test Task&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Stage 0: Base Harness ($k=0$)&lt;/strong&gt; (&lt;code&gt;harness/base&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Stage 1: Single-Step Inner Recursion ($k=1$)&lt;/strong&gt; (&lt;code&gt;k1_single_step&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Stage 2: Multi-Step Parallel Recursion ($k=2$, No Memory)&lt;/strong&gt; (&lt;code&gt;k2_parallel&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;
&lt;strong&gt;Stage 3: Two-Layer Sequential Recursion + Long-Term Memory ($k=2$)&lt;/strong&gt; (&lt;code&gt;k2_sequential_memory&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;baseline_ingest_ming_wang&lt;/code&gt;&lt;/strong&gt; (Standard Chinese Entry)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.00 (4/4)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.00 (4/4)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.00 (4/4)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;1.00 (4/4)&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;baseline_dedup_appier_promotion&lt;/code&gt;&lt;/strong&gt; (Promotion Deduplication Update)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.00 (4/4)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.00 (4/4)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.00 (4/4)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;1.00 (4/4)&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;baseline_search_cloud_summit_ai&lt;/code&gt;&lt;/strong&gt; (Standard Keyword Search)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.00 (4/4)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.00 (4/4)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.00 (4/4)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;1.00 (4/4)&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;baseline_email_google_cloud&lt;/code&gt;&lt;/strong&gt; (General Tech Icebreaker Email)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.00 (4/4)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.00 (4/4)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.00 (4/4)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;1.00 (4/4)&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;challenge_jp_bilingual_ingest&lt;/code&gt;&lt;/strong&gt; (JP-EN Bilingual Card OCR)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0.25 (1/4)&lt;/code&gt; ❌&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;1.00 (4/4)&lt;/code&gt; ✅&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;1.00 (4/4)&lt;/code&gt; ✅&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;1.00 (4/4)&lt;/code&gt;&lt;/strong&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;challenge_colloquial_tw_search&lt;/code&gt;&lt;/strong&gt; (Hsinchu Science Park Colloquial Search)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0.25 (1/4)&lt;/code&gt; ❌&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;1.00 (4/4)&lt;/code&gt; ✅&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;1.00 (4/4)&lt;/code&gt; ✅&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;1.00 (4/4)&lt;/code&gt;&lt;/strong&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;challenge_noisy_multi_edit_hitl&lt;/code&gt;&lt;/strong&gt; (Multi-clause Colloquial HITL Edit)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0.25 (1/4)&lt;/code&gt; ❌&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0.25 (1/4)&lt;/code&gt; ❌&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;1.00 (4/4)&lt;/code&gt; ✅&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;1.00 (4/4)&lt;/code&gt;&lt;/strong&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;challenge_pii_safe_executive_followup&lt;/code&gt;&lt;/strong&gt; (Financial Exec Compliance Icebreaker)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0.25 (1/4)&lt;/code&gt; ❌&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0.50 (2/4)&lt;/code&gt; ⚠️&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;0.75 (3/4)&lt;/code&gt; ⚠️&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;&lt;code&gt;1.00 (4/4)&lt;/code&gt;&lt;/strong&gt; ✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total Task Score (Max 8.00)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;5.00 / 8.00&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;6.75 / 8.00&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;7.75 / 8.00&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;8.00 / 8.00&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fine-grained Rubric Pass Rate (Max 32)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;20 / 32 (62.5%)&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;27 / 32 (84.4%)&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;31 / 32 (96.9%)&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;32 / 32 (100.0%)&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Resident Bytes (&lt;code&gt;c1..c4&lt;/code&gt; / 24,000 B)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1,599 B (6.7%)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2,505 B (10.4%)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2,998 B (12.5%)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4,661 B (19.4%)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;On-demand Skills (&lt;code&gt;c6&lt;/code&gt; Skills)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;4 + LongTermMEMORY&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  2. Key Findings Analysis: What evolved in each stage?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🔹 Stage 0 $\rightarrow$ Stage 1 (&lt;code&gt;62.5%&lt;/code&gt; $\rightarrow$ &lt;code&gt;84.4%&lt;/code&gt;): Diagnosing the first two on-demand skills (&lt;code&gt;c6&lt;/code&gt;) from OpenTelemetry trajectories
&lt;/h3&gt;

&lt;p&gt;In the &lt;code&gt;Base Harness ($k=0$)&lt;/code&gt;, while all 4 Baseline tasks scored full points (&lt;code&gt;4.00/4.00&lt;/code&gt;), all 4 Challenge tasks scored only &lt;code&gt;0.25&lt;/code&gt; because they only passed the &lt;code&gt;RouterNode&lt;/code&gt; intent routing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the &lt;code&gt;trajectory.md&lt;/code&gt; of &lt;code&gt;challenge_jp_bilingual_ingest&lt;/code&gt;, the Spans for &lt;code&gt;VisionOCRNode&lt;/code&gt; and &lt;code&gt;CleanAndDedupNode&lt;/code&gt; diagnosed that the Japanese phone number &lt;code&gt;+81 (0)3-1234-5678&lt;/code&gt; failed to strip the domestic trunk prefix &lt;code&gt;(0)&lt;/code&gt;, and the Japanese title &lt;code&gt;取締役 最高技術責任者 (CTO)&lt;/code&gt; failed to retain the bilingual title.&lt;/li&gt;
&lt;li&gt;In the &lt;code&gt;trajectory.md&lt;/code&gt; of &lt;code&gt;challenge_colloquial_tw_search&lt;/code&gt;, the &lt;code&gt;QueryIntentNode&lt;/code&gt; Span diagnosed that colloquial Taiwanese terms like "Hsinchu Science Park" (竹科) and "Sales Director" (業務總監) were not expanded into bilingual synonyms.&lt;/li&gt;
&lt;li&gt;After the first round of inner recursion (&lt;code&gt;Propose -&amp;gt; Probe -&amp;gt; Compose&lt;/code&gt;), &lt;code&gt;Harness²&lt;/code&gt; automatically synthesized &lt;code&gt;skills/jp_card_ocr/SKILL.md&lt;/code&gt; and &lt;code&gt;skills/tw_semicon_search/SKILL.md&lt;/code&gt; in &lt;code&gt;k1_single_step&lt;/code&gt;, and updated the PII masking rules in &lt;code&gt;guardrails.md&lt;/code&gt;, jumping the accuracy from &lt;strong&gt;&lt;code&gt;62.5%&lt;/code&gt; to &lt;code&gt;84.4%&lt;/code&gt;&lt;/strong&gt;!&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🔹 Stage 1 $\rightarrow$ Stage 2 (&lt;code&gt;84.4%&lt;/code&gt; $\rightarrow$ &lt;code&gt;96.9%&lt;/code&gt;): Second round of recursion conquers multi-clause HITL modification and executive email structure
&lt;/h3&gt;

&lt;p&gt;In the second round of inner recursion, &lt;code&gt;Harness²&lt;/code&gt; continued to probe the remaining two failing tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For &lt;code&gt;challenge_noisy_multi_edit_hitl&lt;/code&gt; (where &lt;code&gt;ApplyEditNode&lt;/code&gt; only modified the title in the first clause, missing the mobile suffix replacement and note addition), it evolved &lt;code&gt;skills/hitl_multi_edit/SKILL.md&lt;/code&gt; (containing &lt;code&gt;ATOMIC_MULTI_CLAUSE_EDIT&lt;/code&gt; rules).&lt;/li&gt;
&lt;li&gt;For &lt;code&gt;challenge_pii_safe_executive_followup&lt;/code&gt; (where &lt;code&gt;EmailDraftNode&lt;/code&gt; lacked structured executive follow-up paragraphs), it evolved &lt;code&gt;skills/executive_email_drafter/SKILL.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;At this point, under &lt;strong&gt;Parallel Recursion (no cross-round long-term memory)&lt;/strong&gt;, the score reached &lt;strong&gt;&lt;code&gt;31/32 (96.9%)&lt;/code&gt;&lt;/strong&gt;!&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🔹 Stage 2 $\rightarrow$ Stage 3 (&lt;code&gt;96.9%&lt;/code&gt; $\rightarrow$ &lt;code&gt;100.0%&lt;/code&gt;): Why is &lt;code&gt;Sequential + LongTermMEMORY.md&lt;/code&gt; needed to complete the final 3.1%?
&lt;/h3&gt;

&lt;p&gt;Why did &lt;code&gt;k2_parallel&lt;/code&gt; get &lt;code&gt;96.9%&lt;/code&gt; (31/32), yet fail on the 4th Rubric (&lt;code&gt;r4_fintech_compliance_subject_tag&lt;/code&gt;) of &lt;code&gt;challenge_pii_safe_executive_followup&lt;/code&gt;?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Because in cross-enterprise outgoing emails for financial institutions (e.g., Digital Risk Officer at Cathay Financial Holdings), beyond single-skill email formatting, cross-domain &lt;strong&gt;industry compliance memory (FinTech Outbound Compliance Header &lt;code&gt;[Confidential / FinTech Compliance Verified]&lt;/code&gt;)&lt;/strong&gt; is required.&lt;/li&gt;
&lt;li&gt;In &lt;strong&gt;Sequential Recursion + &lt;code&gt;Experience Reflector&lt;/code&gt;&lt;/strong&gt; mode, the outer recursion reviewed the residual failure trajectories from the first to the second round. The &lt;code&gt;ReflectorPrompt&lt;/code&gt; then wrote the &lt;code&gt;FINTECH_COMPLIANCE_SUBJECT_TAG&lt;/code&gt; and cross-channel PII protection principles into the resident component &lt;strong&gt;&lt;code&gt;c3: LongTermMEMORY.md&lt;/code&gt;&lt;/strong&gt;!&lt;/li&gt;
&lt;li&gt;Note the &lt;strong&gt;Resident Bytes (Always-On Bytes)&lt;/strong&gt;: even with 4 rich domain skills and long-term memory evolved, the resident size of &lt;code&gt;c1..c4&lt;/code&gt; is only &lt;strong&gt;&lt;code&gt;4,661 bytes&lt;/code&gt;&lt;/strong&gt; (only &lt;strong&gt;&lt;code&gt;19.4%&lt;/code&gt;&lt;/strong&gt; of the &lt;code&gt;24,000 bytes&lt;/code&gt; limit), perfectly avoiding Context Bloat!&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  3. Future Outlook and Application Scenarios for &lt;code&gt;Harness²&lt;/code&gt; and Agents
&lt;/h1&gt;

&lt;p&gt;This practical implementation of Google Research &lt;code&gt;Harness²&lt;/code&gt; into &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant" rel="noopener noreferrer"&gt;LINE AI Card Master&lt;/a&gt;&lt;/strong&gt; has shown us a brand-new &lt;strong&gt;AgentOps development paradigm&lt;/strong&gt;. Looking ahead, we believe &lt;code&gt;Harness²&lt;/code&gt; has four high-potential application scenarios in the enterprise-grade AI Agent field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dot"&gt;&lt;code&gt;&lt;span class="nv"&gt;flowchart&lt;/span&gt; &lt;span class="nv"&gt;LR&lt;/span&gt;
    &lt;span class="k"&gt;subgraph&lt;/span&gt; &lt;span class="nv"&gt;Prod&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"1. Production Observability (Cloud Run + Agent Runtime)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;Traffic&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Real User Traffic\n(LINE Webhook / A2A)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;Trace&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Cloud Trace OTel Spans\n+ Negative Feedback (HITL Retry)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;Traffic&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Trace&lt;/span&gt;
    &lt;span class="nv"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;subgraph&lt;/span&gt; &lt;span class="nv"&gt;Nightly&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"2. Nightly Autonomous Evolution Flywheel (Nightly Harness² CI/CD)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;Extract&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Auto Mask PII &amp;amp; Convert to\nChallenge Dataset Cases"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;Evolve&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Harness² Two-Layer Recursion\n(Propose-Probe-Compose + Reflector)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;PR&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Auto Initiate GitHub PR\nUpdate harness/evolved/ c1..c8"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;Extract&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Evolve&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;PR&lt;/span&gt;
    &lt;span class="nv"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;subgraph&lt;/span&gt; &lt;span class="nv"&gt;Scenarios&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"3. Future Four High-Value Application Scenarios"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;S1&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Scenario A: Fully Automated Nightly Self-Healing AgentOps"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;S2&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Scenario B: Cross-Model Harnessing &amp;amp; Tuning"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;S3&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Scenario C: Multi-Tenant Enterprise Compliance &amp;amp; Skills"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;S4&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Scenario D: Dual Evolution with ADK Graph Topology (Issue #15)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;end&lt;/span&gt;

    &lt;span class="nv"&gt;Trace&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Extract&lt;/span&gt;
    &lt;span class="nv"&gt;PR&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Scenarios&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  1. Scenario A: Fully Automated "Nightly Self-Healing AgentOps"
&lt;/h2&gt;

&lt;p&gt;In the past, when a production LINE Bot encountered frequent "✏️ Edit Data" triggers via &lt;code&gt;HITLConfirmNode&lt;/code&gt; or received negative feedback, engineers had to manually analyze Cloud Trace logs the next day. Combining the Cloud Trace established in Phase 5 with the &lt;code&gt;LineCardAdapter&lt;/code&gt; from this series, future CI/CD can achieve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automatically filtering anonymous Span trajectories from Cloud Trace every night where "users modified data more than twice consecutively" or "search results were 0," and adding them to the Challenge Dataset.&lt;/li&gt;
&lt;li&gt;Triggering a GitHub Action to run a round of &lt;code&gt;Harness²&lt;/code&gt; two-layer recursion to automatically synthesize new &lt;code&gt;c6&lt;/code&gt; skills or update &lt;code&gt;LongTermMEMORY.md&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Once regression tests (Baseline + Challenge) confirm score improvements and zero regression, &lt;strong&gt;the Bot automatically initiates a Pull Request&lt;/strong&gt; modifying only the &lt;code&gt;harness/&lt;/code&gt; directory. Engineers only need to review the skill Markdown in the morning to deploy with one click!&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Scenario B: Cross-Model Upgrades and "Harness Compensation" for Lightweight Models
&lt;/h2&gt;

&lt;p&gt;Enterprises often encounter behavioral drift when switching underlying models from high-cost models to ultra-fast lightweight models (e.g., switching from Gemini 1.5 Pro to Gemini 1.5 Flash or Flash-Lite to reduce latency and cost by 80%) or upgrading to next-gen models. Through &lt;code&gt;Harness²&lt;/code&gt;, we can automatically run a round of &lt;code&gt;Propose-Probe-Compose&lt;/code&gt; for the target model while keeping the Go code and Benchmark fixed. &lt;code&gt;Harness²&lt;/code&gt; will automatically compensate for the lightweight model's gaps with required reasoning auxiliary skills (&lt;code&gt;c6&lt;/code&gt; Skills) and guardrails (&lt;code&gt;c2&lt;/code&gt; Guardrails), using "smarter gear" to bridge the gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Scenario C: Multi-Tenant Enterprise-Specific Compliance and Domain Memory
&lt;/h2&gt;

&lt;p&gt;As seen in &lt;code&gt;challenge_pii_safe_executive_followup&lt;/code&gt;, industries like FinTech, MedTech, and the semiconductor supply chain each have distinct terminology dictionaries and compliance guardrails. Because &lt;code&gt;Harness²&lt;/code&gt; externalizes the gear as a plain-text file tree, enterprises only need to mount different &lt;code&gt;LINE_CARD_HARNESS_DIR&lt;/code&gt; (e.g., &lt;code&gt;harness/tenants/fintech/&lt;/code&gt; vs &lt;code&gt;harness/tenants/semiconductor/&lt;/code&gt;) for different departments or tenants. This allows the same Go Container Binary to serve multiple highly regulated vertical industries simultaneously!&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Scenario D: Dual Evolution of "Topology + Harness" with ADK v2.6+ Advanced Graph Workflow Primitives
&lt;/h2&gt;

&lt;p&gt;In &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/issues/15" rel="noopener noreferrer"&gt;Issue #15&lt;/a&gt;&lt;/strong&gt;, we planned ADK v2.6+ features like &lt;code&gt;Parallel&lt;/code&gt; dynamic dispatch, &lt;code&gt;MultiRoute&lt;/code&gt; multi-path concurrency, and &lt;code&gt;VerifyOCRQualityNode&lt;/code&gt; reflection loops. In the future, &lt;code&gt;Harness²&lt;/code&gt;'s &lt;code&gt;c5&lt;/code&gt; (&lt;code&gt;sub_agents/&lt;/code&gt;) and &lt;code&gt;c8&lt;/code&gt; (&lt;code&gt;plugin/&lt;/code&gt;) could even further control Graph Workflow routing thresholds and sub-agent concurrency strategies—achieving an ultimate architecture where &lt;strong&gt;"Go is responsible for the type-safe topological skeleton, and Harness² is responsible for dynamically evolving the neural network and skill muscles."&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  4. Recap of the "Agent Platform in Practice" Series
&lt;/h1&gt;

&lt;p&gt;From the first to the ninth post, we have completed a very solid production-grade AI Agent engineering journey:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://dev.to/2026/10/16/adk-graph-workflow-go/"&gt;[Agent Platform in Practice (1)] Building LINE AI Card Master with Go and Google ADK 2.0 Graph Workflow&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://dev.to/2026/10/17/adk-graph-workflow-go-2/"&gt;[Agent Platform in Practice (2)] From Monolithic Cloud Run to Google Cloud Agent Runtime (Phase 1)&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://dev.to/2026/10/20/adk-graph-workflow-go-3/"&gt;[Agent Platform in Practice (3)] Implementing Cross-Instance Recognition and Pause/Resume with ADK 2.0 GCS Artifact Service and Agent Platform Sessions (Phase 2 Part 1)&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://dev.to/2026/10/21/adk-graph-workflow-go-4/"&gt;[Agent Platform in Practice (4)] Completing the Architecture with Cloud Run LINE Webhook Gateway, Agent Runtime, and Cloud SQL PostgreSQL (Phase 2 Part 2 &amp;amp; Phase 3)&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://dev.to/2026/10/22/adk-graph-workflow-go-5/"&gt;[Agent Platform in Practice (5)] Establishing Automated Evaluation Benchmarks with agents-cli eval and ADK 2.0 Go (Phase 4)&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://dev.to/2026/10/24/adk-graph-workflow-go-6/"&gt;[Agent Platform in Practice (6)] Building a Full-Stack Observable LINE AI Card Master with agents-cli deploy, Terraform IaC, GitHub Actions CI/CD, and Cloud Trace (Phase 5)&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://dev.to/2026/10/25/adk-graph-workflow-go-7/"&gt;[Agent Platform in Practice (7)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 1): Understanding the Two-Layer Recursive Agent Harnessing Framework and c1..c8 Decoupling Practice&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://dev.to/2026/10/26/adk-graph-workflow-go-8/"&gt;[Agent Platform in Practice (8)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 2): Turning Phase 4 Eval Datasets and Phase 5 OpenTelemetry Spans into LineCardAdapter&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href="https://dev.to/2026/10/27/adk-graph-workflow-go-9/"&gt;[Agent Platform in Practice (9)] When ADK Go Graph Workflow Meets Google Research Harness² (Part 3): Two-Layer Recursive Testing Guide, 62.5% to 100% Performance Benchmark, and Future AgentOps Outlook (This post)&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;All code, Terraform IaC, evaluation datasets, and evolved &lt;code&gt;c1..c8&lt;/code&gt; Harness file trees are fully open-sourced on GitHub: &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/line-card-assistant&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;. If you are also building enterprise-grade AI Agents with Go and ADK, feel free to Star, Fork, and experience the power of &lt;code&gt;Harness²&lt;/code&gt; two-layer recursion!&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>go</category>
      <category>testing</category>
    </item>
    <item>
      <title>[Agent Platform in Action (6)] Towards Production-Grade AI Agent Ops: Building Full-Stack Observable LINE Agents with agents-cli</title>
      <dc:creator>Evan Lin</dc:creator>
      <pubDate>Sun, 25 Oct 2026 10:05:18 +0000</pubDate>
      <link>https://dev.to/gde/agent-platform-in-action-6-towards-production-grade-ai-agent-ops-building-full-stack-38aa</link>
      <guid>https://dev.to/gde/agent-platform-in-action-6-towards-production-grade-ai-agent-ops-building-full-stack-38aa</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz9kbdrf0t5un6gb6qobc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fz9kbdrf0t5un6gb6qobc.png" alt="Phase 5: Production Deployment, Terraform IaC, GitHub Actions CI/CD &amp;amp; Cloud Trace Observability" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Recap: The Five-Stage Evolution from Local Prototype to Cloud Production-Grade AI Agent
&lt;/h1&gt;

&lt;p&gt;Welcome to the sixth (and final) article in the "&lt;strong&gt;Agent Platform in Action&lt;/strong&gt;" series! Looking back at the past five articles, we have taken &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant" rel="noopener noreferrer"&gt;LINE AI Card Assistant (&lt;code&gt;kkdai/line-card-assistant&lt;/code&gt;)&lt;/a&gt;&lt;/strong&gt; through the four major stages of evolution from a local prototype to an enterprise-grade AI Agent architecture:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;&lt;a href="https://dev.to/2026/10/16/adk-graph-workflow-go/"&gt;[Agent Platform in Action (1)] Building LINE AI Card Assistant with Go and Google ADK 2.0 Graph Workflow&lt;/a&gt;&lt;/strong&gt;: Implemented a business card workflow graph using &lt;code&gt;google.golang.org/adk/v2/workflow&lt;/code&gt; covering Vision OCR, &lt;code&gt;AddFanOut&lt;/code&gt; / &lt;code&gt;AddFanIn&lt;/code&gt; parallel background research, and &lt;code&gt;NewRequestInputEvent&lt;/code&gt; HITL interaction loops.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;&lt;a href="https://dev.to/2026/10/17/adk-graph-workflow-go-2/"&gt;[Agent Platform in Action (2)] Moving from Monolithic Cloud Run to Google Cloud Agent Runtime: Refactoring LINE AI Card Assistant with agents-cli and ADK Go&lt;/a&gt;&lt;/strong&gt;: Completed &lt;strong&gt;Phase 1 (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/10" rel="noopener noreferrer"&gt;PR #10&lt;/a&gt;)&lt;/strong&gt;, standardizing the project into an &lt;code&gt;agents-cli&lt;/code&gt; (&lt;code&gt;adk_go&lt;/code&gt;) dual-channel architecture, supporting LINE Webhook, ADK WebUI Playground, and A2A protocols.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;&lt;a href="https://dev.to/2026/10/20/adk-graph-workflow-go-3/"&gt;[Agent Platform in Action (3)] Solving HITL Amnesia in Stateless Containers: Implementing Cross-Instance Card Recognition and Pause/Resume with ADK 2.0 GCS Artifact Service and Agent Platform Sessions&lt;/a&gt;&lt;/strong&gt;: Completed the &lt;strong&gt;first half of Phase 2 (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/11" rel="noopener noreferrer"&gt;PR #11&lt;/a&gt;)&lt;/strong&gt;, introducing &lt;code&gt;artifact/gcsartifact&lt;/code&gt; and &lt;code&gt;session/vertexai&lt;/code&gt; to solve cross-instance HITL recovery and image storage issues in stateless containers.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;&lt;a href="https://dev.to/2026/10/21/adk-graph-workflow-go-4/"&gt;[Agent Platform in Action (4)] Towards Cloud Dual-Layer Decoupling and Full Persistence: Completing the LINE AI Card Assistant Architecture with Cloud Run LINE Webhook Gateway, Agent Runtime, and Cloud SQL PostgreSQL&lt;/a&gt;&lt;/strong&gt;: Completed the &lt;strong&gt;second half of Phase 2 (&lt;a href="https://github.com/kkdai/line-card-assistant/issues/5" rel="noopener noreferrer"&gt;Issue #5&lt;/a&gt;)&lt;/strong&gt; and &lt;strong&gt;Phase 3 (&lt;a href="https://github.com/kkdai/line-card-assistant/issues/6" rel="noopener noreferrer"&gt;Issue #6&lt;/a&gt;, &lt;a href="https://github.com/kkdai/line-card-assistant/pull/12" rel="noopener noreferrer"&gt;PR #12&lt;/a&gt;)&lt;/strong&gt;, migrating the card library to Cloud SQL PostgreSQL (&lt;code&gt;PostgresStore&lt;/code&gt;) and building a stateless Cloud Run LINE Webhook Gateway (&lt;code&gt;internal/gateway&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;&lt;a href="https://dev.to/2026/10/22/adk-graph-workflow-go-5/"&gt;[Agent Platform in Action (5)] Building the AI Agent Quality Flywheel: Establishing Automated Evaluation Benchmarks for LINE AI Card Assistant with agents-cli eval and ADK 2.0 Go&lt;/a&gt;&lt;/strong&gt;: Completed &lt;strong&gt;Phase 4 (&lt;a href="https://github.com/kkdai/line-card-assistant/issues/7" rel="noopener noreferrer"&gt;Issue #7&lt;/a&gt;, &lt;a href="https://github.com/kkdai/line-card-assistant/pull/13" rel="noopener noreferrer"&gt;PR #13&lt;/a&gt;)&lt;/strong&gt;, using &lt;code&gt;agents-cli eval&lt;/code&gt; to establish an automated quality flywheel covering 6 golden cases and 4 major evaluation metrics, all scoring &lt;code&gt;1.0000&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this final chapter today, we will complete the last piece of the puzzle for &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/issues/9" rel="noopener noreferrer"&gt;Roadmap #9&lt;/a&gt;&lt;/strong&gt; — &lt;strong&gt;Phase 5 (&lt;a href="https://github.com/kkdai/line-card-assistant/issues/8" rel="noopener noreferrer"&gt;Issue #8&lt;/a&gt;): Official Agent Runtime Deployment, Terraform IaC, GitHub Actions CI/CD, and Full-Stack Cloud Trace Observability&lt;/strong&gt;!&lt;/p&gt;




&lt;h1&gt;
  
  
  I. What Problem Does Phase 5 Solve? Moving from "Manual &lt;code&gt;gcloud&lt;/code&gt; Commands" to "Declarative AgentOps"
&lt;/h1&gt;

&lt;p&gt;In many AI projects, even if the code and evaluation tests are written, production environments often become operational nightmares due to the following four issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Infrastructure relies on manual commands (ClickOps / Shell Script drift)&lt;/strong&gt;: If Vertex AI Reasoning Engine, Service Account IAM permissions, GCS Buckets, Secret Manager secrets, and Cloud SQL are all created manually via &lt;code&gt;gcloud&lt;/code&gt;, configuration inconsistencies easily occur between Staging and Production environments.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;PR merges lack an AI quality gatekeeper (No Eval Gate in CI)&lt;/strong&gt;: Running &lt;code&gt;go test&lt;/code&gt; can catch syntax and logic errors, but if someone accidentally breaks a Graph Node's &lt;code&gt;state_delta&lt;/code&gt; or Prompt, how can we automatically intercept it before the PR is merged into &lt;code&gt;main&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Cross-environment deployment and high-concurrency verification (Staging $\rightarrow$ Load Test $\rightarrow$ Prod Approval)&lt;/strong&gt;: After pushing to &lt;code&gt;main&lt;/code&gt;, how can we use keyless &lt;strong&gt;Workload Identity Federation (WIF)&lt;/strong&gt; to automatically deploy to Staging, run high-concurrency pressure tests (Vegeta Load Test), archive the reports to GCS, and finally push to Production only after manual approval (GitHub Environment Approval Gate)?&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Graph Workflow node-level observability (Node-Level Distributed Tracing)&lt;/strong&gt;: When processing a business card from upload to Flex Message preview takes 2.8 seconds, is the time spent in &lt;code&gt;VisionOCRNode&lt;/code&gt; or in the parallel Fan-Out's &lt;code&gt;CompanyEnrichNode&lt;/code&gt; (Google Search Grounding)? We need every node of the ADK 2.0 Graph Workflow to expand into a clear nested Span tree in &lt;strong&gt;Google Cloud Trace&lt;/strong&gt;, and automatically import it into &lt;strong&gt;BigQuery&lt;/strong&gt; for P95/P99 latency and Token cost analysis!&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is exactly what the &lt;strong&gt;Phase 5 Full-Stack Architecture Diagram&lt;/strong&gt; at the beginning of the article depicts through four core pillars:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Top-left (GitHub Actions CI/CD &amp;amp; Quality Gate)&lt;/strong&gt;: &lt;code&gt;.github/workflows/pr_checks.yaml&lt;/code&gt;, &lt;code&gt;staging.yaml&lt;/code&gt;, &lt;code&gt;deploy-to-prod.yaml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Bottom-left (Terraform Infrastructure as Code)&lt;/strong&gt;: &lt;code&gt;deployment/terraform/single-project/&lt;/code&gt;, &lt;code&gt;cicd/&lt;/code&gt;, &lt;code&gt;shared/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Center (Vertex AI Agent Runtime + OpenTelemetry Node Spans)&lt;/strong&gt;: &lt;code&gt;agents-cli deploy&lt;/code&gt; + &lt;code&gt;internal/workflow/telemetry.go&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Right (Full-Stack Observability &amp;amp; Analytics)&lt;/strong&gt;: &lt;code&gt;observability.go&lt;/code&gt; $\rightarrow$ Cloud Trace + Cloud Logging $\rightarrow$ &lt;code&gt;telemetry.tf&lt;/code&gt; BigQuery Sink&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  II. Step 1: Generate Terraform IaC and CI/CD Scaffolding with &lt;code&gt;agents-cli scaffold enhance&lt;/code&gt;
&lt;/h1&gt;

&lt;p&gt;In Phase 1, we introduced the Prototype-First philosophy of &lt;code&gt;agents-cli&lt;/code&gt;: keep the project lightweight first, and then execute &lt;code&gt;agents-cli scaffold enhance&lt;/code&gt; to layer on production infrastructure when ready for the cloud.&lt;/p&gt;

&lt;p&gt;We execute the following directly in the project root directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uvx google-agents-cli@1.10.0 scaffold enhance &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cicd-runner&lt;/span&gt; github_actions &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; asia-east1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;agents-cli&lt;/code&gt; automatically detected our &lt;code&gt;agents-cli-manifest.yaml&lt;/code&gt; (&lt;code&gt;template: adk_go&lt;/code&gt;, &lt;code&gt;deployment_target: agent_runtime&lt;/code&gt;) and, without breaking any existing &lt;code&gt;app/&lt;/code&gt;, &lt;code&gt;internal/&lt;/code&gt;, &lt;code&gt;eval/&lt;/code&gt;, or &lt;code&gt;sessions/&lt;/code&gt; code, added three major modules to the project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;&lt;code&gt;deployment/terraform/single-project/&lt;/code&gt; and &lt;code&gt;deployment/terraform/cicd/&lt;/code&gt;&lt;/strong&gt;: Complete Terraform declarative infrastructure modules.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;&lt;code&gt;deployment/terraform/shared/telemetry.tf&lt;/code&gt; and &lt;code&gt;storage.tf&lt;/code&gt;&lt;/strong&gt;: Cloud Logging linked to Buckets, Datasets, and BigQuery telemetry analysis pipelines.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;&lt;code&gt;.github/workflows/&lt;/code&gt; and &lt;code&gt;e2e/&lt;/code&gt;&lt;/strong&gt;: GitHub Actions CI/CD pipelines and A2A / AppInfo / Vegeta Load Test end-to-end test suites.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Next, we perform customized enhancements for the specific requirements of &lt;strong&gt;LINE AI Card Assistant&lt;/strong&gt; (LINE credentials, Cloud SQL PostgreSQL permissions, Taiwan &lt;code&gt;asia-east1&lt;/code&gt; region)!&lt;/p&gt;




&lt;h1&gt;
  
  
  III. Step 2: Deep Dive into Terraform IaC (Vertex AI Reasoning Engine + Secret Manager + BigQuery Telemetry Sink)
&lt;/h1&gt;

&lt;p&gt;Let's open &lt;code&gt;deployment/terraform/&lt;/code&gt; and see how Terraform declaratively builds the entire Google Cloud Agent Platform:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Declaratively Defining Vertex AI Reasoning Engine (&lt;code&gt;service.tf&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;In &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/deployment/terraform/single-project/service.tf" rel="noopener noreferrer"&gt;&lt;code&gt;deployment/terraform/single-project/service.tf&lt;/code&gt;&lt;/a&gt;, we use the &lt;code&gt;google_vertex_ai_reasoning_engine&lt;/code&gt; resource from the &lt;code&gt;google-beta&lt;/code&gt; provider to package the Go source code and declare it as a managed Agent Runtime instance. We also set &lt;code&gt;lifecycle { ignore_changes = [spec[0].source_code_spec] }&lt;/code&gt; so that daily code updates are handled by the CI/CD's &lt;code&gt;agents-cli deploy&lt;/code&gt; for fast rolling deployment, while infrastructure settings (IAM, Buckets, environment variables) are unified under Terraform control:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_vertex_ai_reasoning_engine"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;provider&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google-beta&lt;/span&gt;
  &lt;span class="nx"&gt;display_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_id&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;

  &lt;span class="nx"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"agent-framework"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agent_framework&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;spec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;identity_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AGENT_IDENTITY"&lt;/span&gt;
    &lt;span class="nx"&gt;agent_framework&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;agent_framework&lt;/span&gt;

    &lt;span class="nx"&gt;source_code_spec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;inline_source&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;source_archive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;archive_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output_path&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;developer_connect_source&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;container_spec&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;image_uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-docker.pkg.dev/vertex-ai/prediction/tf2-cpu.2-15:latest"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;lifecycle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ignore_changes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;source_code_spec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;container_spec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Least Privilege IAM and Secret Manager Integration (&lt;code&gt;iam.tf&lt;/code&gt; &amp;amp; &lt;code&gt;variables.tf&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;To prevent &lt;code&gt;LINE_CHANNEL_SECRET&lt;/code&gt;, &lt;code&gt;LINE_CHANNEL_ACCESS_TOKEN&lt;/code&gt;, and &lt;code&gt;DATABASE_URL&lt;/code&gt; from appearing in plain text in configuration files, we declare &lt;strong&gt;Google Cloud Secret Manager&lt;/strong&gt; secrets in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/deployment/terraform/single-project/iam.tf" rel="noopener noreferrer"&gt;&lt;code&gt;deployment/terraform/single-project/iam.tf&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/deployment/terraform/single-project/variables.tf" rel="noopener noreferrer"&gt;&lt;code&gt;variables.tf&lt;/code&gt;&lt;/a&gt;. We grant &lt;code&gt;roles/secretmanager.secretAccessor&lt;/code&gt; and &lt;code&gt;roles/cloudsql.client&lt;/code&gt; to both the &lt;strong&gt;Vertex AI Service Agent (&lt;code&gt;gcp-sa-aiplatform-re&lt;/code&gt;)&lt;/strong&gt; and the &lt;strong&gt;Agent Identity Principal (&lt;code&gt;principal://${google_vertex_ai_reasoning_engine.app.spec[0].effective_identity}&lt;/code&gt;)&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Secret Manager secrets for LINE Messaging API credentials and Cloud SQL DSN&lt;/span&gt;
&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;line_card_secrets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"line-channel-secret"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"line-channel-access-token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"line-card-database-url"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_secret_manager_secret"&lt;/span&gt; &lt;span class="s2"&gt;"app_secrets"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;toset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;line_card_secrets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_id&lt;/span&gt;
  &lt;span class="nx"&gt;secret_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;

  &lt;span class="nx"&gt;replication&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;auto&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;google_project_service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;services&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, during deployment, you only need to pass &lt;code&gt;--secrets="LINE_CHANNEL_SECRET=line-channel-secret,LINE_CHANNEL_ACCESS_TOKEN=line-channel-access-token"&lt;/code&gt; to &lt;code&gt;agents-cli deploy&lt;/code&gt;, and the Agent Runtime will securely mount the secrets from Secret Manager when starting the container!&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Automatically Creating Cloud Logging $\rightarrow$ BigQuery Telemetry Analysis Dataset (&lt;code&gt;telemetry.tf&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;In &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/deployment/terraform/shared/telemetry.tf" rel="noopener noreferrer"&gt;&lt;code&gt;deployment/terraform/shared/telemetry.tf&lt;/code&gt;&lt;/a&gt;, Terraform automatically creates a Cloud Logging Bucket with a 365-day retention period, enables &lt;code&gt;enable_analytics = true&lt;/code&gt;, and directly maps telemetry logs to a &lt;strong&gt;BigQuery Dataset (&lt;code&gt;telemetry_logs_dataset&lt;/code&gt;)&lt;/strong&gt; via &lt;code&gt;google_logging_linked_dataset&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_logging_project_bucket_config"&lt;/span&gt; &lt;span class="s2"&gt;"telemetry_logs_bucket"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"global"&lt;/span&gt;
  &lt;span class="nx"&gt;bucket_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-telemetry"&lt;/span&gt;
  &lt;span class="nx"&gt;retention_days&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt;
  &lt;span class="nx"&gt;enable_analytics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"google_logging_linked_dataset"&lt;/span&gt; &lt;span class="s2"&gt;"telemetry_logs_dataset"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;google_logging_project_bucket_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;telemetry_logs_bucket&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;bucket_id&lt;/span&gt;
  &lt;span class="nx"&gt;link_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_telemetry"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"-"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"_"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"global"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means all structured telemetry data output by OpenTelemetry can be analyzed directly in BigQuery using standard SQL without writing additional ETL scripts!&lt;/p&gt;




&lt;h1&gt;
  
  
  IV. Step 3: Injecting OpenTelemetry Distributed Tracing into the 14 Nodes of ADK 2.0 Go Graph Workflow (&lt;code&gt;internal/workflow/telemetry.go&lt;/code&gt;)
&lt;/h1&gt;

&lt;p&gt;In the &lt;code&gt;observability.go&lt;/code&gt; generated in Phase 1, the system was already configured with an OTLP HTTP Exporter pointing to &lt;code&gt;telemetry.googleapis.com&lt;/code&gt; and Google ADC automatic authentication. However, by default, we only see top-level traces for the entire HTTP request or LLM call, but not the &lt;strong&gt;execution time and routing decisions of each &lt;code&gt;FunctionNode&lt;/code&gt; within the ADK 2.0 Graph Workflow&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To present the processing of each business card as a complete waterfall span tree on &lt;strong&gt;Google Cloud Trace&lt;/strong&gt;, we added &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/workflow/telemetry.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/workflow/telemetry.go&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;

    &lt;span class="s"&gt;"go.opentelemetry.io/otel"&lt;/span&gt;
    &lt;span class="s"&gt;"go.opentelemetry.io/otel/attribute"&lt;/span&gt;
    &lt;span class="s"&gt;"go.opentelemetry.io/otel/codes"&lt;/span&gt;
    &lt;span class="s"&gt;"go.opentelemetry.io/otel/trace"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;workflowTracerName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"github.com/kkdai/line-card-assistant/internal/workflow"&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;AttrWorkflowNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"adk.workflow.node"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;AttrWorkflowRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"adk.workflow.route"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;AttrInvocationID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"adk.invocation.id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;AttrSessionID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"adk.session.id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;AttrUserID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"adk.user.id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;AttrCardID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"adk.card.id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;AttrCardCompany&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"adk.card.company"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;AttrCardDuplicate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"adk.card.is_duplicate"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;AttrHITLInterrupt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"adk.hitl.interrupt_id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;AttrHITLAction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"adk.hitl.action"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;AttrSearchMatches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"adk.search.matches"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;AttrSearchKeywords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"adk.search.keywords"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;startNodeSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nodeName&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KeyValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Span&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;baseAttrs&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;attribute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KeyValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;baseAttrs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseAttrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AttrWorkflowNode&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nodeName&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;baseAttrs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseAttrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attrs&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;otel&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Tracer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workflowTracerName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"workflow.node."&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;nodeName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;trace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithAttributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseAttrs&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we inject &lt;code&gt;startNodeSpan&lt;/code&gt; and &lt;code&gt;endNodeSpan&lt;/code&gt; into all nodes in &lt;code&gt;internal/workflow/nodes_ingest.go&lt;/code&gt; and &lt;code&gt;internal/workflow/nodes_search.go&lt;/code&gt;. For example, in &lt;code&gt;HybridSearchNode&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;newHybridSearchNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cardStore&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CardStore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FunctionNode&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewFunctionNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;NodeHybridSearch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;spanCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;startNodeSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NodeHybridSearch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AttrInvocationID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InvocationID&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
            &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;routeAttr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Routes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;routeAttr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Routes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="n"&gt;endNodeSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AttrWorkflowRoute&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;routeAttr&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;}()&lt;/span&gt;
            &lt;span class="c"&gt;// ... execute cardStore.HybridSearch(spanCtx, filter) ...&lt;/span&gt;
            &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SetAttributes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AttrSearchMatches&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
            &lt;span class="c"&gt;// ...&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NodeConfig&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a user uploads a business card or performs a hybrid search, Cloud Trace automatically draws a hierarchical Span tree like the one below, including the time spent in the three child nodes of the parallel Fan-Out and the business attributes of each node:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dot"&gt;&lt;code&gt;&lt;span class="nv"&gt;flowchart&lt;/span&gt; &lt;span class="nv"&gt;TD&lt;/span&gt;
    &lt;span class="nv"&gt;Root&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"invoke_workflow (line_card_assistant)&amp;lt;br/&amp;gt;trace_id: 4bf92f3577b34da6a3ce929d0e0e4736"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;R&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"workflow.node.RouterNode&amp;lt;br/&amp;gt;adk.workflow.route = 'ingest' (1.2ms)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;R&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;V&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"workflow.node.VisionOCRNode&amp;lt;br/&amp;gt;adk.card.company = 'Taiwan AI Labs' (640ms)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;V&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;C1&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"workflow.node.CleanAndDedupNode&amp;lt;br/&amp;gt;adk.card.is_duplicate = false (8.5ms)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;V&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;C2&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"workflow.node.CompanyEnrichNode&amp;lt;br/&amp;gt;GoogleSearch Grounding (780ms)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;V&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;C3&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"workflow.node.ContextMatchNode&amp;lt;br/&amp;gt;inferMetContext (310ms)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;C1&lt;/span&gt; &lt;span class="err"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;C2&lt;/span&gt; &lt;span class="err"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;C3&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;B&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"workflow.node.BuildPreviewNode&amp;lt;br/&amp;gt;Flex Bubble JSON (2.1ms)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;B&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;H&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"workflow.node.HITLConfirmNode&amp;lt;br/&amp;gt;adk.hitl.interrupt_id = 'hitl_confirm_1'&amp;lt;br/&amp;gt;adk.hitl.action = 'pause' (0.8ms)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also wrote automated unit tests in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/workflow/telemetry_test.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/workflow/telemetry_test.go&lt;/code&gt;&lt;/a&gt; using the OpenTelemetry SDK's &lt;code&gt;tracetest.NewInMemoryExporter()&lt;/code&gt;, ensuring that "card recognition + HITL modification loop + confirmation save" and "hybrid search with 0 results automatically degrading to fuzzy search" 100% generate the expected Span nodes and attribute tags!&lt;/p&gt;




&lt;h1&gt;
  
  
  V. Step 4: Building a GitHub Actions CI/CD Pipeline Integrated with &lt;code&gt;agents-cli eval&lt;/code&gt; Quality Gatekeeper (&lt;code&gt;.github/workflows/&lt;/code&gt;)
&lt;/h1&gt;

&lt;p&gt;With Terraform IaC and OpenTelemetry tracing in place, we officially embedded the &lt;strong&gt;&lt;code&gt;agents-cli eval&lt;/code&gt; quality flywheel&lt;/strong&gt; created in Phase 4 into the GitHub Actions CI/CD pipeline, forming a two-stage automated delivery:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dot"&gt;&lt;code&gt;&lt;span class="nv"&gt;flowchart&lt;/span&gt; &lt;span class="nv"&gt;LR&lt;/span&gt;
    &lt;span class="k"&gt;subgraph&lt;/span&gt; &lt;span class="nv"&gt;PR&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"1. Pull Request Quality Gate (pr_checks.yaml)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;P1&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"go test -race -v ./...&amp;lt;br/&amp;gt;(Unit + OTel Span + Trajectory Tests)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;P2&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"go test -tags=e2e&amp;lt;br/&amp;gt;(A2A + /app-info E2E Tests)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;P2&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;P3&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"agents-cli eval run&amp;lt;br/&amp;gt;(4 major metrics must = 1.0000)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;subgraph&lt;/span&gt; &lt;span class="nv"&gt;CD&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"2. Staging &amp;amp; Production CD (staging.yaml -&amp;gt; deploy-to-prod.yaml)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;S1&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"WIF Keyless Auth&amp;lt;br/&amp;gt;(google-github-actions/auth)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;S2&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"agents-cli deploy&amp;lt;br/&amp;gt;Deploy to Staging Agent Runtime"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;S2&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;S3&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Vegeta Load Test&amp;lt;br/&amp;gt;30s pressure test + report upload to GCS"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;S3&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;S4&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"GitHub Environment&amp;lt;br/&amp;gt;Production Manual Approval Gate"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
        &lt;span class="nv"&gt;S4&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;S5&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"agents-cli deploy&amp;lt;br/&amp;gt;Deploy to Production Agent Runtime"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;end&lt;/span&gt;

    &lt;span class="nv"&gt;PR&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;|&lt;/span&gt;&lt;span class="nv"&gt;Merge&lt;/span&gt; &lt;span class="nv"&gt;to&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;CD&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1. PR Quality Gatekeeper (&lt;code&gt;.github/workflows/pr_checks.yaml&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;In &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/.github/workflows/pr_checks.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;.github/workflows/pr_checks.yaml&lt;/code&gt;&lt;/a&gt;, whenever a PR modifies &lt;code&gt;app/**&lt;/code&gt;, &lt;code&gt;internal/**&lt;/code&gt;, &lt;code&gt;cmd/**&lt;/code&gt;, &lt;code&gt;sessions/**&lt;/code&gt;, or &lt;code&gt;eval/**&lt;/code&gt;, GitHub Actions will execute in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;code&gt;go test -race -v ./...&lt;/code&gt;: Verify all Go unit tests, cross-instance HITL recovery, OpenTelemetry Node Spans, and trajectory tests.&lt;/li&gt;
&lt;li&gt; &lt;code&gt;go test -v -tags=e2e ./e2e/integration/...&lt;/code&gt;: Start a real ADK Universal Launcher subprocess to verify &lt;code&gt;/.well-known/agent-card.json&lt;/code&gt;, &lt;code&gt;/a2a/invoke&lt;/code&gt;, and &lt;code&gt;/apps/app/app-info&lt;/code&gt; endpoints.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;&lt;code&gt;agents-cli eval run&lt;/code&gt; Quality Gatekeeper&lt;/strong&gt;: Execute 6 golden evaluation cases, verifying that the four major metrics (&lt;code&gt;workflow_trajectory_accuracy&lt;/code&gt;, &lt;code&gt;ocr_and_enrichment_quality&lt;/code&gt;, &lt;code&gt;duplicate_and_hitl_precision&lt;/code&gt;, &lt;code&gt;search_and_email_grounding&lt;/code&gt;) must maintain a perfect score!&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  2. Staging Automated Deployment and Vegeta Load Testing (&lt;code&gt;.github/workflows/staging.yaml&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;After the PR is merged into the &lt;code&gt;main&lt;/code&gt; branch, &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/.github/workflows/staging.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;.github/workflows/staging.yaml&lt;/code&gt;&lt;/a&gt; will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Obtain short-term GCP credentials keylessly via &lt;strong&gt;Workload Identity Federation (WIF)&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Call &lt;code&gt;uvx google-agents-cli@1.10.0 deploy&lt;/code&gt; to deploy the latest commit to the Staging Vertex AI Agent Runtime, while mounting &lt;code&gt;LINE_CHANNEL_SECRET&lt;/code&gt; and &lt;code&gt;LINE_CHANNEL_ACCESS_TOKEN&lt;/code&gt; from Secret Manager and tagging &lt;code&gt;OTEL_RESOURCE_ATTRIBUTES=service.version=$&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; Start &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/e2e/load_test/load_test.go" rel="noopener noreferrer"&gt;&lt;code&gt;e2e/load_test/load_test.go&lt;/code&gt;&lt;/a&gt; (based on &lt;code&gt;tsenart/vegeta/v12&lt;/code&gt;) to perform a 30-second continuous pressure test on the Staging Agent Runtime, checking HTTP 200 success rate, P50/P95/P99 latency, and &lt;code&gt;429 Too Many Requests&lt;/code&gt; ratio, and automatically archiving the results to &lt;code&gt;gs://${LOGS_BUCKET_NAME_STAGING}/load-test-results/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; After the pressure test passes, automatically trigger &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/.github/workflows/deploy-to-prod.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;.github/workflows/deploy-to-prod.yaml&lt;/code&gt;&lt;/a&gt;. After passing the Required Reviewers audit for the GitHub &lt;code&gt;production&lt;/code&gt; environment, it is officially pushed to Production!&lt;/li&gt;
&lt;/ol&gt;




&lt;h1&gt;
  
  
  VI. Practical Verification: &lt;code&gt;agents-cli deploy --dry-run&lt;/code&gt; and Full E2E Tests All Passed!
&lt;/h1&gt;

&lt;p&gt;Before submitting the code as a PR, we fully executed three major verifications locally:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;agents-cli deploy --dry-run&lt;/code&gt; Cloud Deployment Preflight
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;uvx google-agents-cli@1.10.0 deploy &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--project&lt;/span&gt; line-vertex &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--region&lt;/span&gt; asia-east1 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--no-confirm-project&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Preflight output results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;&lt;code&gt;aiplatform.googleapis.com&lt;/code&gt; API&lt;/strong&gt;: Enabled ✅&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Target Agent Runtime Engine&lt;/strong&gt;: &lt;code&gt;line-card-assistant&lt;/code&gt; (&lt;code&gt;asia-east1&lt;/code&gt;) ✅&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;IAM Roles &amp;amp; Service Account Preflight&lt;/strong&gt;: Passed ✅&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Go Unit Tests + OpenTelemetry Span Tests + E2E Integration Tests
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;go &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nt"&gt;-tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;e2e ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Includes the newly added &lt;code&gt;TestWorkflowOpenTelemetrySpans_IngestAndHITL&lt;/code&gt;, &lt;code&gt;TestWorkflowOpenTelemetrySpans_SearchFallback&lt;/code&gt;, and all tests under &lt;code&gt;e2e/integration&lt;/code&gt; such as &lt;code&gt;TestA2AAgentCard&lt;/code&gt;, &lt;code&gt;TestA2AMessageSend&lt;/code&gt;, &lt;code&gt;TestA2AErrorHandling&lt;/code&gt;, and &lt;code&gt;TestAppInfo&lt;/code&gt; — all &lt;strong&gt;PASS&lt;/strong&gt;!&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Phase 4 &lt;code&gt;agents-cli eval&lt;/code&gt; Quality Flywheel Regression Verification
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ DRY_RUN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;make &lt;span class="nb"&gt;eval&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 6 golden cases across the 4 custom metrics (&lt;code&gt;workflow_trajectory_accuracy&lt;/code&gt;, &lt;code&gt;ocr_and_enrichment_quality&lt;/code&gt;, &lt;code&gt;duplicate_and_hitl_precision&lt;/code&gt;, &lt;code&gt;search_and_email_grounding&lt;/code&gt;) still maintain &lt;strong&gt;&lt;code&gt;1.0000&lt;/code&gt; (100% Pass)&lt;/strong&gt;!&lt;/p&gt;




&lt;h1&gt;
  
  
  VII. "Agent Platform in Action" Series Summary: A Complete Five-Stage Evolution Map
&lt;/h1&gt;

&lt;p&gt;With the completion of &lt;strong&gt;Phase 5 (&lt;a href="https://github.com/kkdai/line-card-assistant/issues/8" rel="noopener noreferrer"&gt;Issue #8&lt;/a&gt;)&lt;/strong&gt;, the five stages planned in &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/issues/9" rel="noopener noreferrer"&gt;Roadmap Issue #9&lt;/a&gt;&lt;/strong&gt; have been &lt;strong&gt;100% achieved and closed&lt;/strong&gt;! Let's conclude with a summary table reviewing this journey from 0 to 1 with &lt;strong&gt;Google ADK 2.0 Go + Google Cloud Agent Platform&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Stage&lt;/th&gt;
&lt;th&gt;Corresponding Issue / PR&lt;/th&gt;
&lt;th&gt;Core Technical Breakthroughs&lt;/th&gt;
&lt;th&gt;Corresponding Blog Post&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Prototype&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Initial Commit&lt;/td&gt;
&lt;td&gt;Go &lt;code&gt;adk/v2/workflow&lt;/code&gt; 12-node Graph Workflow, parallel &lt;code&gt;AddFanOut&lt;/code&gt;/&lt;code&gt;AddFanIn&lt;/code&gt;, &lt;code&gt;NewRequestInputEvent&lt;/code&gt; HITL loop&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/2026/10/16/adk-graph-workflow-go/"&gt;In Action (1)&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Phase 1&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/kkdai/line-card-assistant/issues/4" rel="noopener noreferrer"&gt;Issue #4&lt;/a&gt; / &lt;a href="https://github.com/kkdai/line-card-assistant/pull/10" rel="noopener noreferrer"&gt;PR #10&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;agents-cli&lt;/code&gt; (&lt;code&gt;adk_go&lt;/code&gt;) standardization, &lt;code&gt;app.NewRootAgent&lt;/code&gt; dual-channel factory, ADK WebUI Playground, and A2A protocol support&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/2026/10/17/adk-graph-workflow-go-2/"&gt;In Action (2)&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Phase 2 (Part 1)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/kkdai/line-card-assistant/issues/5" rel="noopener noreferrer"&gt;Issue #5&lt;/a&gt; / &lt;a href="https://github.com/kkdai/line-card-assistant/pull/11" rel="noopener noreferrer"&gt;PR #11&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;artifact/gcsartifact&lt;/code&gt; zero-state image bloat, &lt;code&gt;session/vertexai&lt;/code&gt; cross-instance stateless HITL pause and resume&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/2026/10/20/adk-graph-workflow-go-3/"&gt;In Action (3)&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Phase 2 (Part 2) &amp;amp; Phase 3&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/kkdai/line-card-assistant/issues/5" rel="noopener noreferrer"&gt;Issue #5&lt;/a&gt;, &lt;a href="https://github.com/kkdai/line-card-assistant/issues/6" rel="noopener noreferrer"&gt;#6&lt;/a&gt; / &lt;a href="https://github.com/kkdai/line-card-assistant/pull/12" rel="noopener noreferrer"&gt;PR #12&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;Cloud SQL PostgreSQL (&lt;code&gt;PostgresStore&lt;/code&gt;) card persistence, Cloud Run LINE Webhook Gateway (&lt;code&gt;internal/gateway&lt;/code&gt;) and Agent Runtime dual-layer decoupling&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/2026/10/21/adk-graph-workflow-go-4/"&gt;In Action (4)&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Phase 4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/kkdai/line-card-assistant/issues/7" rel="noopener noreferrer"&gt;Issue #7&lt;/a&gt; / &lt;a href="https://github.com/kkdai/line-card-assistant/pull/13" rel="noopener noreferrer"&gt;PR #13&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;agents-cli eval&lt;/code&gt; quality flywheel, &lt;code&gt;/app-info&lt;/code&gt; reflection introspection, 6 golden cases, and &lt;code&gt;setNodeTrace&lt;/code&gt; trajectory repair (all 4 metrics at &lt;code&gt;1.0000&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;a href="https://dev.to/2026/10/22/adk-graph-workflow-go-5/"&gt;In Action (5)&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Phase 5&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://github.com/kkdai/line-card-assistant/issues/8" rel="noopener noreferrer"&gt;Issue #8&lt;/a&gt; / &lt;a href="https://github.com/kkdai/line-card-assistant/pull/14" rel="noopener noreferrer"&gt;PR #14&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;agents-cli deploy&lt;/code&gt;, Terraform IaC (&lt;code&gt;deployment/terraform/&lt;/code&gt;), GitHub Actions CI/CD + WIF + Vegeta pressure testing, OpenTelemetry node-level Cloud Trace &amp;amp; BigQuery observation&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;This article (In Action 6)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The complete open-source code, Terraform modules, and evaluation datasets have been synchronized to the GitHub Repo: &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/line-card-assistant&lt;/code&gt;&lt;/a&gt;&lt;/strong&gt;. If you are also building enterprise-grade AI Agents with Go and Google ADK 2.0, I hope this series serves as your best practical blueprint for moving from local development all the way to the Google Cloud Agent Platform production environment!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cloud</category>
      <category>devops</category>
      <category>go</category>
    </item>
    <item>
      <title>[Agent Platform in Action (3)] Solving Stateless Container HITL Amnesia: Cross-Instance Business Card Recognition and Pause/Resu</title>
      <dc:creator>Evan Lin</dc:creator>
      <pubDate>Fri, 23 Oct 2026 08:33:24 +0000</pubDate>
      <link>https://dev.to/gde/agent-platform-in-action-3-solving-stateless-container-hitl-amnesia-cross-instance-business-3c4h</link>
      <guid>https://dev.to/gde/agent-platform-in-action-3-solving-stateless-container-hitl-amnesia-cross-instance-business-3c4h</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjdcnspaie3wmql69oh5x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjdcnspaie3wmql69oh5x.png" alt="Phase 2: Stateless Cross-Instance HITL &amp;amp; GCS Artifact Architecture" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Recap
&lt;/h1&gt;

&lt;p&gt;In the previous two articles of this series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"&lt;a href="https://dev.to/2026/10/16/adk-graph-workflow-go/"&gt;[Agent Platform in Action (1)] Building a LINE AI Business Card King with Go and Google ADK 2.0 Graph Workflow&lt;/a&gt;"&lt;/strong&gt;: We used &lt;code&gt;google.golang.org/adk/v2/workflow&lt;/code&gt; to implement a 12-node business card assistant featuring a &lt;strong&gt;Deterministic Router&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;AddFanOut&lt;/code&gt; / &lt;code&gt;AddFanIn&lt;/code&gt; three-way parallel background checks&lt;/strong&gt;, and &lt;strong&gt;&lt;code&gt;NewRequestInputEvent&lt;/code&gt; Human-in-the-Loop (HITL) pause and loop correction&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"&lt;a href="https://dev.to/2026/10/17/adk-graph-workflow-go-2/"&gt;[Agent Platform in Action (2)] From Monolithic Cloud Run to Google Cloud Agent Runtime: Refactoring LINE AI Business Card King with agents-cli and ADK Go&lt;/a&gt;"&lt;/strong&gt;: We introduced the &lt;code&gt;agents-cli&lt;/code&gt; official development lifecycle, explained the Agent Runtime's Scale-to-Zero mechanism, and completed &lt;strong&gt;Phase 1 (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/10" rel="noopener noreferrer"&gt;PR #10&lt;/a&gt;)&lt;/strong&gt;, transforming the project into a standard &lt;code&gt;adk_go&lt;/code&gt; dual-channel architecture supporting LINE Webhook, ADK WebUI Playground, and A2A protocols.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Entering &lt;strong&gt;Phase 2 (&lt;a href="https://github.com/kkdai/line-card-assistant/issues/3" rel="noopener noreferrer"&gt;Issue #3&lt;/a&gt; &amp;amp; &lt;a href="https://github.com/kkdai/line-card-assistant/issues/4" rel="noopener noreferrer"&gt;Issue #4&lt;/a&gt;)&lt;/strong&gt;, we face a classic and common pitfall in distributed systems when pushing to production (Cloud Run / Agent Runtime auto-scaling clusters): &lt;strong&gt;When containers can Scale to Zero or be load-balanced to different instances at any time, where should multimodal business card photos (Binary Images) be stored? And how can the HITL pause state, waiting for a user to click a button, be recovered across instances?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the third installment of this series, we will deep dive into how we used &lt;strong&gt;ADK Go v2.6.0&lt;/strong&gt;'s &lt;strong&gt;&lt;code&gt;artifact/gcsartifact&lt;/code&gt; (GCS Artifact Service)&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;session/vertexai&lt;/code&gt; (Agent Platform Sessions)&lt;/strong&gt; in &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/pull/11" rel="noopener noreferrer"&gt;PR #11&lt;/a&gt;&lt;/strong&gt; to completely cure the "HITL amnesia" of stateless containers!&lt;/p&gt;




&lt;h1&gt;
  
  
  1. Why did the original HITL suffer from "Amnesia" and "State Bloat" in cloud multi-instance environments?
&lt;/h1&gt;

&lt;p&gt;During local development or with a single container instance, stuffing everything into memory is fast. However, once deployed to &lt;strong&gt;Google Cloud Run&lt;/strong&gt; or &lt;strong&gt;Vertex AI Agent Runtime&lt;/strong&gt;, which support Horizontal Auto-scaling or Scale-to-Zero, the first version's design immediately hits two major walls:&lt;/p&gt;

&lt;h2&gt;
  
  
  Pain Point 1: The "State Bloat" anti-pattern of stuffing multi-MB business card &lt;code&gt;[]byte&lt;/code&gt; into &lt;code&gt;Session.State&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;In the first version, when a LINE user sent a high-resolution business card photo, we directly put the downloaded &lt;code&gt;imageBytes []byte&lt;/code&gt; into ADK's &lt;code&gt;Session.State["image_bytes"]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ❌ Version 1 approach: Directly stuffing multi-MB raw image []byte into Session State&lt;/span&gt;
&lt;span class="n"&gt;InitialState&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;StateKeyInputType&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RouteIngest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;StateKeyImageBytes&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;imageBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c"&gt;// A 3MB JPEG inflates to a ~4MB Base64 string after JSON serialization!&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When using the local &lt;code&gt;session.InMemoryService()&lt;/code&gt;, this is just a Go slice pointer. But when switching to cloud-persisted &lt;strong&gt;Agent Platform Sessions (&lt;code&gt;session/vertexai&lt;/code&gt;)&lt;/strong&gt; or &lt;strong&gt;Cloud SQL (&lt;code&gt;session/database&lt;/code&gt;)&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Serialization and network transfer for every node state change (StateDelta)&lt;/strong&gt;: As the ADK Graph Workflow moves from &lt;code&gt;RouterNode&lt;/code&gt; to &lt;code&gt;VisionOCRNode&lt;/code&gt;, fans out to three parallel nodes, and finally pauses at &lt;code&gt;HITLConfirmNode&lt;/code&gt;, keeping multi-MB image binary data in &lt;code&gt;Session.State&lt;/code&gt; significantly slows down every Cloud Session API read/write and might even trigger Cloud Session payload size limits!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Difficulty in auditing and versioning history&lt;/strong&gt;: If a user sends multiple business cards across several turns, mixing raw binary files with structured JSON state makes the conversation state extremely bloated.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Pain Point 2: Container instance switching leads to loss of HITL &lt;code&gt;pendingSession&lt;/code&gt; memory mapping
&lt;/h2&gt;

&lt;p&gt;Let's look at a real-world user scenario:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Turn 1 (hits Container Instance A)&lt;/strong&gt;: A user takes a photo of a business card at a conference and sends it to the LINE Bot. &lt;strong&gt;Instance A&lt;/strong&gt; completes Vision OCR and parallel background checks, then pauses at &lt;code&gt;HITLConfirmNode&lt;/code&gt; via &lt;code&gt;workflow.NewRequestInputEvent&lt;/code&gt;, pushing a Flex Message preview card to the user.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;10-minute break&lt;/strong&gt;: The user goes to exchange thoughts with the contact and grabs a coffee. During this time, Instance A scales to 0 due to inactivity, or &lt;strong&gt;Container Instance B&lt;/strong&gt; is scaled out due to other traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn 2 (hits Container Instance B)&lt;/strong&gt;: The user clicks "✏️ Edit Fields" on LINE and enters "&lt;em&gt;Change title to CTO&lt;/em&gt;", or clicks "✅ Confirm Save".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the first version, our &lt;code&gt;Engine&lt;/code&gt; used an in-memory Go &lt;code&gt;map[string]pendingHITL&lt;/code&gt; to track which &lt;code&gt;userID&lt;/code&gt; was waiting for which &lt;code&gt;SessionID&lt;/code&gt; and &lt;code&gt;InterruptID&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ❌ Version 1 approach: Relying solely on a local in-memory map to determine if there's a pending HITL&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Engine&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;mu&lt;/span&gt; &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RWMutex&lt;/span&gt;
    &lt;span class="n"&gt;pendingSession&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;pendingHITL&lt;/span&gt; &lt;span class="c"&gt;// userID -&amp;gt; {SessionID, InterruptID}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As a result, when the Turn 2 Webhook request is routed by the load balancer to the &lt;strong&gt;brand new Instance B&lt;/strong&gt;, Instance B's &lt;code&gt;pendingSession&lt;/code&gt; map is empty! &lt;code&gt;HasPendingHITL(userID)&lt;/code&gt; returns &lt;code&gt;false&lt;/code&gt;, and the system treats "&lt;em&gt;Change title to CTO&lt;/em&gt;" as a brand new "Business Card Search" request, instantly breaking the HITL loop!&lt;/p&gt;




&lt;h1&gt;
  
  
  2. The Standard ADK 2.0 Solution: Separation of Concerns between Session State and Artifact Storage
&lt;/h1&gt;

&lt;p&gt;To solve this, Google ADK 2.0's architectural design explicitly splits Agent runtime data into two independent services:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Comparison Dimension&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Session Service (&lt;code&gt;session.Service&lt;/code&gt;)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Artifact Service (&lt;code&gt;artifact.Service&lt;/code&gt;)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Core Purpose&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stores conversation event logs (Events), workflow breakpoints (Interrupts), and &lt;strong&gt;lightweight structured state (JSON State)&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Stores large binary files (images, PDFs, audio, CSV reports) and provides &lt;strong&gt;automatic version control (Versioning)&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloud Implementation Package&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;google.golang.org/adk/v2/session/vertexai&lt;/code&gt; (Agent Platform Sessions) or &lt;code&gt;session/database&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;google.golang.org/adk/v2/artifact/gcsartifact&lt;/code&gt; (underlying storage in Google Cloud Storage Bucket)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Local Development Implementation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;session.InMemoryService()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;artifact.InMemoryService()&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Role in Business Card King&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stores &lt;code&gt;draft_card&lt;/code&gt; JSON, &lt;code&gt;active_interrupt_id&lt;/code&gt;, and &lt;strong&gt;lightweight pointers (filename + version number)&lt;/strong&gt; to the image&lt;/td&gt;
&lt;td&gt;Stores raw business card photo &lt;code&gt;business_card.jpg&lt;/code&gt; (automatically increments Version &lt;code&gt;1, 2, 3...&lt;/code&gt; per upload)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With this dual-track design, the entire cross-instance multimodal business card recognition and HITL recovery process becomes crystal clear:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dot"&gt;&lt;code&gt;&lt;span class="nv"&gt;sequenceDiagram&lt;/span&gt;
    &lt;span class="nv"&gt;autonumber&lt;/span&gt;
    &lt;span class="nv"&gt;actor&lt;/span&gt; &lt;span class="nv"&gt;User&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;LINE&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt; &lt;span class="nv"&gt;WebUI&lt;/span&gt; &lt;span class="nv"&gt;User&lt;/span&gt;
    &lt;span class="nv"&gt;participant&lt;/span&gt; &lt;span class="nv"&gt;InstA&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;Container&lt;/span&gt; &lt;span class="nv"&gt;Instance&lt;/span&gt; &lt;span class="nv"&gt;A&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;Turn&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Upload&lt;/span&gt; &lt;span class="nv"&gt;Card&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;participant&lt;/span&gt; &lt;span class="nv"&gt;GCS&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;GCS&lt;/span&gt; &lt;span class="nv"&gt;Artifact&lt;/span&gt; &lt;span class="nv"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;br/&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;artifact&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;gcsartifact&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;participant&lt;/span&gt; &lt;span class="nv"&gt;Sess&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;Agent&lt;/span&gt; &lt;span class="nv"&gt;Platform&lt;/span&gt; &lt;span class="nv"&gt;Sessions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;br/&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;session&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;vertexai&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;participant&lt;/span&gt; &lt;span class="nv"&gt;InstB&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;Container&lt;/span&gt; &lt;span class="nv"&gt;Instance&lt;/span&gt; &lt;span class="nv"&gt;B&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;Turn&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Edit&lt;/span&gt; &lt;span class="nv"&gt;Fields&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;participant&lt;/span&gt; &lt;span class="nv"&gt;InstC&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;Container&lt;/span&gt; &lt;span class="nv"&gt;Instance&lt;/span&gt; &lt;span class="nv"&gt;C&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;Turn&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Confirm&lt;/span&gt; &lt;span class="nv"&gt;Save&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;

    &lt;span class="nv"&gt;User&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;InstA&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;Upload&lt;/span&gt; &lt;span class="nv"&gt;business&lt;/span&gt; &lt;span class="nv"&gt;card&lt;/span&gt; &lt;span class="nv"&gt;photo&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;JPEG&lt;/span&gt; &lt;span class="o"&gt;[]&lt;/span&gt;&lt;span class="nv"&gt;byte&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;InstA&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;GCS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;Save&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"business_card.jpg"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;inlineData&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;br/&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;Returns&lt;/span&gt; &lt;span class="n"&gt;Version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="nv"&gt;InstA&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;Sess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;Create&lt;/span&gt; &lt;span class="nv"&gt;Session&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;State&lt;/span&gt; &lt;span class="nv"&gt;only&lt;/span&gt; &lt;span class="nv"&gt;records&lt;/span&gt;&lt;span class="p"&gt;:&amp;lt;&lt;/span&gt;br/&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;card_image_artifact&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"business_card.jpg"&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;br/&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;card_image_version&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="nv"&gt;InstA&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;GCS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;VisionOCRNode&lt;/span&gt; &lt;span class="nv"&gt;calls&lt;/span&gt; &lt;span class="nv"&gt;LoadVersion&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"business_card.jpg"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;Note&lt;/span&gt; &lt;span class="nv"&gt;over&lt;/span&gt; &lt;span class="nv"&gt;InstA&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="nv"&gt;Sess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;Fan&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;Out&lt;/span&gt; &lt;span class="nv"&gt;Parallel&lt;/span&gt; &lt;span class="nv"&gt;duplicate&lt;/span&gt; &lt;span class="nv"&gt;check&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt; &lt;span class="nv"&gt;Company&lt;/span&gt; &lt;span class="nv"&gt;search&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt; &lt;span class="nv"&gt;Interactive&lt;/span&gt; &lt;span class="nv"&gt;retrieval&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Generate&lt;/span&gt; &lt;span class="nv"&gt;preview&lt;/span&gt; &lt;span class="nv"&gt;card&lt;/span&gt;
    &lt;span class="nv"&gt;InstA&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;Sess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;HITLConfirmNode&lt;/span&gt; &lt;span class="nv"&gt;triggers&lt;/span&gt; &lt;span class="nv"&gt;NewRequestInputEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;br/&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;Persists&lt;/span&gt; &lt;span class="nv"&gt;active_interrupt_id&lt;/span&gt; &lt;span class="nv"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;draft_card&lt;/span&gt;
    &lt;span class="nv"&gt;InstA&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;User&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;Return&lt;/span&gt; &lt;span class="nv"&gt;LINE&lt;/span&gt; &lt;span class="nv"&gt;Flex&lt;/span&gt; &lt;span class="nv"&gt;preview&lt;/span&gt; &lt;span class="nv"&gt;card&lt;/span&gt; &lt;span class="err"&gt;(⏸️&lt;/span&gt; &lt;span class="nv"&gt;Workflow&lt;/span&gt; &lt;span class="nv"&gt;Paused&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;

    &lt;span class="nv"&gt;Note&lt;/span&gt; &lt;span class="nv"&gt;over&lt;/span&gt; &lt;span class="nv"&gt;InstA&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="nv"&gt;InstB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;☁️&lt;/span&gt; &lt;span class="nv"&gt;Container&lt;/span&gt; &lt;span class="nv"&gt;scales&lt;/span&gt; &lt;span class="nv"&gt;to&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="nv"&gt;or&lt;/span&gt; &lt;span class="nv"&gt;load&lt;/span&gt; &lt;span class="nv"&gt;balancer&lt;/span&gt; &lt;span class="nv"&gt;switches&lt;/span&gt; &lt;span class="nv"&gt;to&lt;/span&gt; &lt;span class="nv"&gt;a&lt;/span&gt; &lt;span class="nv"&gt;brand&lt;/span&gt; &lt;span class="nv"&gt;new&lt;/span&gt; &lt;span class="nv"&gt;stateless&lt;/span&gt; &lt;span class="nv"&gt;Instance&lt;/span&gt; &lt;span class="nv"&gt;B&lt;/span&gt;

    &lt;span class="nv"&gt;User&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;InstB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Change title to CTO"&lt;/span&gt;
    &lt;span class="nv"&gt;InstB&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;Sess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;resolvePendingHITL&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;userID&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&amp;lt;&lt;/span&gt;br/&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;Query&lt;/span&gt; &lt;span class="nv"&gt;latest&lt;/span&gt; &lt;span class="nv"&gt;Session&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="nv"&gt;s&lt;/span&gt; &lt;span class="nv"&gt;active_interrupt_id&lt;/span&gt;
    &lt;span class="nv"&gt;Sess&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;InstB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;Retrieve&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;SessionID&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;InterruptID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nv"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;draft_card&lt;/span&gt;
    &lt;span class="nv"&gt;InstB&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;Sess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;ResumeHITL&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;ApplyEditNode&lt;/span&gt; &lt;span class="nv"&gt;updates&lt;/span&gt; &lt;span class="nv"&gt;title&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;Re&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;pauses&lt;/span&gt; &lt;span class="nv"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;updates&lt;/span&gt; &lt;span class="nv"&gt;active_interrupt_id&lt;/span&gt;
    &lt;span class="nv"&gt;InstB&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;User&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;Return&lt;/span&gt; &lt;span class="nv"&gt;updated&lt;/span&gt; &lt;span class="nv"&gt;Flex&lt;/span&gt; &lt;span class="nv"&gt;preview&lt;/span&gt; &lt;span class="nv"&gt;card&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;CTO&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;

    &lt;span class="nv"&gt;Note&lt;/span&gt; &lt;span class="nv"&gt;over&lt;/span&gt; &lt;span class="nv"&gt;InstB&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="nv"&gt;InstC&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;☁️&lt;/span&gt; &lt;span class="nv"&gt;Switch&lt;/span&gt; &lt;span class="nv"&gt;again&lt;/span&gt; &lt;span class="nv"&gt;to&lt;/span&gt; &lt;span class="nv"&gt;brand&lt;/span&gt; &lt;span class="nv"&gt;new&lt;/span&gt; &lt;span class="nv"&gt;stateless&lt;/span&gt; &lt;span class="nv"&gt;Instance&lt;/span&gt; &lt;span class="nv"&gt;C&lt;/span&gt;

    &lt;span class="nv"&gt;User&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;InstC&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;Click&lt;/span&gt; &lt;span class="s2"&gt;"✅ Confirm Save"&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;confirm_save&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;InstC&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;Sess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;resolvePendingHITL&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;userID&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;retrieves&lt;/span&gt; &lt;span class="nv"&gt;latest&lt;/span&gt; &lt;span class="nv"&gt;InterruptID&lt;/span&gt;
    &lt;span class="nv"&gt;InstC&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;Sess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;ResumeHITL&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;SaveCardNode&lt;/span&gt; &lt;span class="nv"&gt;writes&lt;/span&gt; &lt;span class="nv"&gt;to&lt;/span&gt; &lt;span class="nv"&gt;card&lt;/span&gt; &lt;span class="nv"&gt;library&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;clears&lt;/span&gt; &lt;span class="nv"&gt;active_interrupt_id&lt;/span&gt;
    &lt;span class="nv"&gt;InstC&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;User&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;Return&lt;/span&gt; &lt;span class="nv"&gt;save&lt;/span&gt; &lt;span class="nv"&gt;success&lt;/span&gt; &lt;span class="nv"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;AI&lt;/span&gt; &lt;span class="nv"&gt;icebreaker&lt;/span&gt; &lt;span class="nv"&gt;draft&lt;/span&gt;&lt;span class="err"&gt;!&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  3. Code Implementation Breakdown (Phase 2 — PR #11)
&lt;/h1&gt;

&lt;p&gt;Now let's look at the code to see how we completed this upgrade with less than a few hundred lines of clean Go code.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Creating a Multi-backend Artifact Factory: &lt;code&gt;sessions/artifacts.go&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;We added &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/sessions/artifacts.go" rel="noopener noreferrer"&gt;&lt;code&gt;sessions/artifacts.go&lt;/code&gt;&lt;/a&gt; under the &lt;code&gt;sessions&lt;/code&gt; package, using ADK Go v2.6.0's built-in &lt;code&gt;google.golang.org/adk/v2/artifact/gcsartifact&lt;/code&gt; package to automatically switch between Cloud GCS Bucket and local memory based on environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;sessions&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"log/slog"&lt;/span&gt;
    &lt;span class="s"&gt;"os"&lt;/span&gt;
    &lt;span class="s"&gt;"strings"&lt;/span&gt;

    &lt;span class="s"&gt;"google.golang.org/adk/v2/artifact"&lt;/span&gt;
    &lt;span class="s"&gt;"google.golang.org/adk/v2/artifact/gcsartifact"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// NewArtifactService returns an ADK artifact.Service backed by Google Cloud Storage&lt;/span&gt;
&lt;span class="c"&gt;// when ARTIFACT_BUCKET_NAME or LOGS_BUCKET_NAME is configured, and falls back to&lt;/span&gt;
&lt;span class="c"&gt;// artifact.InMemoryService() for local development and unit tests.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewArtifactService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;artifact&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;bucketName&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;resolveArtifactBucketName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;bucketName&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Using in-memory artifact service (set ARTIFACT_BUCKET_NAME or LOGS_BUCKET_NAME for GCS)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;artifact&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InMemoryService&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;gcsartifact&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bucketName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Using GCS artifact service"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"bucket"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bucketName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A thoughtful detail: In the &lt;code&gt;agents-cli&lt;/code&gt; Agent Runtime deployment environment, the system usually configures &lt;code&gt;LOGS_BUCKET_NAME&lt;/code&gt; (or our custom &lt;code&gt;ARTIFACT_BUCKET_NAME&lt;/code&gt;). Some might accidentally fill it in with a format like &lt;code&gt;gs://my-bucket/prefix&lt;/code&gt;. Our &lt;code&gt;resolveArtifactBucketName()&lt;/code&gt; automatically strips the &lt;code&gt;gs://&lt;/code&gt; prefix and path to ensure &lt;code&gt;gcsartifact.NewService&lt;/code&gt; connects reliably, falling back to &lt;code&gt;artifact.InMemoryService()&lt;/code&gt; if no bucket is set.&lt;/p&gt;

&lt;p&gt;Simultaneously, we inject &lt;code&gt;ArtifactService&lt;/code&gt; and &lt;code&gt;SessionService&lt;/code&gt; into &lt;code&gt;launcher.Config&lt;/code&gt; in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/main.go" rel="noopener noreferrer"&gt;&lt;code&gt;main.go&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/cmd/server/main.go" rel="noopener noreferrer"&gt;&lt;code&gt;cmd/server/main.go&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// main.go — Injecting both SessionService and ArtifactService into ADK Universal Launcher&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;launcher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;SessionService&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sessionService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ArtifactService&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;artifactService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;AgentLoader&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;NewSingleAgentLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rootAgent&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;A2AOptions&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;a2aOpts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. Storing Business Card Images in Artifacts, Keeping only "Lightweight Pointers" in Session State
&lt;/h2&gt;

&lt;p&gt;Next, we refactor &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/workflow/graph.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/workflow/graph.go&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/workflow/nodes_ingest.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/workflow/nodes_ingest.go&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;First, we define two new lightweight State Keys and a default filename:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;StateKeyImageBytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"image_bytes"&lt;/span&gt; &lt;span class="c"&gt;// Kept only for backward compatibility fallback&lt;/span&gt;
    &lt;span class="n"&gt;StateKeyCardImageArtifact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"card_image_artifact"&lt;/span&gt; &lt;span class="c"&gt;// Artifact filename&lt;/span&gt;
    &lt;span class="n"&gt;StateKeyCardImageVersion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"card_image_version"&lt;/span&gt; &lt;span class="c"&gt;// Artifact version number (int)&lt;/span&gt;

    &lt;span class="n"&gt;DefaultCardImageArtifactName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"business_card.jpg"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  (1) Entering from LINE Webhook (&lt;code&gt;Engine.StartIngest&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;When a LINE user sends an image, &lt;code&gt;StartIngest&lt;/code&gt; first calls &lt;code&gt;e.artifactService.Save&lt;/code&gt; to write the image binary to the Artifact service and &lt;strong&gt;only records the filename and version number&lt;/strong&gt; in &lt;code&gt;InitialState&lt;/code&gt;. &lt;code&gt;StateKeyImageBytes&lt;/code&gt; is no longer written to &lt;code&gt;Session.State&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;initialState&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;StateKeyInputType&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RouteIngest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;StateKeyRecentEvents&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;recentEvents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;artifactService&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;imageBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewPartFromBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;imageBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detectImageMIMEType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;imageBytes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;saveResp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;saveErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;artifactService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;artifact&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SaveRequest&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;AppName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;appName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;UserID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;SessionID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;FileName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DefaultCardImageArtifactName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Part&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;saveErr&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;saveResp&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;StateKeyCardImageArtifact&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DefaultCardImageArtifactName&lt;/span&gt;
        &lt;span class="n"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;StateKeyCardImageVersion&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;saveResp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  (2) Uploading images directly from ADK WebUI / Agent Engine Playground / A2A (&lt;code&gt;RouterNode&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;What if the user isn't using LINE but uploads a business card photo directly via &lt;strong&gt;ADK WebUI (&lt;code&gt;make playground&lt;/code&gt;)&lt;/strong&gt; or &lt;strong&gt;Agent Engine Console&lt;/strong&gt;? In &lt;code&gt;RouterNode&lt;/code&gt;, we use ADK 2.0's &lt;strong&gt;&lt;code&gt;ctx.Artifacts().Save(...)&lt;/code&gt;&lt;/strong&gt; to transfer &lt;code&gt;InlineData&lt;/code&gt; from &lt;code&gt;ctx.UserContent()&lt;/code&gt; into the Artifact Service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// internal/workflow/nodes_ingest.go — RouterNode&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;getStringState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StateKeyCardImageArtifact&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getBytesState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StateKeyImageBytes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserContent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserContent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parts&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InlineData&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InlineData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Artifacts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;mimeType&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InlineData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MIMEType&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mimeType&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;mimeType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;detectImageMIMEType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InlineData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="n"&gt;artifactPart&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewPartFromBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InlineData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mimeType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;saveResp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;saveErr&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Artifacts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DefaultCardImageArtifactName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;artifactPart&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;saveErr&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StateKeyCardImageArtifact&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DefaultCardImageArtifactName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StateKeyCardImageVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;saveResp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StateKeyImageBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InlineData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;getStringState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StateKeyInputType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StateKeyInputType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RouteIngest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  (3) &lt;code&gt;VisionOCRNode&lt;/code&gt; Reads specific versions of business card images on demand (&lt;code&gt;loadCardImageBytes&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;When the workflow enters &lt;code&gt;VisionOCRNode&lt;/code&gt; to call Gemini Vision for multimodal recognition, it uses &lt;code&gt;ctx.Artifacts().LoadVersion&lt;/code&gt; (or &lt;code&gt;Load&lt;/code&gt;) to load the image into memory for the model. It's discarded after use, keeping &lt;code&gt;Session.State&lt;/code&gt; clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;loadCardImageBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;artifactName&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;getStringState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StateKeyCardImageArtifact&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;artifactName&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Artifacts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;getIntState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StateKeyCardImageVersion&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;artifact&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoadResponse&lt;/span&gt;
            &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Artifacts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LoadVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;artifactName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Artifacts&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;artifactName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Part&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Part&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InlineData&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Part&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InlineData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;getBytesState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StateKeyImageBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c"&gt;// Fallback&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3. Implementing Stateless Cross-Instance HITL State Discovery: &lt;code&gt;resolvePendingHITL&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;With image storage solved, here's the main event: &lt;strong&gt;When Container Instance B receives a user's "Change title to CTO" or "✅ Confirm Save", how does it automatically retrieve the HITL workflow paused by Instance A when its local memory is empty?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The secret is: when &lt;code&gt;HITLConfirmNode&lt;/code&gt; calls &lt;code&gt;workflow.NewRequestInputEvent&lt;/code&gt; to pause, we've already written the current &lt;code&gt;InterruptID&lt;/code&gt; into &lt;code&gt;Session.State["active_interrupt_id"]&lt;/code&gt; (and &lt;code&gt;SaveCardNode&lt;/code&gt; clears it to &lt;code&gt;""&lt;/code&gt; once saving is complete).&lt;/p&gt;

&lt;p&gt;Therefore, we implemented &lt;strong&gt;&lt;code&gt;resolvePendingHITL(ctx, userID)&lt;/code&gt;&lt;/strong&gt; in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/workflow/graph.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/workflow/graph.go&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// resolvePendingHITL checks the local cache first and falls back to querying&lt;/span&gt;
&lt;span class="c"&gt;// the shared session.Service for any session belonging to userID with a non-empty&lt;/span&gt;
&lt;span class="c"&gt;// StateKeyActiveInterruptID. This enables stateless cross-instance HITL resume&lt;/span&gt;
&lt;span class="c"&gt;// on Cloud Run / Vertex AI Agent Runtime.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Engine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;resolvePendingHITL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pendingHITL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// 1. Fast path: If a record exists in the same instance's memory, verify with sessionService if it's still paused&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RLock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pendingSession&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RUnlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SessionID&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InterruptID&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;getResp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sessionService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequest&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;AppName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;appName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;UserID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;SessionID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SessionID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;getResp&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;getResp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;activeID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;getResp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StateKeyActiveInterruptID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;activeID&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InterruptID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// 2. Stateless Slow Path: When hitting a brand new container instance, query the shared sessionService directly!&lt;/span&gt;
    &lt;span class="n"&gt;listResp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sessionService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListRequest&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;AppName&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;appName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;UserID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;listResp&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pendingHITL&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;matched&lt;/span&gt; &lt;span class="n"&gt;pendingHITL&lt;/span&gt;
        &lt;span class="n"&gt;latestTime&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt;
        &lt;span class="n"&gt;foundActive&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sess&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;listResp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sessions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sess&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;sess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StateKeyActiveInterruptID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;interruptID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;val&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;interruptID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interruptID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;interruptID&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;updatedAt&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LastUpdateTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;foundActive&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;updatedAt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;After&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latestTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;foundActive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
            &lt;span class="n"&gt;latestTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;updatedAt&lt;/span&gt;
            &lt;span class="n"&gt;matched&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pendingHITL&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;SessionID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;InterruptID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;interruptID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;foundActive&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pendingSession&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;matched&lt;/span&gt;
        &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;matched&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pendingHITL&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Behold the power of this design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Regardless of which stateless instance the user's request is dispatched to by the Cloud Run or Agent Runtime load balancer, &lt;code&gt;HasPendingHITL(userID)&lt;/code&gt; and &lt;code&gt;ResumeHITL(ctx, userID, userInput)&lt;/code&gt; can retrieve the user's latest paused &lt;code&gt;{SessionID, InterruptID}&lt;/code&gt; directly via the shared &lt;code&gt;session.Service&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Then, Instance B simply wraps the user's input into an ADK 2.0 standard &lt;code&gt;adk_request_input&lt;/code&gt; (&lt;code&gt;workflow.WorkflowInputFunctionCallName&lt;/code&gt;) &lt;code&gt;FunctionResponse&lt;/code&gt; and calls &lt;code&gt;e.runner.Run&lt;/code&gt; to seamlessly wake the workflow from &lt;code&gt;HITLConfirmNode&lt;/code&gt;!&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  4. Writing Unit Tests to Verify "Across Three Container Instances"!
&lt;/h2&gt;

&lt;p&gt;To prove this architecture works in a multi-instance environment with no shared local memory, we wrote the &lt;code&gt;TestCrossInstanceHITLResumeAndArtifactStorage&lt;/code&gt; test in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/workflow/graph_test.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/workflow/graph_test.go&lt;/code&gt;&lt;/a&gt;, creating &lt;strong&gt;3 independent &lt;code&gt;Engine&lt;/code&gt; instances (&lt;code&gt;instanceA&lt;/code&gt;, &lt;code&gt;instanceB&lt;/code&gt;, &lt;code&gt;instanceC&lt;/code&gt;)&lt;/strong&gt; to simulate three stateless containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestCrossInstanceHITLResumeAndArtifactStorage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cardStore&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewMemoryStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;mockLLM&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;NewMockLLM&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c"&gt;// Simulating cloud-shared Agent Platform Session Service and GCS Artifact Service&lt;/span&gt;
    &lt;span class="n"&gt;sharedSessionSvc&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InMemoryService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;sharedArtifactSvc&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;artifact&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InMemoryService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="c"&gt;// Creating 3 completely independent Engine instances (each with an independent and empty pendingSession map)&lt;/span&gt;
    &lt;span class="n"&gt;instanceA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;NewEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cardStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mockLLM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;WithSessionService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sharedSessionSvc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;WithArtifactService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sharedArtifactSvc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;instanceB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;NewEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cardStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mockLLM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;WithSessionService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sharedSessionSvc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;WithArtifactService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sharedArtifactSvc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;instanceC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;NewEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cardStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mockLLM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;WithSessionService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sharedSessionSvc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;WithArtifactService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sharedArtifactSvc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"U_cross_instance_user"&lt;/span&gt;
    &lt;span class="n"&gt;fakeImage&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="m"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0xD8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0xE0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0x00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0x10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sc"&gt;'J'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sc"&gt;'F'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sc"&gt;'I'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sc"&gt;'F'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// Turn 1 (hits Instance A): Upload card photo -&amp;gt; Write to Artifact and pause at HITLConfirmNode&lt;/span&gt;
    &lt;span class="n"&gt;res1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;instanceA&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StartIngest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fakeImage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"Met at Cloud Summit"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="c"&gt;// Verify Session State has no bloated raw image_bytes, but lightweight card_image_artifact="business_card.jpg"&lt;/span&gt;

    &lt;span class="c"&gt;// Turn 2 (hits brand new Instance B): User enters edit command 'Change title to CTO'&lt;/span&gt;
    &lt;span class="c"&gt;// Instance B automatically detects the active_interrupt_id left by Instance A from sharedSessionSvc!&lt;/span&gt;
    &lt;span class="n"&gt;res2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;instanceB&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResumeHITL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Change title to CTO"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// Verify title updated to 'CTO' and re-entered HITL pause waiting for confirmation&lt;/span&gt;

    &lt;span class="c"&gt;// Turn 3 (hits brand new Instance C): User clicks '✅ Confirm Save'&lt;/span&gt;
    &lt;span class="n"&gt;res3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;instanceC&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResumeHITL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"action=confirm_save"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// Verify card successfully written to CardStore, AI icebreaker generated, and HasPendingHITL is zeroed for all instances!&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running &lt;code&gt;go test -race -v ./...&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=== RUN TestCrossInstanceHITLResumeAndArtifactStorage
--- PASS: TestCrossInstanceHITLResumeAndArtifactStorage (0.00s)
=== RUN TestRootAgentDirectImageArtifactIngestAndHITL
--- PASS: TestRootAgentDirectImageArtifactIngestAndHITL (0.00s)
PASS
ok github.com/kkdai/line-card-assistant/internal/workflow   0.642s

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not only was the three-stage relay from &lt;code&gt;instanceA&lt;/code&gt; $\rightarrow$ &lt;code&gt;instanceB&lt;/code&gt; $\rightarrow$ &lt;code&gt;instanceC&lt;/code&gt; seamless, but &lt;code&gt;TestRootAgentDirectImageArtifactIngestAndHITL&lt;/code&gt; in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/app/agent_test.go" rel="noopener noreferrer"&gt;&lt;code&gt;app/agent_test.go&lt;/code&gt;&lt;/a&gt; also verified that when a &lt;code&gt;genai.Part&lt;/code&gt; image is passed directly from ADK WebUI / A2A, &lt;code&gt;RouterNode&lt;/code&gt; automatically saves it to &lt;code&gt;ArtifactService&lt;/code&gt; and pauses at &lt;code&gt;HITLConfirmNode&lt;/code&gt;!&lt;/p&gt;




&lt;h1&gt;
  
  
  4. Summary and Next Steps
&lt;/h1&gt;

&lt;p&gt;Through this practical guide (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/11" rel="noopener noreferrer"&gt;PR #11&lt;/a&gt;, completing &lt;a href="https://github.com/kkdai/line-card-assistant/issues/3" rel="noopener noreferrer"&gt;Issue #3&lt;/a&gt; and &lt;a href="https://github.com/kkdai/line-card-assistant/issues/4" rel="noopener noreferrer"&gt;Issue #4&lt;/a&gt; in the Roadmap), we've solved the two most critical persistence challenges for moving an AI Agent from a "toy prototype" to a "cloud stateless multi-instance production environment":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Separation of Large Files and State (Artifact vs. Session State)&lt;/strong&gt;: Using ADK Go v2.6.0's &lt;strong&gt;&lt;code&gt;artifact/gcsartifact&lt;/code&gt;&lt;/strong&gt; and &lt;code&gt;ctx.Artifacts()&lt;/code&gt;, business card photos no longer bloat &lt;code&gt;Session.State&lt;/code&gt;. Instead, they have independent GCS versioned storage, with Session State only keeping a few dozen bytes of pointers (&lt;code&gt;card_image_artifact&lt;/code&gt; + &lt;code&gt;card_image_version&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stateless Cross-Instance HITL Recovery&lt;/strong&gt;: Combining &lt;strong&gt;&lt;code&gt;session/vertexai&lt;/code&gt;&lt;/strong&gt; (Agent Platform Sessions) and &lt;code&gt;resolvePendingHITL&lt;/code&gt;, even if a container scales to 0 or switches instances during the minutes spent waiting for human confirmation, the system can still resume the "edit field loop" or "confirm save" perfectly.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In the next article, we will continue with cloud database persistence and a two-layer decoupled architecture, and use &lt;strong&gt;&lt;code&gt;agents-cli eval&lt;/code&gt;&lt;/strong&gt; to build an automated evaluation flywheel. Stay tuned!&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources and Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Previous articles in the series&lt;/strong&gt;: 

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/2026/10/16/adk-graph-workflow-go/"&gt;[Agent Platform in Action (1)] Building a LINE AI Business Card King with Go and Google ADK 2.0 Graph Workflow: From Fan-Out Parallel Retrieval to HITL Pause Loop Correction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/2026/10/17/adk-graph-workflow-go-2/"&gt;[Agent Platform in Action (2)] From Monolithic Cloud Run to Google Cloud Agent Runtime: Refactoring LINE AI Business Card King with agents-cli and ADK Go&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open source code and PRs for this project&lt;/strong&gt;: 

&lt;ul&gt;
&lt;li&gt;GitHub Repository: &lt;a href="https://github.com/kkdai/line-card-assistant" rel="noopener noreferrer"&gt;kkdai/line-card-assistant&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Phase 2 Implementation Pull Request: &lt;a href="https://github.com/kkdai/line-card-assistant/pull/11" rel="noopener noreferrer"&gt;PR #11: feat(phase2): integrate GCS Artifact Service and cross-instance HITL session recovery&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Completed Issues: &lt;a href="https://github.com/kkdai/line-card-assistant/issues/3" rel="noopener noreferrer"&gt;Issue #3&lt;/a&gt;, &lt;a href="https://github.com/kkdai/line-card-assistant/issues/4" rel="noopener noreferrer"&gt;Issue #4&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google ADK and Agent Platform Official Documentation&lt;/strong&gt;: 

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://google.github.io/adk-docs/llms.txt" rel="noopener noreferrer"&gt;ADK Documentation (llms.txt)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://google.github.io/agents-cli/guide/getting-started/" rel="noopener noreferrer"&gt;Getting Started - agents-cli&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>architecture</category>
      <category>go</category>
    </item>
    <item>
      <title>[Agent Platform in Practice (4)] Towards Cloud Two-layer Decoupling and Full Persistence: Completing the LINE AI Business Card M</title>
      <dc:creator>Evan Lin</dc:creator>
      <pubDate>Fri, 23 Oct 2026 08:32:27 +0000</pubDate>
      <link>https://dev.to/gde/agent-platform-in-practice-4-towards-cloud-two-layer-decoupling-and-full-persistence-60g</link>
      <guid>https://dev.to/gde/agent-platform-in-practice-4-towards-cloud-two-layer-decoupling-and-full-persistence-60g</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyihnwxfymge8t2hbqs6u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fyihnwxfymge8t2hbqs6u.png" alt="Phase 3: Decoupled LINE Webhook Gateway + Agent Runtime &amp;amp; Cloud SQL Architecture" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Recap
&lt;/h1&gt;

&lt;p&gt;In the first three articles of the "Agent Platform in Action" series, we step-by-step pushed the &lt;strong&gt;LINE AI Business Card Assistant (&lt;code&gt;line-card-assistant&lt;/code&gt;)&lt;/strong&gt; from a conceptual prototype to a cloud-native architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/2026/10/16/adk-graph-workflow-go/"&gt;[Agent Platform in Action (1)] Building a LINE AI Business Card Assistant with Go and Google ADK 2.0 Graph Workflow&lt;/a&gt;&lt;/strong&gt;: We used &lt;code&gt;google.golang.org/adk/v2/workflow&lt;/code&gt; to build a 12-node business card processing graph (Graph Workflow), covering Vision OCR, &lt;code&gt;AddFanOut&lt;/code&gt; / &lt;code&gt;AddFanIn&lt;/code&gt; parallel background investigations, and the HITL (Human-in-the-Loop) pause and modification loop of &lt;code&gt;NewRequestInputEvent&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/2026/10/17/adk-graph-workflow-go-2/"&gt;[Agent Platform in Action (2)] From Monolithic Cloud Run to Google Cloud Agent Runtime: Refactoring LINE AI Business Card Assistant with agents-cli and ADK Go&lt;/a&gt;&lt;/strong&gt;: Completed &lt;strong&gt;Phase 1 (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/10" rel="noopener noreferrer"&gt;PR #10&lt;/a&gt;)&lt;/strong&gt;, introducing the &lt;code&gt;agents-cli&lt;/code&gt; (&lt;code&gt;adk_go&lt;/code&gt;) standard dual-channel architecture, allowing the project to support LINE Webhook, ADK WebUI Playground, and the A2A protocol simultaneously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/2026/10/20/adk-graph-workflow-go-3/"&gt;[Agent Platform in Action (3)] Solving HITL Amnesia in Stateless Containers: Implementing Cross-Instance Business Card Recognition and Pause/Resume with ADK 2.0 GCS Artifact Service and Agent Platform Sessions&lt;/a&gt;&lt;/strong&gt;: Completed the &lt;strong&gt;first half of Phase 2 (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/11" rel="noopener noreferrer"&gt;PR #11&lt;/a&gt;)&lt;/strong&gt;, using &lt;code&gt;artifact/gcsartifact&lt;/code&gt; to move multi-MB business card photos out of &lt;code&gt;Session.State&lt;/code&gt;, and combining &lt;code&gt;session/vertexai&lt;/code&gt; to implement &lt;code&gt;resolvePendingHITL&lt;/code&gt;, allowing stateless containers to seamlessly continue HITL modification and saving even if scaled to 0 or instances are switched.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In today's fourth episode of the series, we will complete the &lt;strong&gt;final piece of Phase 2 (&lt;a href="https://github.com/kkdai/line-card-assistant/issues/5" rel="noopener noreferrer"&gt;Issue #5&lt;/a&gt;)&lt;/strong&gt; and the &lt;strong&gt;Phase 3 core architecture upgrade (&lt;a href="https://github.com/kkdai/line-card-assistant/issues/6" rel="noopener noreferrer"&gt;Issue #6&lt;/a&gt;)&lt;/strong&gt; (corresponding to &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/pull/12" rel="noopener noreferrer"&gt;PR #12&lt;/a&gt;&lt;/strong&gt;):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Business Card Database Persistence (&lt;code&gt;PostgresStore&lt;/code&gt; / Cloud SQL)&lt;/strong&gt;: Officially upgrading the business card library, which was originally stored in local memory or container temporary files, to a relational and JSON hybrid storage supporting &lt;strong&gt;Cloud SQL PostgreSQL&lt;/strong&gt;, and enabling &lt;code&gt;HybridSearch&lt;/code&gt; and token-scoring fault-tolerant &lt;code&gt;BroadSearch&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two-Tier Decoupled Call Architecture (Cloud Run LINE Webhook Gateway $\rightarrow$ Agent Runtime)&lt;/strong&gt;: Completely decoupling the lightweight edge layer (Edge Layer) responsible for "LINE verification and image downloading" from the execution layer (Execution Layer) responsible for "ADK 2.0 Graph Workflow multimodal inference and search"!&lt;/li&gt;
&lt;/ol&gt;




&lt;h1&gt;
  
  
  1. Why Do We Need the "Cloud Persistence Iron Triangle" and "Two-Tier Decoupled Architecture"?
&lt;/h1&gt;

&lt;p&gt;When we deploy &lt;code&gt;line-card-assistant&lt;/code&gt; to a Google Cloud production environment, a careful examination of the responsibilities of every piece of data and component in the system reveals two key problems that must be solved:&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Last Remaining "Local State": &lt;code&gt;CardStore&lt;/code&gt; Business Card Database
&lt;/h2&gt;

&lt;p&gt;In the previous post (PR #11), we already placed the &lt;strong&gt;"original business card photos"&lt;/strong&gt; in &lt;strong&gt;Google Cloud Storage (&lt;code&gt;GCSArtifactService&lt;/code&gt;)&lt;/strong&gt; and the &lt;strong&gt;"workflow execution state and HITL breakpoints"&lt;/strong&gt; in &lt;strong&gt;Vertex AI / Cloud SQL (&lt;code&gt;SessionService&lt;/code&gt;)&lt;/strong&gt;. However, when a user clicks "✅ Confirm Save" in LINE and reaches &lt;code&gt;SaveCardNode&lt;/code&gt;, if the final confirmed business card data (&lt;code&gt;models.BusinessCard&lt;/code&gt;) is still written to local memory &lt;code&gt;MemoryStore&lt;/code&gt; or a local SQLite file inside the container:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cleared on container restart or Scale-to-Zero&lt;/strong&gt;: Once the Agent Runtime instance scales to 0, the hard-earned network business card library will evaporate instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple instances cannot share search results&lt;/strong&gt;: When a user saves a business card on Instance A, and a search request is routed to Instance B a second later, &lt;code&gt;HybridSearchNode&lt;/code&gt; won't find the card just saved in Instance B's memory!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Therefore, we must complete the final corner of the &lt;strong&gt;"Cloud-Native Persistence Triad"&lt;/strong&gt; — &lt;strong&gt;Cloud SQL PostgreSQL (&lt;code&gt;PostgresStore&lt;/code&gt;)&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Persistence Dimension&lt;/th&gt;
&lt;th&gt;Content Responsible for Storing&lt;/th&gt;
&lt;th&gt;Cloud Backend Service&lt;/th&gt;
&lt;th&gt;ADK / Go Implementation Package&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1. Binary Large Files (Artifacts)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Original business card photos (&lt;code&gt;business_card.jpg&lt;/code&gt; v1, v2…)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Google Cloud Storage (GCS)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sessions.NewArtifactService&lt;/code&gt; (&lt;code&gt;artifact/gcsartifact&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2. Conversation and Graph Breakpoints (Sessions)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Graph Checkpoints, &lt;code&gt;active_interrupt_id&lt;/code&gt;, &lt;code&gt;draft_card&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Vertex AI Agent Engine / Cloud SQL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sessions.NewService&lt;/code&gt; (&lt;code&gt;session/vertexai&lt;/code&gt;, &lt;code&gt;session/database&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3. Structured Business Data (Domain DB)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Verified &lt;code&gt;BusinessCard&lt;/code&gt; official records, JSON industry tags, hybrid retrieval&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cloud SQL for PostgreSQL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;store.NewStore&lt;/code&gt; (&lt;code&gt;internal/store/postgres.go&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  2. Why Decouple LINE Webhook Gateway and Agent Runtime into a Two-Tier Architecture?
&lt;/h2&gt;

&lt;p&gt;In monolithic (All-in-One) mode, &lt;code&gt;cmd/server&lt;/code&gt; receives LINE Webhooks, runs Vision OCR, and executes three parallel Grounding search nodes in the same process. However, in an enterprise-grade Google Cloud Agent Platform architecture, splitting the system into &lt;strong&gt;"Cloud Run LINE Webhook Gateway (Edge Layer)"&lt;/strong&gt; and &lt;strong&gt;"Google Cloud Agent Runtime (Agent Execution Layer)"&lt;/strong&gt; offers three major advantages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Isolation of Public Webhook Verification and Internal IAM Permissions&lt;/strong&gt;: 

&lt;ul&gt;
&lt;li&gt;LINE Messaging API requires a public HTTPS Endpoint (&lt;code&gt;POST /webhook&lt;/code&gt;) to push events, uses &lt;code&gt;LINE_CHANNEL_SECRET&lt;/code&gt; to verify the &lt;code&gt;X-Line-Signature&lt;/code&gt;, and must &lt;strong&gt;immediately return &lt;code&gt;200 OK&lt;/code&gt; within 1-2 seconds&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Meanwhile, the actual &lt;strong&gt;Agent Runtime&lt;/strong&gt; only needs internal authorization via &lt;strong&gt;Google Cloud IAM / Application Default Credentials (ADC)&lt;/strong&gt;, without needing exposure to unauthenticated public traffic or holding the LINE Channel Secret.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple Channels Sharing the Same Session-backed Remote Agent&lt;/strong&gt;: 

&lt;ul&gt;
&lt;li&gt;After decoupling, whether a user sends a business card photo from &lt;strong&gt;LINE Bot&lt;/strong&gt;, queries contacts from internal &lt;strong&gt;Gemini Enterprise (A2A)&lt;/strong&gt;, or a developer tests from &lt;strong&gt;ADK WebUI&lt;/strong&gt;, they all call the &lt;strong&gt;same &lt;code&gt;app.NewRootAgent&lt;/code&gt; hosted on the Agent Runtime&lt;/strong&gt;!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extreme Deployment Flexibility (One-click switch between All-in-One vs. Decoupled)&lt;/strong&gt;: 

&lt;ul&gt;
&lt;li&gt;During local development or small projects, if &lt;code&gt;AGENT_RUNTIME_URL&lt;/code&gt; is not set, &lt;code&gt;cmd/server&lt;/code&gt; automatically runs in &lt;strong&gt;All-in-One monolithic mode&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;In production, simply setting &lt;code&gt;AGENT_RUNTIME_URL=https://...&lt;/code&gt; in Cloud Run automatically switches &lt;code&gt;cmd/server&lt;/code&gt; to a &lt;strong&gt;lightweight stateless LINE Webhook Gateway&lt;/strong&gt;!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's look at the cross-layer interaction under the two-tier decoupled architecture with a complete system sequence diagram:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dot"&gt;&lt;code&gt;&lt;span class="nv"&gt;sequenceDiagram&lt;/span&gt;
    &lt;span class="nv"&gt;autonumber&lt;/span&gt;
    &lt;span class="nv"&gt;actor&lt;/span&gt; &lt;span class="nv"&gt;User&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;LINE&lt;/span&gt; &lt;span class="nv"&gt;User&lt;/span&gt;
    &lt;span class="nv"&gt;participant&lt;/span&gt; &lt;span class="nv"&gt;GW&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;Cloud&lt;/span&gt; &lt;span class="nv"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;br/&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;LINE&lt;/span&gt; &lt;span class="nv"&gt;Webhook&lt;/span&gt; &lt;span class="nv"&gt;Gateway&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;br/&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;cmd&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;server&lt;/span&gt; &lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;gateway&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;Client&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;participant&lt;/span&gt; &lt;span class="nv"&gt;AR&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;Google&lt;/span&gt; &lt;span class="nv"&gt;Cloud&lt;/span&gt; &lt;span class="nv"&gt;Agent&lt;/span&gt; &lt;span class="nv"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;br/&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;adkrest&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;Server&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;NewRootAgent&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;participant&lt;/span&gt; &lt;span class="nv"&gt;GCS&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;Cloud&lt;/span&gt; &lt;span class="nv"&gt;Storage&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;br/&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;GCSArtifactService&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;participant&lt;/span&gt; &lt;span class="nv"&gt;Sess&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;Vertex&lt;/span&gt; &lt;span class="nv"&gt;AI&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt; &lt;span class="nv"&gt;Cloud&lt;/span&gt; &lt;span class="nv"&gt;SQL&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;br/&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;SessionService&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;participant&lt;/span&gt; &lt;span class="nv"&gt;PG&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;Cloud&lt;/span&gt; &lt;span class="nv"&gt;SQL&lt;/span&gt; &lt;span class="nv"&gt;PostgreSQL&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;br/&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;PostgresStore&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;

    &lt;span class="nv"&gt;User&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;GW&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;Send&lt;/span&gt; &lt;span class="nv"&gt;business&lt;/span&gt; &lt;span class="nv"&gt;card&lt;/span&gt; &lt;span class="nv"&gt;photo&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;POST&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;webhook&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;GW&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;User&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;Verify&lt;/span&gt; &lt;span class="nv"&gt;X&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;Line&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;Signature&lt;/span&gt; &lt;span class="nv"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;immediately&lt;/span&gt; &lt;span class="nv"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;HTTP&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="nv"&gt;OK&lt;/span&gt;
    &lt;span class="nv"&gt;GW&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;GW&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;Download&lt;/span&gt; &lt;span class="nv"&gt;image&lt;/span&gt; &lt;span class="nv"&gt;from&lt;/span&gt; &lt;span class="nv"&gt;LINE&lt;/span&gt; &lt;span class="nv"&gt;Blob&lt;/span&gt; &lt;span class="nv"&gt;API&lt;/span&gt; &lt;span class="o"&gt;[]&lt;/span&gt;&lt;span class="nv"&gt;byte&lt;/span&gt;
    &lt;span class="nv"&gt;GW&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;AR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;POST&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;api&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;apps&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;users&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;sessions&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&amp;lt;&lt;/span&gt;br/&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;POST&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;api&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;run&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;With&lt;/span&gt; &lt;span class="nv"&gt;Google&lt;/span&gt; &lt;span class="nv"&gt;Cloud&lt;/span&gt; &lt;span class="nv"&gt;ADC&lt;/span&gt; &lt;span class="nv"&gt;ID&lt;/span&gt; &lt;span class="nv"&gt;Token&lt;/span&gt; &lt;span class="nv"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;image&lt;/span&gt; &lt;span class="nv"&gt;InlineData&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;AR&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;GCS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;RouterNode&lt;/span&gt; &lt;span class="nv"&gt;automatically&lt;/span&gt; &lt;span class="nv"&gt;saves&lt;/span&gt; &lt;span class="nv"&gt;image&lt;/span&gt; &lt;span class="nv"&gt;to&lt;/span&gt; &lt;span class="nv"&gt;GCS&lt;/span&gt; &lt;span class="nv"&gt;Artifact&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"business_card.jpg"&lt;/span&gt; &lt;span class="nv"&gt;v1&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;AR&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;AR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;VisionOCRNode&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;AddFanOut&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;Deduplication&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt; &lt;span class="nv"&gt;Google&lt;/span&gt; &lt;span class="nv"&gt;Search&lt;/span&gt; &lt;span class="nv"&gt;company&lt;/span&gt; &lt;span class="nv"&gt;background&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt; &lt;span class="nv"&gt;Conversation&lt;/span&gt; &lt;span class="nv"&gt;context&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;BuildPreviewNode&lt;/span&gt;
    &lt;span class="nv"&gt;AR&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;Sess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;HITLConfirmNode&lt;/span&gt; &lt;span class="nv"&gt;triggers&lt;/span&gt; &lt;span class="nv"&gt;NewRequestInputEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;br/&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;Persist&lt;/span&gt; &lt;span class="n"&gt;active_interrupt_id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"hitl_confirm_1"&lt;/span&gt;
    &lt;span class="nv"&gt;AR&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;GW&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;Return&lt;/span&gt; &lt;span class="nv"&gt;Events&lt;/span&gt; &lt;span class="nv"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;Session&lt;/span&gt; &lt;span class="nv"&gt;State&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;including&lt;/span&gt; &lt;span class="nv"&gt;flex_preview_json&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;GW&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;User&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;Push&lt;/span&gt; &lt;span class="nv"&gt;LINE&lt;/span&gt; &lt;span class="nv"&gt;Flex&lt;/span&gt; &lt;span class="nv"&gt;Message&lt;/span&gt; &lt;span class="nv"&gt;business&lt;/span&gt; &lt;span class="nv"&gt;card&lt;/span&gt; &lt;span class="nv"&gt;preview&lt;/span&gt; &lt;span class="nv"&gt;card&lt;/span&gt; &lt;span class="err"&gt;(⏸️&lt;/span&gt; &lt;span class="nv"&gt;Waiting&lt;/span&gt; &lt;span class="nv"&gt;for&lt;/span&gt; &lt;span class="nv"&gt;user&lt;/span&gt; &lt;span class="nv"&gt;confirmation&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;

    &lt;span class="nv"&gt;Note&lt;/span&gt; &lt;span class="nv"&gt;over&lt;/span&gt; &lt;span class="nv"&gt;GW&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="nv"&gt;AR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;☁️&lt;/span&gt; &lt;span class="nv"&gt;Gateway&lt;/span&gt; &lt;span class="nv"&gt;is&lt;/span&gt; &lt;span class="nv"&gt;completely&lt;/span&gt; &lt;span class="nv"&gt;stateless&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;the&lt;/span&gt; &lt;span class="nv"&gt;next&lt;/span&gt; &lt;span class="nv"&gt;request&lt;/span&gt; &lt;span class="nv"&gt;can&lt;/span&gt; &lt;span class="nv"&gt;land&lt;/span&gt; &lt;span class="nv"&gt;on&lt;/span&gt; &lt;span class="nv"&gt;any&lt;/span&gt; &lt;span class="nv"&gt;Gateway&lt;/span&gt; &lt;span class="nv"&gt;instance&lt;/span&gt;

    &lt;span class="nv"&gt;User&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;GW&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;Input&lt;/span&gt; &lt;span class="nv"&gt;modification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"Change title to CTO"&lt;/span&gt; &lt;span class="nv"&gt;or&lt;/span&gt; &lt;span class="nv"&gt;click&lt;/span&gt; &lt;span class="s2"&gt;"✅ Confirm Save"&lt;/span&gt;
    &lt;span class="nv"&gt;GW&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;AR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;HasPendingHITL&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;GET&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;api&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;apps&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;users&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;br/&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;Automatically&lt;/span&gt; &lt;span class="nv"&gt;retrieve&lt;/span&gt; &lt;span class="nv"&gt;remote&lt;/span&gt; &lt;span class="nv"&gt;paused&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;SessionID&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;active_interrupt_id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;GW&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;AR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;POST&lt;/span&gt; &lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;api&lt;/span&gt;&lt;span class="err"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;run&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;Send&lt;/span&gt; &lt;span class="nv"&gt;adk_request_input&lt;/span&gt; &lt;span class="nv"&gt;FunctionResponse&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
    &lt;span class="nv"&gt;AR&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;PG&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;If&lt;/span&gt; &lt;span class="nv"&gt;confirming&lt;/span&gt; &lt;span class="nv"&gt;save&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;SaveCardNode&lt;/span&gt; &lt;span class="nv"&gt;calls&lt;/span&gt; &lt;span class="nv"&gt;PostgresStore&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;Save&lt;/span&gt;&lt;span class="err"&gt;()&lt;/span&gt; &lt;span class="nv"&gt;Upsert&lt;/span&gt; &lt;span class="nv"&gt;to&lt;/span&gt; &lt;span class="nv"&gt;write&lt;/span&gt; &lt;span class="nv"&gt;to&lt;/span&gt; &lt;span class="nv"&gt;Cloud&lt;/span&gt; &lt;span class="nv"&gt;SQL&lt;/span&gt;
    &lt;span class="nv"&gt;AR&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;Sess&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;Clear&lt;/span&gt; &lt;span class="n"&gt;active_interrupt_id&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="nv"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;record&lt;/span&gt; &lt;span class="nv"&gt;saved_card_id&lt;/span&gt;
    &lt;span class="nv"&gt;AR&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;GW&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;Return&lt;/span&gt; &lt;span class="nv"&gt;save&lt;/span&gt; &lt;span class="nv"&gt;success&lt;/span&gt; &lt;span class="nv"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;AI&lt;/span&gt; &lt;span class="nv"&gt;icebreaker&lt;/span&gt; &lt;span class="nv"&gt;follow&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;up&lt;/span&gt; &lt;span class="nv"&gt;draft&lt;/span&gt;
    &lt;span class="nv"&gt;GW&lt;/span&gt;&lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;User&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;Push&lt;/span&gt; &lt;span class="nv"&gt;save&lt;/span&gt; &lt;span class="nv"&gt;confirmation&lt;/span&gt; &lt;span class="nv"&gt;and&lt;/span&gt; &lt;span class="nv"&gt;icebreaker&lt;/span&gt; &lt;span class="nv"&gt;to&lt;/span&gt; &lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="err"&gt;!&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  2. Implementation Breakdown (1): Building the Cloud SQL PostgreSQL Business Card Library (&lt;code&gt;internal/store/postgres.go&lt;/code&gt;) — Issue #5
&lt;/h1&gt;

&lt;p&gt;In our project, since &lt;code&gt;sessions/sessions.go&lt;/code&gt; already introduced &lt;code&gt;gorm.io/gorm&lt;/code&gt; and &lt;code&gt;gorm.io/driver/postgres&lt;/code&gt; to support &lt;code&gt;SESSION_TYPE=cloud_sql&lt;/code&gt;, we can directly use GORM to build a production-grade &lt;strong&gt;&lt;code&gt;PostgresStore&lt;/code&gt;&lt;/strong&gt; for &lt;code&gt;CardStore&lt;/code&gt; without extra dependency bloat!&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Extending the &lt;code&gt;store.CardStore&lt;/code&gt; Interface: Native &lt;code&gt;BroadSearch&lt;/code&gt; Support for All Backends
&lt;/h2&gt;

&lt;p&gt;While checking the original &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/workflow/nodes_search.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/workflow/nodes_search.go&lt;/code&gt;&lt;/a&gt;, we found a type assertion for &lt;code&gt;*store.MemoryStore&lt;/code&gt; in &lt;code&gt;BroadSearchNode&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// ❌ Old way: Only MemoryStore supports BroadSearch; other Stores fall back to ListByUser&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;memStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;cardStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MemoryStore&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;memStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BroadSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;cards&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cardStore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListByUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To ensure &lt;code&gt;PostgresStore&lt;/code&gt; and &lt;code&gt;MemoryStore&lt;/code&gt; share a consistent hybrid retrieval and fuzzy fallback experience, we officially included &lt;code&gt;BroadSearch&lt;/code&gt; in the &lt;code&gt;CardStore&lt;/code&gt; standard interface in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/store/store.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/store/store.go&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// CardStore defines the persistence and search interface for business cards.&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;CardStore&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BusinessCard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;GetByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BusinessCard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;CheckDuplicate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BusinessCard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;HybridSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SearchFilter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BusinessCard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;BroadSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keywords&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BusinessCard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ListByUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BusinessCard&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, &lt;code&gt;BroadSearchNode&lt;/code&gt; only needs to cleanly call one line: &lt;code&gt;cardStore.BroadSearch(ctx, userID, keywords)&lt;/code&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Designing GORM Schema and Upsert Mechanism (&lt;code&gt;BusinessCardRecord&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/store/postgres.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/store/postgres.go&lt;/code&gt;&lt;/a&gt;, we defined &lt;code&gt;BusinessCardRecord&lt;/code&gt; corresponding to the PostgreSQL &lt;code&gt;business_cards&lt;/code&gt; table, added indexes to common query fields (&lt;code&gt;user_id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;company&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;met_date&lt;/code&gt;, &lt;code&gt;updated_at&lt;/code&gt;), and stored &lt;code&gt;Tags []string&lt;/code&gt; as a JSON string in &lt;code&gt;tags_json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// BusinessCardRecord is the GORM database model for persisting a models.BusinessCard&lt;/span&gt;
&lt;span class="c"&gt;// in PostgreSQL / Cloud SQL.&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;BusinessCardRecord&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`gorm:"primaryKey;size:128"`&lt;/span&gt;
    &lt;span class="n"&gt;UserID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`gorm:"index;size:128;not null"`&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`gorm:"index;size:255"`&lt;/span&gt;
    &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`gorm:"size:255"`&lt;/span&gt;
    &lt;span class="n"&gt;Company&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`gorm:"index;size:255"`&lt;/span&gt;
    &lt;span class="n"&gt;Email&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`gorm:"index;size:255"`&lt;/span&gt;
    &lt;span class="n"&gt;Phone&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`gorm:"size:64"`&lt;/span&gt;
    &lt;span class="n"&gt;Address&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`gorm:"type:text"`&lt;/span&gt;
    &lt;span class="n"&gt;TaxID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`gorm:"size:32"`&lt;/span&gt;
    &lt;span class="n"&gt;Website&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`gorm:"size:512"`&lt;/span&gt;
    &lt;span class="n"&gt;RawOCR&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`gorm:"type:text"`&lt;/span&gt;
    &lt;span class="n"&gt;CompanySummary&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`gorm:"type:text"`&lt;/span&gt;
    &lt;span class="n"&gt;CompanyInfo&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`gorm:"type:text"`&lt;/span&gt;
    &lt;span class="n"&gt;TagsJSON&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`gorm:"column:tags_json;type:text"`&lt;/span&gt;
    &lt;span class="n"&gt;MetContext&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`gorm:"type:text"`&lt;/span&gt;
    &lt;span class="n"&gt;RawImageURL&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="s"&gt;`gorm:"type:text"`&lt;/span&gt;
    &lt;span class="n"&gt;MetDate&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="s"&gt;`gorm:"index"`&lt;/span&gt;
    &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="s"&gt;`gorm:"index"`&lt;/span&gt;
    &lt;span class="n"&gt;UpdatedAt&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="s"&gt;`gorm:"index"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a user takes a photo of an existing contact's business card (e.g., the person was promoted from "Senior Engineer" to "VP of Engineering", and &lt;code&gt;CleanAndDedupNode&lt;/code&gt; identifies the duplicate and reuses the original &lt;code&gt;card.ID&lt;/code&gt;), &lt;code&gt;PostgresStore.Save&lt;/code&gt; performs an atomic Upsert using PostgreSQL's native &lt;code&gt;ON CONFLICT (id) DO UPDATE&lt;/code&gt; (&lt;code&gt;clause.OnConflict{UpdateAll: true}&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Clauses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clause&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OnConflict&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Columns&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;clause&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Column&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;UpdateAll&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"upsert business card %q: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. The &lt;code&gt;store.NewStore(ctx)&lt;/code&gt; Factory: Auto-sensing Cloud SQL Unix Sockets and Environment Variables
&lt;/h2&gt;

&lt;p&gt;When connecting to Cloud SQL from Cloud Run or Agent Runtime, the official recommendation is to use the Unix Domain Socket (&lt;code&gt;/cloudsql/&amp;lt;PROJECT&amp;gt;:&amp;lt;REGION&amp;gt;:&amp;lt;INSTANCE&amp;gt;&lt;/code&gt;) established by the Cloud SQL Auth Proxy; locally or on GKE, &lt;code&gt;DATABASE_URL&lt;/code&gt; or &lt;code&gt;DB_HOST&lt;/code&gt; are common. We support all three modes in &lt;code&gt;BuildPostgresDSNFromEnv()&lt;/code&gt; and &lt;code&gt;store.NewStore(ctx)&lt;/code&gt;, and &lt;strong&gt;automatically fall back seamlessly to &lt;code&gt;MemoryStore&lt;/code&gt; preloaded with 4 Taiwan sample business cards&lt;/strong&gt; when no database environment variables are set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// BuildPostgresDSNFromEnv resolves a PostgreSQL / Cloud SQL connection string from environment variables.&lt;/span&gt;
&lt;span class="c"&gt;// Priority:&lt;/span&gt;
&lt;span class="c"&gt;// 1. CARD_STORE_DSN / DATABASE_URL / POSTGRES_DSN&lt;/span&gt;
&lt;span class="c"&gt;// 2. Cloud SQL Unix socket when INSTANCE_CONNECTION_NAME + DB_PASS are set&lt;/span&gt;
&lt;span class="c"&gt;// 3. Standard TCP host when DB_HOST + DB_PASS are set&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;BuildPostgresDSNFromEnv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"CARD_STORE_DSN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"DATABASE_URL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"POSTGRES_DSN"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;dbUser&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;envOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DB_USER"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"postgres"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;dbName&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;envOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DB_NAME"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"postgres"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;dbPass&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DB_PASS"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;instanceConn&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"INSTANCE_CONNECTION_NAME"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;instanceConn&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;dbPass&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"host=/cloudsql/%s user=%s password=%s dbname=%s sslmode=disable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;instanceConn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dbUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dbPass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dbName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, as long as your environment has &lt;code&gt;INSTANCE_CONNECTION_NAME&lt;/code&gt; and &lt;code&gt;DB_PASS&lt;/code&gt; set for &lt;code&gt;SESSION_TYPE=cloud_sql&lt;/code&gt;, &lt;strong&gt;&lt;code&gt;SessionService&lt;/code&gt; and &lt;code&gt;CardStore&lt;/code&gt; can share the same Cloud SQL PostgreSQL instance&lt;/strong&gt;, without spending an extra penny on database costs!&lt;/p&gt;




&lt;h1&gt;
  
  
  3. Implementation Breakdown (2): Building a Stateless LINE Webhook Gateway and Remote &lt;code&gt;AgentRuntimeClient&lt;/code&gt; (&lt;code&gt;internal/gateway/client.go&lt;/code&gt;) — Issue #6
&lt;/h1&gt;

&lt;p&gt;Next is the core engineering of Phase 3: &lt;strong&gt;How to make the &lt;code&gt;cmd/server&lt;/code&gt; LINE Bot Webhook Handler forward all business card recognition, HITL recovery, search, and icebreaker requests to a remote Agent Runtime via HTTP instead of running &lt;code&gt;workflow.Engine&lt;/code&gt; locally?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Remember how we had the foresight to separate the Webhook Handler and Workflow Engine with a &lt;strong&gt;&lt;code&gt;linebot.WorkflowRunner&lt;/code&gt; interface&lt;/strong&gt; in the first version of &lt;code&gt;internal/linebot/handler.go&lt;/code&gt;?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// internal/linebot/handler.go&lt;/span&gt;
&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;WorkflowRunner&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;HasPendingHITL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="n"&gt;RecordUserMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;StartIngest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;imageBytes&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recentMessages&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;WorkflowResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ResumeHITL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userReply&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;WorkflowResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;RunSearch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;WorkflowResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;RunEmailDraft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cardID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;WorkflowResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because of this clean interface, we &lt;strong&gt;don't need to change a single line of code&lt;/strong&gt; in &lt;code&gt;linebot.Handler&lt;/code&gt;! We just need to implement a &lt;strong&gt;&lt;code&gt;gateway.Client&lt;/code&gt;&lt;/strong&gt; that satisfies the &lt;code&gt;linebot.WorkflowRunner&lt;/code&gt; interface in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/gateway/client.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/gateway/client.go&lt;/code&gt;&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;gateway.NewClient&lt;/code&gt; Supporting Automatic Google Cloud ADC Token Issuance
&lt;/h2&gt;

&lt;p&gt;When the LINE Webhook Gateway on Cloud Run needs to call an IAM-protected remote Agent Runtime (or another ADK REST Server on an internal Cloud Run), &lt;code&gt;gateway.NewClient&lt;/code&gt; automatically obtains the current Service Account's Google Cloud ADC credentials via &lt;code&gt;google.golang.org/api/idtoken&lt;/code&gt; (or &lt;code&gt;golang.org/x/oauth2/google&lt;/code&gt;) and attaches them to the &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; header of every HTTP request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;baseURL&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;useADC&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="n"&gt;ClientOption&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;trimmed&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimRight&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;baseURL&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;trimmed&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"agent runtime baseURL cannot be empty"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasSuffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/api"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;trimmed&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s"&gt;"/api"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;hc&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;useADC&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;idClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;idtoken&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSuffix&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trimmed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/api"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;hc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idClient&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;oauthClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;google&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"https://www.googleapis.com/auth/cloud-platform"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;hc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;oauthClient&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Cross-Layer Image Forwarding: Gateway Downloads Image $\rightarrow$ Agent Runtime Automatically Writes to GCS Artifact
&lt;/h2&gt;

&lt;p&gt;When a user uploads a business card photo on LINE:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Gateway downloads &lt;code&gt;imageBytes&lt;/code&gt; from LINE Blob API.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gateway.Client.StartIngest&lt;/code&gt; calls the remote ADK REST API &lt;code&gt;POST /api/apps/{appName}/users/{userID}/sessions/{sessionID}&lt;/code&gt; to create a session.&lt;/li&gt;
&lt;li&gt;Then it calls &lt;code&gt;POST /api/run&lt;/code&gt;, wrapping &lt;code&gt;imageBytes&lt;/code&gt; in the &lt;code&gt;InlineData&lt;/code&gt; of &lt;code&gt;newMessage.Parts&lt;/code&gt; to send to the remote Agent Runtime:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;StartIngest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;imageBytes&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recentMessages&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;linebot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WorkflowResponse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;sessionID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="s"&gt;"ingest-"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;initialState&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StateKeyInputType&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RouteIngest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StateKeyUserID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StateKeyRecentMessages&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;recentMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;createSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sessionID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Part&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RouteIngest&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;imageBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;parts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Part&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;InlineData&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Blob&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;MIMEType&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"image/jpeg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;imageBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Role&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RoleUser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Parts&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;runTurn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sessionID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point, the logic we wrote for &lt;code&gt;RouterNode&lt;/code&gt; in Phase 2 works like magic: when the remote Agent Runtime's &lt;code&gt;RouterNode&lt;/code&gt; receives this request with &lt;code&gt;InlineData&lt;/code&gt;, it &lt;strong&gt;automatically calls &lt;code&gt;ctx.Artifacts().Save(ctx, "business_card.jpg", ...)&lt;/code&gt; to store the image in the GCS Artifact Bucket&lt;/strong&gt;, leaving only lightweight &lt;code&gt;card_image_artifact&lt;/code&gt; and &lt;code&gt;card_image_version&lt;/code&gt; in &lt;code&gt;Session.State&lt;/code&gt;! This means the &lt;strong&gt;LINE Webhook Gateway doesn't even need direct write permissions to the GCS Bucket&lt;/strong&gt; for the remote Agent Runtime to complete versioned GCS Artifact archiving!&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Cross-Layer Stateless HITL Breakpoint Discovery and Recovery (&lt;code&gt;resolvePendingHITL&lt;/code&gt; + &lt;code&gt;ResumeHITL&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;The most exciting part: when a user sees the LINE Flex preview card and inputs "&lt;em&gt;Change title to CTO&lt;/em&gt;" 5 minutes later, and this Webhook request lands on &lt;strong&gt;another completely new Cloud Run Gateway instance&lt;/strong&gt;, how does &lt;code&gt;gateway.Client&lt;/code&gt; know this user has a pending HITL Session on the remote Agent Runtime?&lt;/p&gt;

&lt;p&gt;We use the &lt;code&gt;GET /api/apps/{appName}/users/{userID}/sessions&lt;/code&gt; endpoint via the ADK REST API in &lt;code&gt;gateway.Client.resolvePendingHITL&lt;/code&gt; to query the remote Agent Runtime for the user's session status and check &lt;code&gt;state["active_interrupt_id"]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;resolvePendingHITL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pendingHITL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// 1. If local cache has SessionID, call remote GET /sessions/{id} to get the latest active_interrupt_id&lt;/span&gt;
    &lt;span class="c"&gt;// (Important! Because after each Edit loop, hitl_seq increments to hitl_confirm_2, hitl_confirm_3...)&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RLock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hasCached&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pendingSession&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RUnlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hasCached&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SessionID&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fullSess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SessionID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;fullSess&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;activeID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;readStringFromMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fullSess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StateKeyActiveInterruptID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;activeID&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;pendingHITL&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SessionID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SessionID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;InterruptID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;activeID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pendingSession&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;
                &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c"&gt;// 2. If this is a brand-new stateless Gateway instance, call remote GET /sessions to find sessions with active_interrupt_id&lt;/span&gt;
    &lt;span class="n"&gt;sessions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;listSessions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pendingHITL&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sess&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;sessions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;activeID&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;readStringFromMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StateKeyActiveInterruptID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;activeID&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;sess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fullSess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;fullSess&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;activeID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;readStringFromMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fullSess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;State&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StateKeyActiveInterruptID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;activeID&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;found&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;pendingHITL&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SessionID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;InterruptID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;activeID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pendingSession&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;
            &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pendingHITL&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="no"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is a real bug and key detail we caught while writing integration tests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When the user uploads a business card in Turn 1, the first breakpoint ID generated by &lt;code&gt;HITLConfirmNode&lt;/code&gt; is &lt;strong&gt;&lt;code&gt;hitl_confirm_1&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;When the user inputs "&lt;em&gt;Change title to CTO&lt;/em&gt;" in Turn 2 (possibly handled by Gateway Instance 2), &lt;code&gt;ApplyEditNode&lt;/code&gt; modifies the title and loops back to &lt;code&gt;HITLConfirmNode&lt;/code&gt; to pause again, incrementing the breakpoint ID to &lt;strong&gt;&lt;code&gt;hitl_confirm_2&lt;/code&gt;&lt;/strong&gt;!&lt;/li&gt;
&lt;li&gt;If Turn 3 (user clicks "✅ Confirm Save") happens to go back to Gateway Instance 1, and Instance 1 blindly trusts its old local cache (&lt;code&gt;hitl_confirm_1&lt;/code&gt;), the sent &lt;code&gt;FunctionResponse&lt;/code&gt; won't match the latest breakpoint &lt;code&gt;hitl_confirm_2&lt;/code&gt;!&lt;/li&gt;
&lt;li&gt;Therefore, in &lt;code&gt;resolvePendingHITL&lt;/code&gt;, we ensure that &lt;strong&gt;even if the local cache hits a &lt;code&gt;SessionID&lt;/code&gt;, it synchronizes the latest &lt;code&gt;active_interrupt_id&lt;/code&gt; from the remote via &lt;code&gt;getSession&lt;/code&gt;&lt;/strong&gt;. This way, no matter how many times the request jumps between Gateway instances or how many fields are modified, it always aligns perfectly with the latest &lt;code&gt;InterruptID&lt;/code&gt;!&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;cmd/server/main.go&lt;/code&gt;: Seamless Switching Between Monolithic and Two-Tier Gateway Modes
&lt;/h2&gt;

&lt;p&gt;Finally, we added mode detection in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/cmd/server/main.go" rel="noopener noreferrer"&gt;&lt;code&gt;cmd/server/main.go&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt; &lt;span class="n"&gt;linebot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WorkflowRunner&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AgentRuntimeURL&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// 🌐 Mode A: Decoupled Two-Tier LINE Webhook Gateway Mode (Forwarding to remote Agent Runtime)&lt;/span&gt;
        &lt;span class="n"&gt;gwClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AgentRuntimeURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AgentRuntimeUseADC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithAppName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AgentRuntimeAppName&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to initialize remote Agent Runtime gateway client"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AgentRuntimeURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"err"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"running in decoupled LINE Webhook Gateway mode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"agent_runtime_url"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AgentRuntimeURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"agent_runtime_app"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AgentRuntimeAppName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"use_adc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AgentRuntimeUseADC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;runner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gwClient&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// 🚀 Mode B: All-in-One Local Engine Mode (Default for local development and single container deployment)&lt;/span&gt;
        &lt;span class="n"&gt;cardStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c"&gt;// ... Initialize local workflow.NewEngine ...&lt;/span&gt;
        &lt;span class="n"&gt;runner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;engine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LinebotRunner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;botHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;linebot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  4. End-to-End Two-Tier Architecture Integration Testing (&lt;code&gt;internal/gateway/client_test.go&lt;/code&gt;)
&lt;/h1&gt;

&lt;p&gt;To ensure &lt;code&gt;gateway.Client&lt;/code&gt; is 100% compatible with the official ADK Go v2.6.0 &lt;code&gt;server/adkrest&lt;/code&gt; REST server, we started a real &lt;strong&gt;&lt;code&gt;adkrest.NewServer&lt;/code&gt;&lt;/strong&gt; (mounting our &lt;code&gt;app.NewRootAgentWithDeps&lt;/code&gt;) using &lt;code&gt;httptest.NewServer&lt;/code&gt; in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/gateway/client_test.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/gateway/client_test.go&lt;/code&gt;&lt;/a&gt; and established two completely independent Gateway Clients (&lt;code&gt;client1&lt;/code&gt; and &lt;code&gt;client2&lt;/code&gt;) for a five-stage end-to-end test:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Turn 1 (&lt;code&gt;client1.StartIngest&lt;/code&gt;)&lt;/strong&gt;: Upload business card photo via HTTP $\rightarrow$ Verify remote &lt;code&gt;ArtifactService&lt;/code&gt; successfully writes &lt;code&gt;business_card.jpg&lt;/code&gt; $\rightarrow$ Verify return of &lt;code&gt;WaitingHITL=true&lt;/code&gt; (&lt;code&gt;hitl_confirm_1&lt;/code&gt;) and LINE Flex Preview JSON containing "Wang Daming".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-Instance State Probing (&lt;code&gt;client2.HasPendingHITL&lt;/code&gt;)&lt;/strong&gt;: The brand-new stateless &lt;code&gt;client2&lt;/code&gt; probes the paused session created by &lt;code&gt;client1&lt;/code&gt; via &lt;code&gt;GET /api/apps/app/users/{user}/sessions&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn 2 (&lt;code&gt;client2.ResumeHITL&lt;/code&gt; — Natural Language Modification)&lt;/strong&gt;: &lt;code&gt;client2&lt;/code&gt; sends "&lt;em&gt;Change title to CTO&lt;/em&gt;" $\rightarrow$ Verify remote execution of &lt;code&gt;ApplyEditNode&lt;/code&gt;, updating &lt;code&gt;DraftCard.Title == "CTO"&lt;/code&gt;, and pausing again at &lt;code&gt;hitl_confirm_2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn 3 (&lt;code&gt;client1.ResumeHITL&lt;/code&gt; — Confirm Save)&lt;/strong&gt;: Switch back to &lt;code&gt;client1&lt;/code&gt; and send &lt;code&gt;"action=confirm_save"&lt;/code&gt; $\rightarrow$ &lt;code&gt;client1&lt;/code&gt; automatically synchronizes the latest breakpoint &lt;code&gt;hitl_confirm_2&lt;/code&gt; and wakes up &lt;code&gt;SaveCardNode&lt;/code&gt; $\rightarrow$ Verify business card successfully written to remote &lt;code&gt;CardStore&lt;/code&gt; and returns AI icebreaker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn 4 &amp;amp; Turn 5 (&lt;code&gt;client2.RunSearch&lt;/code&gt; &amp;amp; &lt;code&gt;client2.RunEmailDraft&lt;/code&gt;)&lt;/strong&gt;: Call remote hybrid search and individual icebreaker drafting via HTTP, verifying return of Flex Carousel JSON and icebreaker content.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Running the full test suite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;go &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;-race&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; ./...
&lt;span class="o"&gt;===&lt;/span&gt; RUN TestGatewayClient_EndToEndDecoupledIngestHITLAndSearch
&lt;span class="nt"&gt;---&lt;/span&gt; PASS: TestGatewayClient_EndToEndDecoupledIngestHITLAndSearch &lt;span class="o"&gt;(&lt;/span&gt;0.01s&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;===&lt;/span&gt; RUN TestBusinessCardRecordRoundTrip
&lt;span class="nt"&gt;---&lt;/span&gt; PASS: TestBusinessCardRecordRoundTrip &lt;span class="o"&gt;(&lt;/span&gt;0.00s&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;===&lt;/span&gt; RUN TestBuildPostgresDSNFromEnv
&lt;span class="nt"&gt;---&lt;/span&gt; PASS: TestBuildPostgresDSNFromEnv &lt;span class="o"&gt;(&lt;/span&gt;0.00s&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;===&lt;/span&gt; RUN TestNewStoreFactoryFallback
&lt;span class="nt"&gt;---&lt;/span&gt; PASS: TestNewStoreFactoryFallback &lt;span class="o"&gt;(&lt;/span&gt;0.00s&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;===&lt;/span&gt; RUN TestRankCardsByKeywords
&lt;span class="nt"&gt;---&lt;/span&gt; PASS: TestRankCardsByKeywords &lt;span class="o"&gt;(&lt;/span&gt;0.00s&lt;span class="o"&gt;)&lt;/span&gt;
PASS

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All unit tests and cross-layer integration tests passed with green lights under &lt;code&gt;-race&lt;/code&gt; detector!&lt;/p&gt;




&lt;h1&gt;
  
  
  5. Summary and Roadmap Progress Check
&lt;/h1&gt;

&lt;p&gt;After completing &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/pull/12" rel="noopener noreferrer"&gt;PR #12&lt;/a&gt;&lt;/strong&gt;, looking back at the five stages of &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/issues/9" rel="noopener noreferrer"&gt;Roadmap Issue #9&lt;/a&gt;&lt;/strong&gt;, &lt;strong&gt;Phase 1, Phase 2, and Phase 3 are now 100% complete&lt;/strong&gt;!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phase 1 (&lt;code&gt;agents-cli&lt;/code&gt; &amp;amp; &lt;code&gt;adk_go&lt;/code&gt; Scaffolding)&lt;/strong&gt;: Completed Issues &lt;a href="https://github.com/kkdai/line-card-assistant/issues/1" rel="noopener noreferrer"&gt;#1&lt;/a&gt;, &lt;a href="https://github.com/kkdai/line-card-assistant/issues/2" rel="noopener noreferrer"&gt;#2&lt;/a&gt; (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/10" rel="noopener noreferrer"&gt;PR #10&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 2 (Agent Runtime Adaptation and Cloud Persistence Iron Triangle)&lt;/strong&gt;: Completed Issues &lt;a href="https://github.com/kkdai/line-card-assistant/issues/3" rel="noopener noreferrer"&gt;#3&lt;/a&gt;, &lt;a href="https://github.com/kkdai/line-card-assistant/issues/4" rel="noopener noreferrer"&gt;#4&lt;/a&gt; (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/11" rel="noopener noreferrer"&gt;PR #11&lt;/a&gt;) and Issue &lt;a href="https://github.com/kkdai/line-card-assistant/issues/5" rel="noopener noreferrer"&gt;#5&lt;/a&gt; (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/12" rel="noopener noreferrer"&gt;PR #12&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 3 (LINE Webhook Gateway and Agent Runtime Two-Tier Decoupled Architecture)&lt;/strong&gt;: Completed Issue &lt;a href="https://github.com/kkdai/line-card-assistant/issues/6" rel="noopener noreferrer"&gt;#6&lt;/a&gt; (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/12" rel="noopener noreferrer"&gt;PR #12&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 4 (&lt;code&gt;agents-cli eval&lt;/code&gt; Automated Evaluation and Benchmarking)&lt;/strong&gt;: Issue &lt;a href="https://github.com/kkdai/line-card-assistant/issues/7" rel="noopener noreferrer"&gt;#7&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 5 (Agent Runtime Official Deployment, Terraform IaC, and Cloud Trace Observability)&lt;/strong&gt;: Issue &lt;a href="https://github.com/kkdai/line-card-assistant/issues/8" rel="noopener noreferrer"&gt;#8&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the upcoming articles of this series, we will enter &lt;strong&gt;Phase 4 and Phase 5&lt;/strong&gt;, using &lt;code&gt;agents-cli eval&lt;/code&gt; to establish a Quality Flywheel for this 12-node Graph Workflow, and officially deploy the entire system using &lt;code&gt;agents-cli deploy&lt;/code&gt; and OpenTelemetry. Stay tuned!&lt;/p&gt;




&lt;h2&gt;
  
  
  Resources and Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"Agent Platform in Action" Series Articles&lt;/strong&gt;: 

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/2026/10/16/adk-graph-workflow-go/"&gt;[Agent Platform in Action (1)] Building a LINE AI Business Card Assistant with Go and Google ADK 2.0 Graph Workflow: From Fan-Out Parallel Search to HITL Pause Loop Correction&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/2026/10/17/adk-graph-workflow-go-2/"&gt;[Agent Platform in Action (2)] From Monolithic Cloud Run to Google Cloud Agent Runtime: Refactoring LINE AI Business Card Assistant with agents-cli and ADK Go&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/2026/10/20/adk-graph-workflow-go-3/"&gt;[Agent Platform in Action (3)] Solving HITL Amnesia in Stateless Containers: Implementing Cross-Instance Business Card Recognition and Pause/Resume with ADK 2.0 GCS Artifact Service and Agent Platform Sessions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project Open Source Code and PRs&lt;/strong&gt;: 

&lt;ul&gt;
&lt;li&gt;GitHub Repository: &lt;a href="https://github.com/kkdai/line-card-assistant" rel="noopener noreferrer"&gt;kkdai/line-card-assistant&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Phase 2 &amp;amp; 3 Implementation Pull Request: &lt;a href="https://github.com/kkdai/line-card-assistant/pull/12" rel="noopener noreferrer"&gt;PR #12: feat(phase2-3): Cloud SQL PostgresStore (#5) &amp;amp; Decoupled LINE Webhook Gateway (#6)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Completed Issues: &lt;a href="https://github.com/kkdai/line-card-assistant/issues/5" rel="noopener noreferrer"&gt;Issue #5&lt;/a&gt;, &lt;a href="https://github.com/kkdai/line-card-assistant/issues/6" rel="noopener noreferrer"&gt;Issue #6&lt;/a&gt;, &lt;a href="https://github.com/kkdai/line-card-assistant/issues/9" rel="noopener noreferrer"&gt;Roadmap #9&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google ADK and Agent Platform Official Documentation&lt;/strong&gt;: 

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://google.github.io/adk-docs/llms.txt" rel="noopener noreferrer"&gt;ADK Documentation (llms.txt)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://google.github.io/agents-cli/guide/getting-started/" rel="noopener noreferrer"&gt;Getting Started - agents-cli&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>architecture</category>
      <category>cloud</category>
      <category>go</category>
    </item>
    <item>
      <title>[Agent Platform in Practice (5)] Building an AI Agent Quality Flywheel: Automated Benchmarking for LINE AI Business Card King wi</title>
      <dc:creator>Evan Lin</dc:creator>
      <pubDate>Fri, 23 Oct 2026 08:31:26 +0000</pubDate>
      <link>https://dev.to/gde/agent-platform-in-practice-5-building-an-ai-agent-quality-flywheel-automated-benchmarking-for-58m2</link>
      <guid>https://dev.to/gde/agent-platform-in-practice-5-building-an-ai-agent-quality-flywheel-automated-benchmarking-for-58m2</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpmk7a6rnruefyl2ygnzm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpmk7a6rnruefyl2ygnzm.png" alt="Phase 4: ADK 2.0 Go Agent Evaluation &amp;amp; Quality Flywheel ( raw `agents-cli eval` endraw )" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Recap
&lt;/h1&gt;

&lt;p&gt;In the first four articles of the "Agent Platform in Action" series, we have step-by-step advanced the &lt;strong&gt;LINE AI Business Card Master (&lt;code&gt;line-card-assistant&lt;/code&gt;)&lt;/strong&gt; from a standalone prototype to a cloud-native, dual-layer decoupled &lt;strong&gt;Google Cloud Agent Platform&lt;/strong&gt; architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/2026/10/16/adk-graph-workflow-go/"&gt;[Agent Platform in Action (1)] Building LINE AI Business Card Master with Go and Google ADK 2.0 Graph Workflow&lt;/a&gt;&lt;/strong&gt;: Used &lt;code&gt;google.golang.org/adk/v2/workflow&lt;/code&gt; to build a 12-node business card processing graph (Graph Workflow), covering Vision OCR, &lt;code&gt;AddFanOut&lt;/code&gt; / &lt;code&gt;AddFanIn&lt;/code&gt; parallel background investigations, and the HITL (Human-in-the-Loop) pause and modification loop via &lt;code&gt;NewRequestInputEvent&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/2026/10/17/adk-graph-workflow-go-2/"&gt;[Agent Platform in Action (2)] From Monolithic Cloud Run to Google Cloud Agent Runtime: Refactoring LINE AI Business Card Master with agents-cli and ADK Go&lt;/a&gt;&lt;/strong&gt;: Completed &lt;strong&gt;Phase 1 (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/10" rel="noopener noreferrer"&gt;PR #10&lt;/a&gt;)&lt;/strong&gt;, introducing the &lt;code&gt;agents-cli&lt;/code&gt; (&lt;code&gt;adk_go&lt;/code&gt;) standard dual-channel architecture, supporting LINE Webhook, ADK WebUI Playground, and the A2A protocol.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/2026/10/20/adk-graph-workflow-go-3/"&gt;[Agent Platform in Action (3)] Solving HITL Amnesia in Stateless Containers: Implementing Cross-Instance Recognition and Pause/Resume with ADK 2.0 GCS Artifact Service and Agent Platform Sessions&lt;/a&gt;&lt;/strong&gt;: Completed the &lt;strong&gt;first half of Phase 2 (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/11" rel="noopener noreferrer"&gt;PR #11&lt;/a&gt;)&lt;/strong&gt;, using &lt;code&gt;artifact/gcsartifact&lt;/code&gt; and &lt;code&gt;session/vertexai&lt;/code&gt; to enable cross-instance HITL pause and resume for stateless containers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://dev.to/2026/10/21/adk-graph-workflow-go-4/"&gt;[Agent Platform in Action (4)] Towards Cloud Dual-Layer Decoupling and Full Persistence: Completing the Architecture with Cloud Run LINE Webhook Gateway, Agent Runtime, and Cloud SQL PostgreSQL&lt;/a&gt;&lt;/strong&gt;: Completed the &lt;strong&gt;second half of Phase 2 (&lt;a href="https://github.com/kkdai/line-card-assistant/issues/5" rel="noopener noreferrer"&gt;Issue #5&lt;/a&gt;)&lt;/strong&gt; and &lt;strong&gt;Phase 3 (&lt;a href="https://github.com/kkdai/line-card-assistant/issues/6" rel="noopener noreferrer"&gt;Issue #6&lt;/a&gt;, &lt;a href="https://github.com/kkdai/line-card-assistant/pull/12" rel="noopener noreferrer"&gt;PR #12&lt;/a&gt;)&lt;/strong&gt;, migrating the business card database to Cloud SQL PostgreSQL (&lt;code&gt;PostgresStore&lt;/code&gt;) and implementing the stateless Cloud Run LINE Webhook Gateway (&lt;code&gt;gateway.Client&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the architecture and features in place and ready to officially deploy the Agent to the cloud production environment (Phase 5), every AI engineering team faces a core soul-searching question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"When we modify a line of Prompt, adjust a Graph Node's routing condition, or upgrade the underlying model from &lt;code&gt;gemini-2.5-flash&lt;/code&gt; to &lt;code&gt;gemini-3.8-flash&lt;/code&gt;, how do we ensure that business card OCR fields aren't missed, duplicate card promotion detection doesn't fail, HITL modification loops don't get stuck, and hybrid search doesn't degrade?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In today's fifth episode of the series, we are going to implement &lt;strong&gt;Phase 4 (&lt;a href="https://github.com/kkdai/line-card-assistant/issues/7" rel="noopener noreferrer"&gt;Issue #7&lt;/a&gt;, corresponding to &lt;a href="https://github.com/kkdai/line-card-assistant/pull/13" rel="noopener noreferrer"&gt;PR #13&lt;/a&gt;)&lt;/strong&gt;: using &lt;strong&gt;&lt;code&gt;agents-cli eval&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;ADK 2.0 Go trajectory testing&lt;/strong&gt; to build a quantifiable, comparable, and CI/CD-automatable &lt;strong&gt;AI Agent Quality Flywheel&lt;/strong&gt; for the LINE AI Business Card Master!&lt;/p&gt;




&lt;h1&gt;
  
  
  I. Why Traditional Unit Tests Aren't Enough? Understanding the &lt;code&gt;agents-cli eval&lt;/code&gt; "Quality Flywheel"
&lt;/h1&gt;

&lt;p&gt;In traditional software engineering, we are used to writing deterministic unit tests like &lt;code&gt;if got != want&lt;/code&gt;. However, in an AI Agent system driven by &lt;strong&gt;ADK 2.0 Graph Workflow + Multimodal LLM&lt;/strong&gt;, what we need to verify is far more than just the return value of a single function; it is the entire &lt;strong&gt;"Execution Trajectory and State Evolution (State Delta)"&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Multi-branch and Parallel Convergence (Fan-Out / Fan-In Trajectory)&lt;/strong&gt;: When a business card image is received, does the &lt;code&gt;RouterNode&lt;/code&gt; correctly enter the &lt;code&gt;VisionOCRNode&lt;/code&gt;? Subsequently, are the three parallel branches &lt;code&gt;CleanAndDedupNode&lt;/code&gt;, &lt;code&gt;CompanyEnrichNode&lt;/code&gt;, and &lt;code&gt;ContextMatchNode&lt;/code&gt; triggered simultaneously and converged at &lt;code&gt;JoinPreviewNode&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intermediate State Changes (Session &lt;code&gt;state_delta&lt;/code&gt;)&lt;/strong&gt;: Even if the final return to the user is a LINE Flex Message JSON, did the intermediate nodes correctly normalize the Taiwan mobile number (&lt;code&gt;+886-912-345-678&lt;/code&gt;) and detect an existing contact's promotion (&lt;code&gt;IsDuplicate=true&lt;/code&gt;, &lt;code&gt;VP of Engineering&lt;/code&gt; $\rightarrow$ &lt;code&gt;Chief Technology Officer (CTO)&lt;/code&gt;)?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human-in-the-Loop (HITL) Pause and Multi-turn Dialogue Loop&lt;/strong&gt;: After the &lt;code&gt;HITLConfirmNode&lt;/code&gt; issues an &lt;code&gt;adk_request_input&lt;/code&gt; pause event, if the user replies "Change title to Technical VP, change occasion to COMPUTEX 2026 AI Forum" and confirms the save, does the workflow actually traverse &lt;code&gt;ApplyEditNode&lt;/code&gt; $\rightarrow$ &lt;code&gt;BuildPreviewNode&lt;/code&gt; $\rightarrow$ &lt;code&gt;HITLConfirmNode&lt;/code&gt; $\rightarrow$ &lt;code&gt;SaveCardNode&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval Degradation and Grounding&lt;/strong&gt;: When a strict &lt;code&gt;HybridSearchNode&lt;/code&gt; finds no results (0 hits), does it automatically degrade to trigger &lt;code&gt;BroadSearchNode&lt;/code&gt;? Does the generated icebreaker follow-up email draft truly reference the meeting occasion and company background from the business card without hallucination?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To systematically solve this problem, Google Cloud &lt;code&gt;agents-cli&lt;/code&gt; proposes a five-step &lt;strong&gt;Quality Flywheel&lt;/strong&gt; workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dot"&gt;&lt;code&gt;&lt;span class="nv"&gt;flowchart&lt;/span&gt; &lt;span class="nv"&gt;LR&lt;/span&gt;
    &lt;span class="nv"&gt;A&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"1. Generate&amp;lt;br/&amp;gt;(agents-cli eval generate)&amp;lt;br/&amp;gt;Start ADK server and collect real trajectories"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;B&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"2. Grade&amp;lt;br/&amp;gt;(agents-cli eval grade)&amp;lt;br/&amp;gt;Execute Trajectory and quality metric scoring"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;B&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;C&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"3. Analyze&amp;lt;br/&amp;gt;Review scores for each metric and&amp;lt;br/&amp;gt;Explanation for failed cases"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;C&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;D&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"4. Fix&amp;lt;br/&amp;gt;Fix Graph Node, Prompt&amp;lt;br/&amp;gt;or event tracking structure"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;D&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;E&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"5. Compare&amp;lt;br/&amp;gt;(agents-cli eval compare)&amp;lt;br/&amp;gt;Baseline vs. Candidate quantitative comparison"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;E&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;|&lt;/span&gt;&lt;span class="nv"&gt;Continuous&lt;/span&gt; &lt;span class="nv"&gt;Iteration&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;A&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  II. Revealing How &lt;code&gt;agents-cli eval generate&lt;/code&gt; Tests a Go-based ADK 2.0 Agent?
&lt;/h1&gt;

&lt;p&gt;Many Go developers new to &lt;code&gt;agents-cli&lt;/code&gt; wonder: &lt;strong&gt;"The &lt;code&gt;agents-cli&lt;/code&gt; is a CLI tool written in Python; how can it evaluate my &lt;code&gt;adk/v2/workflow&lt;/code&gt; Agent written in Go?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The answer lies within the &lt;strong&gt;&lt;code&gt;main.go&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;appinfo/appinfo.go&lt;/code&gt;&lt;/strong&gt; we introduced in Phase 1! When you execute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DRY_RUN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;agents-cli &lt;span class="nb"&gt;eval &lt;/span&gt;generate &lt;span class="nt"&gt;--dataset&lt;/span&gt; &lt;span class="nb"&gt;eval&lt;/span&gt;/datasets/basic-dataset.json

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;agents-cli&lt;/code&gt; automatically completes the following four steps in the background:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Automatically compile and start your Go Agent server&lt;/strong&gt;: &lt;code&gt;agents-cli&lt;/code&gt; reads the &lt;code&gt;agents-cli-manifest.yaml&lt;/code&gt; in the project root (identified as &lt;code&gt;template: adk_go&lt;/code&gt;) and automatically starts in the background:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go run &lt;span class="nb"&gt;.&lt;/span&gt; web &lt;span class="nt"&gt;--port&lt;/span&gt; &amp;lt;random_port&amp;gt; api &lt;span class="nt"&gt;-path_prefix&lt;/span&gt; / a2a appinfo

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Introspect Agent structure via &lt;code&gt;GET /apps/app/app-info&lt;/code&gt; reflection&lt;/strong&gt;: &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/appinfo/appinfo.go" rel="noopener noreferrer"&gt;&lt;code&gt;appinfo/appinfo.go&lt;/code&gt;&lt;/a&gt; uses Go's &lt;code&gt;reflect&lt;/code&gt; and &lt;code&gt;unsafe&lt;/code&gt; to traverse the Edges and Nodes of the entire &lt;code&gt;workflowagent&lt;/code&gt;, assembling the embedded &lt;code&gt;CompanyEnrichAgent&lt;/code&gt; (&lt;code&gt;llmagent&lt;/code&gt;), Instructions, and tool declarations into a standard &lt;code&gt;AppInfo&lt;/code&gt; JSON returned to &lt;code&gt;agents-cli&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Create Sessions one by one and call &lt;code&gt;POST /run_sse&lt;/code&gt; to collect event streams&lt;/strong&gt;: For each test case (&lt;code&gt;eval_case_id&lt;/code&gt;) in &lt;code&gt;eval/datasets/basic-dataset.json&lt;/code&gt;, &lt;code&gt;agents-cli&lt;/code&gt; first calls &lt;code&gt;POST /apps/app/users/eval_user/sessions&lt;/code&gt; to create an independent Session, then sends the test case's &lt;code&gt;prompt&lt;/code&gt; to &lt;code&gt;POST /run_sse&lt;/code&gt;, and records every ADK Event (including &lt;code&gt;author&lt;/code&gt;, &lt;code&gt;content&lt;/code&gt;, &lt;code&gt;actions.state_delta&lt;/code&gt;, &lt;code&gt;actions.requested_tool_confirmations&lt;/code&gt;, etc.) streamed back from the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output standardized trace file &lt;code&gt;artifacts/traces/traces_&amp;lt;timestamp&amp;gt;.json&lt;/code&gt;&lt;/strong&gt;: After testing is complete, it automatically shuts down the Go server and saves the full trajectory as a JSON, passing it to the next stage, &lt;code&gt;agents-cli eval grade&lt;/code&gt;, for scoring.&lt;/li&gt;
&lt;/ol&gt;




&lt;h1&gt;
  
  
  III. Practical Design: A Golden Evaluation Set Covering 4 Core Scenarios (&lt;code&gt;eval/datasets/basic-dataset.json&lt;/code&gt;)
&lt;/h1&gt;

&lt;p&gt;To fully cover the four core acceptance scenarios defined in &lt;a href="https://github.com/kkdai/line-card-assistant/issues/7" rel="noopener noreferrer"&gt;Issue #7&lt;/a&gt;, we designed &lt;strong&gt;6 Golden Eval Cases&lt;/strong&gt; in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/eval/datasets/basic-dataset.json" rel="noopener noreferrer"&gt;&lt;code&gt;eval/datasets/basic-dataset.json&lt;/code&gt;&lt;/a&gt;, marking the expected node trajectory (&lt;code&gt;expected_nodes&lt;/code&gt;) and state assertions in each case's &lt;code&gt;custom_Properties&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Eval Case ID&lt;/th&gt;
&lt;th&gt;Corresponding Core Scenario&lt;/th&gt;
&lt;th&gt;Input Prompt (Simulated Channel Signal)&lt;/th&gt;
&lt;th&gt;Verification Focus (&lt;code&gt;custom_Properties&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;case_1_ingest_fanout_fanin_ocr&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Scenario 1: Business Card Recognition and Parallel Enhancement&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;action=ingest&amp;amp;context=2026 Cloud Summit Taipei&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Verify &lt;code&gt;RouterNode&lt;/code&gt; $\rightarrow$ &lt;code&gt;VisionOCRNode&lt;/code&gt; $\rightarrow$ Parallel Fan-Out (&lt;code&gt;CleanAndDedupNode&lt;/code&gt;, &lt;code&gt;CompanyEnrichNode&lt;/code&gt;, &lt;code&gt;ContextMatchNode&lt;/code&gt;) $\rightarrow$ &lt;code&gt;BuildPreviewNode&lt;/code&gt; $\rightarrow$ &lt;code&gt;HITLConfirmNode&lt;/code&gt; pause, and phone normalization to &lt;code&gt;+886-912-345-678&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;case_2_duplicate_promotion_detection&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Scenario 2: Duplicate Card and Promotion Detection&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;action=ingest&amp;amp;card=promotion_david_chen_appier&amp;amp;context=2026 AI DevFest Taipei&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Test database already contains David Chen from Appier (&lt;code&gt;VP of Engineering&lt;/code&gt;); when a new card (&lt;code&gt;Chief Technology Officer (CTO)&lt;/code&gt;) is received, &lt;code&gt;CleanAndDedupNode&lt;/code&gt; must mark &lt;code&gt;IsDuplicate=true&lt;/code&gt; and generate a promotion alert message.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;case_3_hitl_edit_and_confirm_loop&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Scenario 3: HITL Loop Correction and Saving&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;action=ingest&amp;amp;context=2026 Cloud Summit Taipei&amp;amp;edit=Change title to Technical VP, change occasion to COMPUTEX 2026 AI Forum&amp;amp;hitl_action=confirm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Verify from OCR preview $\rightarrow$ &lt;code&gt;ApplyEditNode&lt;/code&gt; semantic modification of title and occasion $\rightarrow$ &lt;code&gt;BuildPreviewNode&lt;/code&gt; re-rendering preview $\rightarrow$ &lt;code&gt;HITLConfirmNode&lt;/code&gt; confirmation $\rightarrow$ &lt;code&gt;SaveCardNode&lt;/code&gt; writing to database and generating follow-up email.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;case_4a_hybrid_search_cloud_summit_ai&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Scenario 4a: Natural Language Hybrid Retrieval&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Help me find people doing AI I met at Cloud Summit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Verify &lt;code&gt;QueryIntentNode&lt;/code&gt; converts colloquial speech to &lt;code&gt;SearchFilter&lt;/code&gt; $\rightarrow$ &lt;code&gt;HybridSearchNode&lt;/code&gt; hits 2 business cards $\rightarrow$ &lt;code&gt;FormatCarouselNode&lt;/code&gt; outputs Flex Carousel JSON.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;case_4b_broad_search_fallback&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Scenario 4b: 0 Hits Automatic Degradation to Fuzzy Retrieval&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Help me find Zhang San who does quantum finance blockchain I met at Tokyo Game Show&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Verify that when &lt;code&gt;HybridSearchNode&lt;/code&gt; finds no results (&lt;code&gt;0 hits&lt;/code&gt;), conditional routing automatically degrades to &lt;code&gt;BroadSearchNode&lt;/code&gt; for Token fuzzy retrieval to avoid blank replies.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;case_4c_personalized_icebreaker_email&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Scenario 4c: Personalized Icebreaker Follow-up Email&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;action=draft_email&amp;amp;card_id=card-seed-1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Verify &lt;code&gt;EmailDraftNode&lt;/code&gt; writes a hallucination-free business icebreaker email based on the specified card's &lt;code&gt;MetContext&lt;/code&gt; (&lt;code&gt;2026 Cloud Summit Taipei&lt;/code&gt;) and &lt;code&gt;CompanyInfo&lt;/code&gt; (&lt;code&gt;Google Cloud Taiwan&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h1&gt;
  
  
  IV. Real-world Pitfalls and Debugging: Why was the Trajectory Score &lt;code&gt;0.0000&lt;/code&gt; on the First &lt;code&gt;agents-cli eval grade&lt;/code&gt; Run?
&lt;/h1&gt;

&lt;p&gt;After writing &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/eval/eval_config.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;eval/eval_config.yaml&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/eval/metrics.py" rel="noopener noreferrer"&gt;&lt;code&gt;eval/metrics.py&lt;/code&gt;&lt;/a&gt;, we confidently ran the first Baseline evaluation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;DRY_RUN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;agents-cli &lt;span class="nb"&gt;eval &lt;/span&gt;run &lt;span class="nt"&gt;--config&lt;/span&gt; &lt;span class="nb"&gt;eval&lt;/span&gt;/eval_config.yaml

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first Baseline scorecard (&lt;code&gt;results_20261022_223104.json&lt;/code&gt;) printed in the terminal shocked us:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;╭─ Evaluation Summary ───────────────────────────╮
│ Metric Score Valid │
│ ────────────────────────────────────────────── │
│ workflow_trajectory_accuracy 0.0000 6/6 │
│ ocr_and_enrichment_quality 1.0000 6/6 │
│ duplicate_and_hitl_precision 0.7333 6/6 │
│ search_and_email_grounding 1.0000 6/6 │
╰────────────────────────────────────────────────╯

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the same time, a very critical warning message appeared in the &lt;code&gt;agents-cli eval grade&lt;/code&gt; execution log:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Dropped 26 event(s) with no content; the eval service rejects them&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  🔍 Root Cause: It turns out &lt;code&gt;drop_empty_content_events&lt;/code&gt; filtered out intermediate node events!
&lt;/h3&gt;

&lt;p&gt;Why were 26 events dropped? We immediately checked the implementation of ADK Go &lt;code&gt;workflow.FunctionNode&lt;/code&gt; and the event preprocessing logic of &lt;code&gt;agents-cli&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;In our original &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/workflow/nodes_ingest.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/workflow/nodes_ingest.go&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/workflow/nodes_search.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/workflow/nodes_search.go&lt;/code&gt;&lt;/a&gt;, &lt;strong&gt;Intermediate Nodes&lt;/strong&gt; like &lt;code&gt;RouterNode&lt;/code&gt;, &lt;code&gt;VisionOCRNode&lt;/code&gt;, &lt;code&gt;CleanAndDedupNode&lt;/code&gt;, &lt;code&gt;CompanyEnrichNode&lt;/code&gt;, &lt;code&gt;ContextMatchNode&lt;/code&gt;, and &lt;code&gt;QueryIntentNode&lt;/code&gt; were only responsible for writing results into &lt;code&gt;ev.Actions.StateDelta&lt;/code&gt; (e.g., &lt;code&gt;ev.Actions.StateDelta["draft_card"] = ...&lt;/code&gt;), while keeping &lt;strong&gt;&lt;code&gt;ev.Content&lt;/code&gt; as &lt;code&gt;nil&lt;/code&gt;&lt;/strong&gt;; only the final output node (such as &lt;code&gt;BuildPreviewNode&lt;/code&gt;, &lt;code&gt;SaveCardNode&lt;/code&gt;, &lt;code&gt;FormatCarouselNode&lt;/code&gt;) would set &lt;code&gt;ev.Content&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;However, before &lt;code&gt;agents-cli eval grade&lt;/code&gt; sends the trajectory to the grader, the underlying &lt;code&gt;drop_empty_content_events()&lt;/code&gt;—to comply with the Schema constraints of the cloud Vertex AI Eval Service—&lt;strong&gt;will directly remove all events where &lt;code&gt;event.content == null&lt;/code&gt; from &lt;code&gt;agent_data.turns[].events&lt;/code&gt;!&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Once intermediate node events are removed because &lt;code&gt;content == nil&lt;/code&gt;, the &lt;code&gt;actions.state_delta&lt;/code&gt; and node execution records attached to those events also disappear! This caused the grader to only see the output of the last node, resulting in a &lt;code&gt;workflow_trajectory_accuracy&lt;/code&gt; of &lt;code&gt;0.0000&lt;/code&gt;!&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  🛠️ Elegant Solution: Adding Structured Trajectory Summaries &lt;code&gt;setNodeTrace&lt;/code&gt; to Intermediate Nodes
&lt;/h3&gt;

&lt;p&gt;Once the root cause was found, the solution was clear and elegant! We designed a lightweight helper function &lt;code&gt;setNodeTrace&lt;/code&gt; in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/workflow/nodes_ingest.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/workflow/nodes_ingest.go&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// setNodeTrace attaches a concise model text part to intermediate workflow node&lt;/span&gt;
&lt;span class="c"&gt;// events so that `agents-cli eval` (which drops events with empty Content)&lt;/span&gt;
&lt;span class="c"&gt;// retains the full node execution trajectory and state_delta history.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;setNodeTrace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nodeName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewContentFromText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"[%s] %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nodeName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RoleModel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, we call &lt;code&gt;setNodeTrace&lt;/code&gt; before each intermediate node finishes, for example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;RouterNode&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;setNodeTrace(ev, "RouterNode", "route="+route)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;VisionOCRNode&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;setNodeTrace(ev, "VisionOCRNode", fmt.Sprintf("ocr extracted name=%s company=%s title=%s", card.Name, card.Company, card.Title))&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CleanAndDedupNode&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;setNodeTrace(ev, "CleanAndDedupNode", fmt.Sprintf("phone=%s is_duplicate=%v", card.Phone, isDup))&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;CompanyEnrichNode&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;setNodeTrace(ev, "CompanyEnrichNode", fmt.Sprintf("company_info=%s tags=%v", card.CompanyInfo, card.Tags))&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ContextMatchNode&lt;/code&gt;&lt;/strong&gt;: &lt;code&gt;setNodeTrace(ev, "ContextMatchNode", "met_context="+card.MetContext)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;QueryIntentNode&lt;/code&gt; / &lt;code&gt;HybridSearchNode&lt;/code&gt; / &lt;code&gt;BroadSearchNode&lt;/code&gt;&lt;/strong&gt;: Record the converted &lt;code&gt;SearchFilter&lt;/code&gt; and the number of business cards hit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the same time, to prevent the &lt;code&gt;[NodeName] ...&lt;/code&gt; trajectory text of intermediate nodes from overwriting the &lt;code&gt;ReplyText&lt;/code&gt; that &lt;code&gt;Engine.executeTurn&lt;/code&gt; ultimately returns to the LINE caller, we simultaneously strengthened the state convergence logic in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/workflow/graph.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/workflow/graph.go&lt;/code&gt;&lt;/a&gt;—allowing &lt;code&gt;TurnResult&lt;/code&gt; to read authoritative &lt;code&gt;StateKeyReplyText&lt;/code&gt;, &lt;code&gt;StateKeyFlexPreviewJSON&lt;/code&gt;, &lt;code&gt;StateKeyFlexCarouselJSON&lt;/code&gt;, and &lt;code&gt;StateKeyDraftEmail&lt;/code&gt; directly from the final converged &lt;code&gt;Session.State&lt;/code&gt;!&lt;/p&gt;




&lt;h1&gt;
  
  
  V. Turning the Quality Flywheel: From Baseline to Candidate, All Four Metrics Reached &lt;code&gt;1.0000&lt;/code&gt;!
&lt;/h1&gt;

&lt;p&gt;After adding &lt;code&gt;setNodeTrace&lt;/code&gt; and supporting single-turn batch HITL modification verification, we re-executed &lt;code&gt;DRY_RUN=true agents-cli eval run --config eval/eval_config.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Loaded 6 cases from eval/datasets/basic-dataset.json
Starting local Go ADK server...
Running 6 cases against http://127.0.0.1:56649 (app=app, concurrency=4)...
Wrote 6 traces to artifacts/traces/traces_20261022_223515.json
Loaded 6 trace(s) from artifacts/traces/traces_20261022_223515.json
Evaluating metrics: ['workflow_trajectory_accuracy', 'ocr_and_enrichment_quality', 'duplicate_and_hitl_precision', 'search_and_email_grounding']

╭─ Evaluation Summary ───────────────────────────╮
│ Metric Score Valid │
│ ────────────────────────────────────────────── │
│ workflow_trajectory_accuracy 1.0000 6/6 │
│ ocr_and_enrichment_quality 1.0000 6/6 │
│ duplicate_and_hitl_precision 1.0000 6/6 │
│ search_and_email_grounding 1.0000 6/6 │
╰────────────────────────────────────────────────╯

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time, &lt;strong&gt;&lt;code&gt;Dropped 0 event(s)&lt;/code&gt;&lt;/strong&gt;, and all intermediate node trajectories and &lt;code&gt;state_delta&lt;/code&gt; were 100% preserved! We immediately used the 5th step of the Quality Flywheel, &lt;strong&gt;&lt;code&gt;agents-cli eval compare&lt;/code&gt;&lt;/strong&gt;, to quantitatively compare the Baseline before the fix with the Candidate after the fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;agents-cli eval compare \
  --baseline artifacts/grade_results/results_20261022_223104.json \
  --candidate artifacts/grade_results/results_20261022_223515.json


╭─ Summary Comparison ─────────────────────────────────────────────────────────╮
│ Metric Baseline Candidate Delta │
│ ──────────────────────────────────────────────────────────────────────────── │
│ duplicate_and_hitl_precision 0.7333 1.0000 +0.2667 │
│ ocr_and_enrichment_quality 1.0000 1.0000 0.0000 │
│ search_and_email_grounding 1.0000 1.0000 0.0000 │
│ workflow_trajectory_accuracy 0.0000 1.0000 +1.0000 │
╰──────────────────────────────────────────────────────────────────────────────╯

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Advancing from &lt;code&gt;0.0000&lt;/code&gt; / &lt;code&gt;0.7333&lt;/code&gt; to &lt;strong&gt;all four metrics reaching &lt;code&gt;1.0000&lt;/code&gt; (&lt;code&gt;+1.0000&lt;/code&gt; / &lt;code&gt;+0.2667&lt;/code&gt;)&lt;/strong&gt; is exactly the most charming value of the &lt;code&gt;agents-cli eval&lt;/code&gt; quality flywheel—&lt;strong&gt;making every architectural adjustment traceable and provable!&lt;/strong&gt;&lt;/p&gt;




&lt;h1&gt;
  
  
  VI. Dual-Track Verification: Python Custom Graders (&lt;code&gt;eval/metrics.py&lt;/code&gt;) + Go Native Multi-turn Trajectory Tests (&lt;code&gt;eval/eval_test.go&lt;/code&gt;)
&lt;/h1&gt;

&lt;p&gt;In the &lt;a href="https://github.com/kkdai/line-card-assistant/tree/main/eval" rel="noopener noreferrer"&gt;&lt;code&gt;eval/&lt;/code&gt;&lt;/a&gt; directory, we adopted a &lt;strong&gt;"Python &lt;code&gt;agents-cli eval&lt;/code&gt; + Go &lt;code&gt;go test&lt;/code&gt; dual-track verification architecture"&lt;/strong&gt;:&lt;/p&gt;

&lt;h2&gt;
  
  
  1. &lt;code&gt;eval/eval_config.yaml&lt;/code&gt; and &lt;code&gt;eval/metrics.py&lt;/code&gt;: Four Custom Evaluation Metrics
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/eval/eval_config.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;eval/eval_config.yaml&lt;/code&gt;&lt;/a&gt;, we registered 4 deterministic trajectory and quality graders (plus 1 &lt;code&gt;card_assistant_llm_rubric&lt;/code&gt; available for online Vertex AI Judge models):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;metrics_to_run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;workflow_trajectory_accuracy&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ocr_and_enrichment_quality&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;duplicate_and_hitl_precision&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;search_and_email_grounding&lt;/span&gt;

&lt;span class="na"&gt;custom_metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_trajectory_accuracy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;custom&lt;/span&gt;
    &lt;span class="na"&gt;module&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eval.metrics&lt;/span&gt;
    &lt;span class="na"&gt;function&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;evaluate_workflow_trajectory&lt;/span&gt;
  &lt;span class="na"&gt;ocr_and_enrichment_quality&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;custom&lt;/span&gt;
    &lt;span class="na"&gt;module&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eval.metrics&lt;/span&gt;
    &lt;span class="na"&gt;function&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;evaluate_ocr_and_enrichment&lt;/span&gt;
  &lt;span class="na"&gt;duplicate_and_hitl_precision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;custom&lt;/span&gt;
    &lt;span class="na"&gt;module&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eval.metrics&lt;/span&gt;
    &lt;span class="na"&gt;function&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;evaluate_duplicate_and_hitl&lt;/span&gt;
  &lt;span class="na"&gt;search_and_email_grounding&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;custom&lt;/span&gt;
    &lt;span class="na"&gt;module&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eval.metrics&lt;/span&gt;
    &lt;span class="na"&gt;function&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;evaluate_search_and_email&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/eval/metrics.py" rel="noopener noreferrer"&gt;&lt;code&gt;eval/metrics.py&lt;/code&gt;&lt;/a&gt;, we defined helper functions &lt;code&gt;_extract_executed_nodes(instance)&lt;/code&gt; and &lt;code&gt;_merged_state_delta(instance)&lt;/code&gt; to parse the actual node sequence traversed by the Graph Workflow and the cumulative Session State directly from &lt;code&gt;agent_data.turns[].events&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_extract_executed_nodes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Extract executed workflow node names in order from `[NodeName] ...` traces.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;_iter_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;[]:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;NODE_TRACE_PATTERN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, &lt;code&gt;evaluate_workflow_trajectory&lt;/code&gt; can accurately check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Whether each node was executed in topological order (Subsequence Order)?&lt;/li&gt;
&lt;li&gt;Whether all &lt;code&gt;expected_state_keys&lt;/code&gt; (such as &lt;code&gt;draft_card&lt;/code&gt;, &lt;code&gt;flex_preview_json&lt;/code&gt;, &lt;code&gt;saved_card_id&lt;/code&gt;, &lt;code&gt;search_results&lt;/code&gt;) were written to &lt;code&gt;state_delta&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Whether the final output simultaneously satisfies &lt;code&gt;expected_response_substrings&lt;/code&gt; and excludes &lt;code&gt;forbidden_substrings&lt;/code&gt;?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;eval/eval_test.go&lt;/code&gt;: Go Native Multi-turn HITL Pause/Resume and &lt;code&gt;/app-info&lt;/code&gt; Introspection Tests
&lt;/h2&gt;

&lt;p&gt;In addition to &lt;code&gt;agents-cli eval&lt;/code&gt; called via HTTP SSE, we also wrote Go native &lt;strong&gt;&lt;code&gt;TestEvalDatasetAndAppInfoIntrospection&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;TestPhase4WorkflowTrajectorySuite&lt;/code&gt;&lt;/strong&gt; in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/eval/eval_test.go" rel="noopener noreferrer"&gt;&lt;code&gt;eval/eval_test.go&lt;/code&gt;&lt;/a&gt;, which can be executed in milliseconds via &lt;code&gt;go test ./...&lt;/code&gt; or GitHub Actions CI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;TestEvalDatasetAndAppInfoIntrospection&lt;/code&gt;&lt;/strong&gt;: Verifies the Schema integrity of &lt;code&gt;eval/datasets/basic-dataset.json&lt;/code&gt; and directly calls &lt;code&gt;appinfo.BuildAppInfo&lt;/code&gt; to confirm that the &lt;code&gt;CompanyEnrichAgent&lt;/code&gt; within the &lt;code&gt;line_card_assistant&lt;/code&gt; graph can be correctly introspected via reflection by &lt;code&gt;/apps/{app}/app-info&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;TestPhase4WorkflowTrajectorySuite&lt;/code&gt;&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Case1_IngestFanOutFanInOCR&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Case2_DuplicatePromotionDetection&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Case3_MultiTurnHITLEditAndConfirmLoop&lt;/code&gt; (Actually calls &lt;code&gt;StartIngest&lt;/code&gt; $\rightarrow$ &lt;code&gt;ResumeHITL(edit)&lt;/code&gt; $\rightarrow$ &lt;code&gt;ResumeHITL(confirm)&lt;/code&gt; in three turns to verify the cross-turn Pause/Resume state machine)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Case4_SearchHybridAndBroadFallbackAndEmailDraft&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, with just one line &lt;code&gt;make eval&lt;/code&gt;, you can run both &lt;code&gt;agents-cli eval run&lt;/code&gt; and the Go native test suite:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ make eval
DRY_RUN=true agents-cli eval run --config eval/eval_config.yaml
...
go test -v ./eval/...
=== RUN TestEvalDatasetAndAppInfoIntrospection
--- PASS: TestEvalDatasetAndAppInfoIntrospection (0.00s)
=== RUN TestPhase4WorkflowTrajectorySuite
    --- PASS: TestPhase4WorkflowTrajectorySuite/Case1_IngestFanOutFanInOCR (0.00s)
    --- PASS: TestPhase4WorkflowTrajectorySuite/Case2_DuplicatePromotionDetection (0.00s)
    --- PASS: TestPhase4WorkflowTrajectorySuite/Case3_MultiTurnHITLEditAndConfirmLoop (0.00s)
    --- PASS: TestPhase4WorkflowTrajectorySuite/Case4_SearchHybridAndBroadFallbackAndEmailDraft (0.00s)
PASS
ok github.com/kkdai/line-card-assistant/eval    0.436s

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h1&gt;
  
  
  VII. Summary and Next Preview (Phase 5 Final Chapter: Official Deployment, Terraform IaC, and Cloud Trace Observability)
&lt;/h1&gt;

&lt;p&gt;As of now, our &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/issues/9" rel="noopener noreferrer"&gt;LINE AI Business Card Master Roadmap (Issue #9)&lt;/a&gt;&lt;/strong&gt; has completed the first four major stages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; &lt;strong&gt;Phase 1&lt;/strong&gt;: &lt;code&gt;agents-cli&lt;/code&gt; environment and &lt;code&gt;adk_go&lt;/code&gt; project Scaffolding (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/10" rel="noopener noreferrer"&gt;PR #10&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Phase 2&lt;/strong&gt;: ADK Go Graph Workflow adaptation for Agent Runtime, GCS Artifacts, Agent Platform Sessions, and Cloud SQL &lt;code&gt;PostgresStore&lt;/code&gt; (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/11" rel="noopener noreferrer"&gt;PR #11&lt;/a&gt;, &lt;a href="https://github.com/kkdai/line-card-assistant/pull/12" rel="noopener noreferrer"&gt;PR #12&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Phase 3&lt;/strong&gt;: Cloud Run LINE Webhook Gateway and Agent Runtime dual-layer decoupled architecture (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/12" rel="noopener noreferrer"&gt;PR #12&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Phase 4&lt;/strong&gt;: &lt;code&gt;agents-cli eval&lt;/code&gt; automated evaluation and quality flywheel (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/13" rel="noopener noreferrer"&gt;PR #13&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the next article (which is also the final chapter of this series, &lt;strong&gt;Phase 5 — &lt;a href="https://github.com/kkdai/line-card-assistant/issues/8" rel="noopener noreferrer"&gt;Issue #8&lt;/a&gt;&lt;/strong&gt;), we will officially push the LINE AI Business Card Master, which passed the evaluation with a perfect &lt;code&gt;1.0000&lt;/code&gt; score, into the production environment via &lt;code&gt;agents-cli deploy&lt;/code&gt;. We will also fully integrate &lt;strong&gt;Terraform IaC (Infrastructure as Code), GitHub Actions CI/CD automated evaluation and deployment pipelines, and OpenTelemetry + Google Cloud Trace full-link observability&lt;/strong&gt;. Stay tuned!&lt;/p&gt;




&lt;h2&gt;
  
  
  Reference Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository&lt;/strong&gt;: &lt;a href="https://github.com/kkdai/line-card-assistant" rel="noopener noreferrer"&gt;kkdai/line-card-assistant&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 4 Pull Request&lt;/strong&gt;: &lt;a href="https://github.com/kkdai/line-card-assistant/pull/13" rel="noopener noreferrer"&gt;PR #13 — &lt;code&gt;feat(eval): add Phase 4 agents-cli eval suite and Go workflow trajectory tests (#7)&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Cloud &lt;code&gt;agents-cli&lt;/code&gt; Official Documentation&lt;/strong&gt;: &lt;a href="https://google.github.io/agents-cli/guide/lifecycle/" rel="noopener noreferrer"&gt;The Quality Flywheel &amp;amp; Agent Evaluation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google ADK Official Documentation&lt;/strong&gt;: &lt;a href="https://adk.dev/graphs/" rel="noopener noreferrer"&gt;Graph-based Agent Workflows (&lt;code&gt;adk/v2/workflow&lt;/code&gt;)&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>architecture</category>
      <category>cloud</category>
    </item>
    <item>
      <title>[Agent Platform in Practice (2)] From Monolithic Cloud Run to Google Cloud Agent Runtime: Refactoring LINE AI Business Card King</title>
      <dc:creator>Evan Lin</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:12:59 +0000</pubDate>
      <link>https://dev.to/gde/agent-platform-in-practice-2-from-monolithic-cloud-run-to-google-cloud-agent-runtime-bo1</link>
      <guid>https://dev.to/gde/agent-platform-in-practice-2-from-monolithic-cloud-run-to-google-cloud-agent-runtime-bo1</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh4d6ems2crnj3jcf3a8k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh4d6ems2crnj3jcf3a8k.png" alt="LINE Card Assistant + Google Cloud Agent Platform Architecture" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Recap
&lt;/h1&gt;

&lt;p&gt;In the previous article, "&lt;a href="https://dev.to/2026/10/16/adk-graph-workflow-go/"&gt;[Agent Platform in Practice (1)] Building a LINE AI Business Card Assistant with Go and Google ADK 2.0 Graph Workflow&lt;/a&gt;", we used &lt;strong&gt;Golang (&lt;code&gt;google.golang.org/adk/v2/workflow&lt;/code&gt;)&lt;/strong&gt; and the &lt;strong&gt;LINE Messaging API SDK v8&lt;/strong&gt; to build &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant" rel="noopener noreferrer"&gt;line-card-assistant (LINE AI Business Card Assistant)&lt;/a&gt;&lt;/strong&gt; from scratch. We successfully implemented and deployed the four core capabilities of ADK 2.0 Graph Workflow—&lt;strong&gt;Deterministic Router&lt;/strong&gt;, &lt;strong&gt;&lt;code&gt;AddFanOut&lt;/code&gt; / &lt;code&gt;AddFanIn&lt;/code&gt; for parallel background investigation and aggregation&lt;/strong&gt;, and &lt;strong&gt;&lt;code&gt;NewRequestInputEvent&lt;/code&gt; for Human-in-the-Loop (HITL) pause and loop correction&lt;/strong&gt;—to Google Cloud Run.&lt;/p&gt;

&lt;p&gt;However, as we evolve this monolithic LINE Bot into a &lt;strong&gt;Production-grade Agent Platform&lt;/strong&gt;, we immediately encounter several architectural questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Where is the cross-instance HITL Session state stored?&lt;/strong&gt; In the first version, we used &lt;code&gt;session.InMemoryService()&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;How can this business card Agent be called by other Agents (Agent-to-Agent, A2A)?&lt;/strong&gt; 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 &lt;strong&gt;A2A protocol&lt;/strong&gt;, a monolithic server tied to the LINE Webhook &lt;code&gt;/webhook&lt;/code&gt; entry point cannot directly provide a standard Agent interface.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;How to systematically evaluate and observe the entire Graph?&lt;/strong&gt; 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?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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—&lt;strong&gt;&lt;a href="https://google.github.io/agents-cli/guide/getting-started/" rel="noopener noreferrer"&gt;&lt;code&gt;agents-cli&lt;/code&gt; (Google Agents CLI)&lt;/a&gt;&lt;/strong&gt;, serving as the unified development entry point for the &lt;strong&gt;Google Cloud Agent Platform (now integrated as Gemini Enterprise Agent Platform)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this second episode of the series, we will dive deep into the &lt;strong&gt;&lt;a href="https://google.github.io/agents-cli/guide/getting-started/" rel="noopener noreferrer"&gt;&lt;code&gt;agents-cli&lt;/code&gt; official development guide&lt;/a&gt;&lt;/strong&gt;, answer frequently asked questions about &lt;strong&gt;Agent Runtime scaling (Scale-to-Zero) and billing mechanisms&lt;/strong&gt;, and walk you through how we completed the &lt;strong&gt;Phase 1 architecture upgrade (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/10" rel="noopener noreferrer"&gt;PR #10&lt;/a&gt;)&lt;/strong&gt;: refactoring the project into a dual-channel architecture that complies with &lt;code&gt;adk_go&lt;/code&gt; + &lt;code&gt;agent_runtime&lt;/code&gt; specifications without breaking the original LINE Bot functionality!&lt;/p&gt;




&lt;h1&gt;
  
  
  1. In-depth Analysis of Google Cloud Agent Platform and the &lt;code&gt;agents-cli&lt;/code&gt; Official Guide
&lt;/h1&gt;

&lt;p&gt;Before diving into the code, let's clarify Google's latest technical landscape for the Agent Platform through two core documents: "&lt;a href="https://google.github.io/agents-cli/guide/getting-started/" rel="noopener noreferrer"&gt;Getting Started - agents-cli&lt;/a&gt;" and "&lt;a href="https://google.github.io/agents-cli/guide/lifecycle/" rel="noopener noreferrer"&gt;The Lifecycle - agents-cli&lt;/a&gt;".&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Product Terminology Comparison: What are Agent Platform and Agent Runtime?
&lt;/h2&gt;

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

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




&lt;h2&gt;
  
  
  2. The Seven-Stage Development Lifecycle of &lt;code&gt;agents-cli&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;In the past, we were used to a "write code → &lt;code&gt;go test&lt;/code&gt; → &lt;code&gt;gcloud run deploy&lt;/code&gt;" workflow. However, in "&lt;a href="https://google.github.io/agents-cli/guide/lifecycle/" rel="noopener noreferrer"&gt;The Lifecycle&lt;/a&gt;" document, &lt;code&gt;agents-cli&lt;/code&gt; defines a &lt;strong&gt;Seven-Stage Quality Flywheel&lt;/strong&gt; that integrates Coding Agent Skills (supporting Antigravity CLI, Gemini CLI, Claude Code, etc.):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight dot"&gt;&lt;code&gt;&lt;span class="nv"&gt;flowchart&lt;/span&gt; &lt;span class="nv"&gt;LR&lt;/span&gt;
    &lt;span class="nv"&gt;P0&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Phase 0-1\nUnderstand &amp;amp; Recipes\n(.agents-cli-spec.md)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;P2&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Phase 2\nScaffold\n(create / enhance)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;P2&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;P3&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Phase 3\nBuild &amp;amp; Smoke Test\n(run / playground)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;P3&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;P4&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Phase 4\nEvaluate\n(eval run Quality Flywheel)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;P4&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;|&lt;/span&gt;&lt;span class="nv"&gt;Iterative&lt;/span&gt; &lt;span class="nv"&gt;Correction&lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt; &lt;span class="nv"&gt;P3&lt;/span&gt;
    &lt;span class="nv"&gt;P4&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;P5&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Phase 5\nDeploy\n(agent_runtime / cloud_run)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
    &lt;span class="nv"&gt;P5&lt;/span&gt; &lt;span class="o"&gt;--&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;P6&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Phase 6-7\nPublish &amp;amp; Observe\n(Cloud Trace / BigQuery)"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Phase 0 &amp;amp; 1 — Understand &amp;amp; Study Recipes&lt;/strong&gt;: 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 &lt;strong&gt;&lt;code&gt;.agents-cli-spec.md&lt;/code&gt;&lt;/strong&gt; as the Single Source of Truth for the entire development cycle.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Phase 2 — Scaffold&lt;/strong&gt;: Use &lt;code&gt;agents-cli scaffold create&lt;/code&gt; or run &lt;code&gt;agents-cli scaffold enhance .&lt;/code&gt; on an existing project to automatically generate the directory structure, configuration files (&lt;code&gt;agents-cli-manifest.yaml&lt;/code&gt;), A2A endpoints, Session switchers, and OpenTelemetry observation modules that comply with official standards.

&lt;ul&gt;
&lt;li&gt;  Supports two major official built-in templates: &lt;strong&gt;&lt;code&gt;adk&lt;/code&gt; (Python)&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;adk_go&lt;/code&gt; (Golang)&lt;/strong&gt;!&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Phase 3 — Build &amp;amp; Playground&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;  Go projects only need to focus on implementing and exporting &lt;code&gt;NewRootAgent(ctx)&lt;/code&gt; in &lt;code&gt;app/agent.go&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  During development, you can use &lt;code&gt;agents-cli run "your test question"&lt;/code&gt; for a quick terminal smoke test, or run &lt;code&gt;agents-cli playground&lt;/code&gt; (or &lt;code&gt;make playground&lt;/code&gt;) to open a browser-based visual interactive interface for multi-turn conversation testing.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Phase 4 — Evaluate (The core of the Quality Flywheel)&lt;/strong&gt;: The official guide emphasizes: &lt;strong&gt;"Unit Test (&lt;code&gt;go test&lt;/code&gt;) measures code contracts and type correctness; while &lt;code&gt;agents-cli eval&lt;/code&gt; measures Agent behavior quality (Tool Trajectory, response quality, and safety)."&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Phase 5 — Deploy&lt;/strong&gt;: Use &lt;code&gt;agents-cli deploy&lt;/code&gt; to package and deploy the project to &lt;code&gt;agent_runtime&lt;/code&gt;, &lt;code&gt;cloud_run&lt;/code&gt;, or &lt;code&gt;gke&lt;/code&gt;, and automatically record the deployed resource ID in &lt;code&gt;deployment_metadata.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Phase 6 &amp;amp; 7 — Publish &amp;amp; Observe&lt;/strong&gt;: Register to the enterprise directory via &lt;code&gt;agents-cli publish gemini&lt;/code&gt;, and combine with OpenTelemetry to automatically export all Graph node latencies, LLM Prompt/Response, and Token consumption to &lt;strong&gt;Cloud Trace&lt;/strong&gt;, &lt;strong&gt;Cloud Logging&lt;/strong&gt;, and &lt;strong&gt;BigQuery Agent Analytics&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  3. Agent Runtime vs. Cloud Run vs. GKE: Which Deployment Target Should I Choose?
&lt;/h2&gt;

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

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




&lt;h2&gt;
  
  
  4. Key Clarification: Will Instances Moved to Agent Runtime Scale to Zero?
&lt;/h2&gt;

&lt;p&gt;When planning to migrate &lt;code&gt;line-card-assistant&lt;/code&gt; to Agent Runtime, a common concern is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;"According to &lt;code&gt;agents-cli&lt;/code&gt; specifications, will instances moved to Agent Runtime never scale to 0 and constantly burn money?"&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;According to the &lt;code&gt;agents-cli&lt;/code&gt; official deployment specifications (Sizing a Deployment):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;When using &lt;code&gt;agents-cli deploy&lt;/code&gt; directly (Default Scale to Zero)&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;  The CLI's default resource allocation is identical to Cloud Run: &lt;code&gt;--cpu 1&lt;/code&gt;, &lt;code&gt;--memory 4Gi&lt;/code&gt;, &lt;code&gt;--concurrency 8&lt;/code&gt;, &lt;strong&gt;&lt;code&gt;--min-instances 0&lt;/code&gt;&lt;/strong&gt;, &lt;code&gt;--max-instances 10&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  This means when there is no traffic to the Agent Runtime, the number of instances will automatically scale down to &lt;code&gt;0&lt;/code&gt;. Since Agent Runtime is billed based on actual &lt;strong&gt;vCPU-hours and Memory-hours&lt;/strong&gt;, &lt;strong&gt;no compute fees are generated when idling at 0!&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;When using Terraform (&lt;code&gt;service.tf&lt;/code&gt;) for production deployment (Default &lt;code&gt;min_instances = 1&lt;/code&gt;)&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;  When you generate Terraform IaC (&lt;code&gt;deployment/terraform/single-project/service.tf&lt;/code&gt;) for a production environment, the template defaults &lt;code&gt;min_instances&lt;/code&gt; to &lt;code&gt;1&lt;/code&gt; to avoid cold starts for the first user.&lt;/li&gt;
&lt;li&gt;  If you want to save money in production and can tolerate a few seconds of cold start, you can specify &lt;code&gt;agents-cli deploy --min-instances 0&lt;/code&gt; during deployment or change &lt;code&gt;min_instances&lt;/code&gt; back to &lt;code&gt;0&lt;/code&gt; in &lt;code&gt;service.tf&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  💡 Why is a "Dual-layer Decoupled Architecture" best for a LINE Bot with Agent Runtime?
&lt;/h3&gt;

&lt;p&gt;Understanding the characteristics of Agent Runtime, you'll find that splitting the system into two layers is a perfect match:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Outer Layer: LINE Webhook Gateway (Cloud Run, &lt;code&gt;min-instances=0&lt;/code&gt;)&lt;/strong&gt;: Since the LINE server sends Webhooks with only an &lt;code&gt;X-Line-Signature&lt;/code&gt; 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.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Inner Layer: Core Brain (Agent Runtime, &lt;code&gt;min-instances=0&lt;/code&gt;)&lt;/strong&gt;: Focuses on running the ADK 2.0 Graph Workflow, managing cloud persistent &lt;code&gt;VertexAiEngineSessionService&lt;/code&gt; (ensuring HITL pause states aren't lost across containers!), and simultaneously exposing &lt;strong&gt;A2A (&lt;code&gt;/.well-known/agent-card.json&lt;/code&gt;)&lt;/strong&gt; and the &lt;strong&gt;Agent Engine Playground&lt;/strong&gt; to the enterprise!&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  2. Planning the Migration Blueprint: Creating a 5-Phase Roadmap Using GitHub CLI
&lt;/h1&gt;

&lt;p&gt;To complete this architectural evolution methodically, we used the &lt;code&gt;gh&lt;/code&gt; CLI in the GitHub Repository (&lt;a href="https://github.com/kkdai/line-card-assistant" rel="noopener noreferrer"&gt;&lt;code&gt;kkdai/line-card-assistant&lt;/code&gt;&lt;/a&gt;) to establish &lt;strong&gt;5 Milestones (Phases)&lt;/strong&gt;, &lt;strong&gt;8 implementation Issues&lt;/strong&gt;, and an overview tracking list &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/issues/9" rel="noopener noreferrer"&gt;Roadmap Issue #9&lt;/a&gt;&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/milestone/1" rel="noopener noreferrer"&gt;Phase 1: &lt;code&gt;agents-cli&lt;/code&gt; Environment and &lt;code&gt;adk_go&lt;/code&gt; Project Scaffolding&lt;/a&gt;&lt;/strong&gt; (The focus of this article!)

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://github.com/kkdai/line-card-assistant/issues/1" rel="noopener noreferrer"&gt;#1&lt;/a&gt; Upgrade &lt;code&gt;google-agents-cli&lt;/code&gt; to 1.10+ and create &lt;code&gt;.agents-cli-spec.md&lt;/code&gt; and &lt;code&gt;agents-cli-manifest.yaml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://github.com/kkdai/line-card-assistant/issues/2" rel="noopener noreferrer"&gt;#2&lt;/a&gt; Introduce &lt;code&gt;adk_go&lt;/code&gt; + &lt;code&gt;agent_runtime&lt;/code&gt; standard launcher architecture (&lt;code&gt;app/agent.go&lt;/code&gt;, A2A, &lt;code&gt;/app-info&lt;/code&gt;, Sessions, and Observability)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/milestone/2" rel="noopener noreferrer"&gt;Phase 2: ADK Go Graph Workflow Cloud Storage and Persistence Adaptation&lt;/a&gt;&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://github.com/kkdai/line-card-assistant/issues/3" rel="noopener noreferrer"&gt;#3&lt;/a&gt; Deepen ADK Go v2.6+ Graph Workflow and &lt;code&gt;NewRootAgent&lt;/code&gt; state management&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://github.com/kkdai/line-card-assistant/issues/4" rel="noopener noreferrer"&gt;#4&lt;/a&gt; Integrate Agent Platform Sessions (&lt;code&gt;session/vertexai&lt;/code&gt;) and GCS Artifact Service to support cross-instance HITL pause/resume&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://github.com/kkdai/line-card-assistant/issues/5" rel="noopener noreferrer"&gt;#5&lt;/a&gt; Expand business card database (&lt;code&gt;CardStore&lt;/code&gt;) to support Cloud SQL / Firestore cloud persistent storage&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/milestone/3" rel="noopener noreferrer"&gt;Phase 3: LINE Webhook Gateway and Agent Runtime Decoupled Architecture&lt;/a&gt;&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://github.com/kkdai/line-card-assistant/issues/6" rel="noopener noreferrer"&gt;#6&lt;/a&gt; Implement LINE Webhook Gateway (Cloud Run) and Agent Runtime dual-layer decoupled call architecture&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/milestone/4" rel="noopener noreferrer"&gt;Phase 4: &lt;code&gt;agents-cli eval&lt;/code&gt; Automated Evaluation and Benchmarking&lt;/a&gt;&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://github.com/kkdai/line-card-assistant/issues/7" rel="noopener noreferrer"&gt;#7&lt;/a&gt; Establish &lt;code&gt;agents-cli eval&lt;/code&gt; evaluation datasets and quality flywheel (OCR, Fan-Out/Fan-In, HITL loops, Hybrid Search)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/milestone/5" rel="noopener noreferrer"&gt;Phase 5: Agent Runtime Official Deployment, IaC, and Observability&lt;/a&gt;&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://github.com/kkdai/line-card-assistant/issues/8" rel="noopener noreferrer"&gt;#8&lt;/a&gt; Deploy to Agent Runtime via &lt;code&gt;agents-cli deploy&lt;/code&gt; and establish Terraform IaC, CI/CD, and Cloud Trace observability&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  3. Phase 1 Hands-on Breakdown: Transforming an Existing Go Project into an &lt;code&gt;adk_go&lt;/code&gt; + &lt;code&gt;agent_runtime&lt;/code&gt; Dual-Channel Architecture
&lt;/h1&gt;

&lt;p&gt;In Phase 1 (corresponding to the merged &lt;strong&gt;&lt;a href="https://github.com/kkdai/line-card-assistant/pull/10" rel="noopener noreferrer"&gt;PR #10&lt;/a&gt;&lt;/strong&gt;), our goal was to: &lt;strong&gt;retain the original &lt;code&gt;cmd/server/main.go&lt;/code&gt; independent LINE Webhook server while introducing the standard &lt;code&gt;agents-cli&lt;/code&gt; (&lt;code&gt;adk_go&lt;/code&gt; + &lt;code&gt;agent_runtime&lt;/code&gt;) architecture in the project root&lt;/strong&gt;, allowing the same codebase to possess both superpowers!&lt;/p&gt;

&lt;p&gt;Let's look at the key files changed in this upgrade and the new features they bring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  1. Creating &lt;code&gt;agents-cli-manifest.yaml&lt;/code&gt; and &lt;code&gt;.agents-cli-spec.md&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;To make &lt;code&gt;agents-cli&lt;/code&gt; recognize a Go project, the key is the &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/agents-cli-manifest.yaml" rel="noopener noreferrer"&gt;&lt;code&gt;agents-cli-manifest.yaml&lt;/code&gt;&lt;/a&gt; in the root directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Managed by agents-cli. Do not edit manually.&lt;/span&gt;
&lt;span class="na"&gt;acli_version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.10.0"&lt;/span&gt;
&lt;span class="na"&gt;project_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;line-card-assistant"&lt;/span&gt;
&lt;span class="na"&gt;root_agent_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;line_card_assistant"&lt;/span&gt;
&lt;span class="na"&gt;base_template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;adk_go"&lt;/span&gt;
&lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;go"&lt;/span&gt;
&lt;span class="na"&gt;framework&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;google-adk"&lt;/span&gt;
&lt;span class="na"&gt;deployment_target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent_runtime"&lt;/span&gt;
&lt;span class="na"&gt;agent_directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;app"&lt;/span&gt;
&lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asia-east1"&lt;/span&gt;
&lt;span class="na"&gt;agent_guidance_filename&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GEMINI.md"&lt;/span&gt;
&lt;span class="na"&gt;is_a2a&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;prototype&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;generated_at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2026-10-17T16:12:00Z"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ 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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. Encapsulating the Standard Root Agent Entry Point: &lt;code&gt;app/agent.go&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;In the &lt;code&gt;adk_go&lt;/code&gt; template specification, all Agent logic should be exported via &lt;strong&gt;&lt;code&gt;NewRootAgent(ctx context.Context) (agent.Agent, error)&lt;/code&gt;&lt;/strong&gt; under the &lt;code&gt;app&lt;/code&gt; package.&lt;/p&gt;

&lt;p&gt;Since we already used ADK 2.0's &lt;code&gt;workflowagent.New&lt;/code&gt; to encapsulate the 12-node Graph Workflow into a standard &lt;code&gt;agent.Agent&lt;/code&gt; in the first article, in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/app/agent.go" rel="noopener noreferrer"&gt;&lt;code&gt;app/agent.go&lt;/code&gt;&lt;/a&gt;, we only need to initialize the underlying &lt;code&gt;CardStore&lt;/code&gt; (SQLite or Memory) and &lt;code&gt;model.LLM&lt;/code&gt; (auto-detecting Vertex AI ADC or Gemini API Key), then call &lt;code&gt;workflow.NewCardAssistantAgent&lt;/code&gt; for a seamless connection!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"context"&lt;/span&gt;
    &lt;span class="s"&gt;"log/slog"&lt;/span&gt;
    &lt;span class="s"&gt;"os"&lt;/span&gt;
    &lt;span class="s"&gt;"strings"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/kkdai/line-card-assistant/internal/config"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/kkdai/line-card-assistant/internal/store"&lt;/span&gt;
    &lt;span class="s"&gt;"github.com/kkdai/line-card-assistant/internal/workflow"&lt;/span&gt;
    &lt;span class="s"&gt;"google.golang.org/adk/v2/agent"&lt;/span&gt;
    &lt;span class="s"&gt;"google.golang.org/adk/v2/model"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;RootAgentName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WorkflowName&lt;/span&gt; &lt;span class="c"&gt;// "line_card_assistant"&lt;/span&gt;

&lt;span class="c"&gt;// NewRootAgent constructs the root ADK v2 Graph Workflow agent for Agent Runtime,&lt;/span&gt;
&lt;span class="c"&gt;// ADK WebUI playground, A2A server, and evaluation CLI.&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewRootAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cardStore&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CardStore&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SQLitePath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sqliteStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewSQLiteStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SQLitePath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to open SQLite store, falling back to in-memory store"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"err"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;cardStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewMemoryStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;cardStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqliteStore&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;cardStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewMemoryStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LLM&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseVertexAI&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GoogleAPIKey&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GOOGLE_CLOUD_AGENT_ENGINE_ID"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;realLLM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewGeminiModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GeminiModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GoogleAPIKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UseVertexAI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GoogleCloudProject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GoogleCloudLocation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Warn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to initialize Gemini model, falling back to mock LLM"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"err"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewMockLLM&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;realLLM&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewMockLLM&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;NewRootAgentWithDeps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cardStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewRootAgentWithDeps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cardStore&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CardStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LLM&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewCardAssistantAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cardStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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




&lt;h2&gt;
  
  
  3. Agent Runtime Standard HTTP Server and A2A Endpoints (Root &lt;code&gt;main.go&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;Next, let's look at the new &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/main.go" rel="noopener noreferrer"&gt;&lt;code&gt;main.go&lt;/code&gt;&lt;/a&gt; in the project root. When the container is deployed to Agent Runtime (or executed locally via &lt;code&gt;make playground&lt;/code&gt;), this entry point mounts five standard services:&lt;/p&gt;

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




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

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

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

&lt;p&gt;Therefore, we made two fully backward-compatible enhancements in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/workflow/nodes_ingest.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/workflow/nodes_ingest.go&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/internal/workflow/nodes_search.go" rel="noopener noreferrer"&gt;&lt;code&gt;internal/workflow/nodes_search.go&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  (1) &lt;code&gt;RouterNode&lt;/code&gt; Automatically Parses Images and Text from &lt;code&gt;ctx.UserContent()&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// When uploading a business card image directly from ADK WebUI / A2A / Agent Engine Playground,&lt;/span&gt;
&lt;span class="c"&gt;// automatically extract InlineData.Data from ctx.UserContent().Parts and direct to RouteIngest!&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;getBytesState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StateKeyImageBytes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserContent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserContent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parts&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InlineData&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InlineData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StateKeyImageBytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InlineData&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;getStringState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StateKeyInputType&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;setState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StateKeyInputType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RouteIngest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;// If it's text input and the input parameter is empty, automatically extract query text or commands from ctx.UserContent().Parts&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;inputType&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;sTrim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sTrim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sTrim&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sTrim&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserContent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserContent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Parts&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;sTrim&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// Automatically determine if it's searching for a card (RouteSearch) or generating an icebreaker (action=draft_email&amp;amp;card_id=...)&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  (2) Terminal Nodes Synchronously Write to &lt;code&gt;ev.Content&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;BuildPreviewNode&lt;/code&gt;, &lt;code&gt;SaveCardNode&lt;/code&gt;, &lt;code&gt;FormatCarouselNode&lt;/code&gt;, and &lt;code&gt;EmailDraftNode&lt;/code&gt;, in addition to writing to &lt;code&gt;StateDelta&lt;/code&gt; for the LINE Bot, we added a line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewContentFromText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flexJSON&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;genai&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RoleModel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Sending or searching cards from LINE&lt;/strong&gt;: The LINE Webhook still gets the Flex JSON from &lt;code&gt;StateDelta&lt;/code&gt; to render the beautiful LINE card.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Testing from ADK WebUI (&lt;code&gt;make playground&lt;/code&gt;) or Agent Engine Playground&lt;/strong&gt;: Without needing a LINE account, you can upload a business card image or type "&lt;em&gt;Find someone I met at Cloud Summit who does AI&lt;/em&gt;" 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!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We also added a &lt;code&gt;TestRootAgentDirectSearchExecution&lt;/code&gt; unit test in &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/app/agent_test.go" rel="noopener noreferrer"&gt;&lt;code&gt;app/agent_test.go&lt;/code&gt;&lt;/a&gt;, using the standard ADK &lt;code&gt;runner.Runner&lt;/code&gt; to send a &lt;code&gt;genai.Content&lt;/code&gt; query to &lt;code&gt;app.NewRootAgent&lt;/code&gt;, verifying that the business card Carousel JSON can be obtained directly from the Event Stream without going through the LINE Engine!&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Multi-binary &lt;code&gt;Dockerfile&lt;/code&gt;: One Image for Both Agent Runtime and LINE Webhook
&lt;/h2&gt;

&lt;p&gt;Finally, to allow the same container image to be deployed to &lt;strong&gt;Agent Runtime&lt;/strong&gt; via &lt;code&gt;agents-cli deploy&lt;/code&gt; and to &lt;strong&gt;Cloud Run&lt;/strong&gt; as an independent LINE Bot server, we updated the &lt;a href="https://github.com/kkdai/line-card-assistant/blob/main/Dockerfile" rel="noopener noreferrer"&gt;&lt;code&gt;Dockerfile&lt;/code&gt;&lt;/a&gt; to compile two static executables in a multi-stage build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Build the Agent Runtime / ADK WebUI / A2A entrypoint binary (root main.go)&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;linux &lt;span class="nv"&gt;GOARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;amd64 go build &lt;span class="nt"&gt;-ldflags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-s -w"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /out/agent .

&lt;span class="c"&gt;# Also build the standalone LINE Bot Webhook server binary (cmd/server/main.go)&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;linux &lt;span class="nv"&gt;GOARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;amd64 go build &lt;span class="nt"&gt;-ldflags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-s -w"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /out/line-card-assistant ./cmd/server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;  The default &lt;code&gt;ENTRYPOINT ["/agent"]&lt;/code&gt; starts the Agent Runtime standard server (listening on &lt;code&gt;0.0.0.0:8080&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;  If it needs to run as a monolithic LINE Webhook Server on Cloud Run, simply specify the Container Command as &lt;code&gt;/bin/line-card-assistant&lt;/code&gt;!&lt;/li&gt;
&lt;/ul&gt;




&lt;h1&gt;
  
  
  4. Summary and Teaser for the Next Step
&lt;/h1&gt;

&lt;p&gt;Through the Phase 1 transformation (&lt;a href="https://github.com/kkdai/line-card-assistant/pull/10" rel="noopener noreferrer"&gt;PR #10&lt;/a&gt;), our &lt;strong&gt;LINE AI Business Card Assistant (&lt;code&gt;line-card-assistant&lt;/code&gt;)&lt;/strong&gt; has officially evolved from a simple LINE Bot application into an &lt;strong&gt;&lt;code&gt;adk_go&lt;/code&gt; Agent&lt;/strong&gt; that complies with Google Cloud Agent Platform standards:&lt;/p&gt;

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

&lt;p&gt;In the next article of this series, we will enter &lt;strong&gt;Phase 2 &amp;amp; Phase 3&lt;/strong&gt;, 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!&lt;/p&gt;




&lt;h2&gt;
  
  
  Reference Resources and Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;First article in the series&lt;/strong&gt;: &lt;a href="https://dev.to/2026/10/16/adk-graph-workflow-go/"&gt;[Agent Platform in Practice (1)] Building a LINE AI Business Card Assistant with Go and Google ADK 2.0 Graph Workflow: From Fan-Out Parallel Retrieval to HITL Pause Loop Correction&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Open source code and Roadmap for this project&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;  GitHub Repository: &lt;a href="https://github.com/kkdai/line-card-assistant" rel="noopener noreferrer"&gt;kkdai/line-card-assistant&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  Agent Runtime Migration Roadmap: &lt;a href="https://github.com/kkdai/line-card-assistant/issues/9" rel="noopener noreferrer"&gt;Issue #9&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  Phase 1 Implementation Pull Request: &lt;a href="https://github.com/kkdai/line-card-assistant/pull/10" rel="noopener noreferrer"&gt;PR #10: feat(phase1): scaffold adk_go Agent Runtime project structure and app.NewRootAgent&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Google &lt;code&gt;agents-cli&lt;/code&gt; and Agent Platform official documentation&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://google.github.io/agents-cli/guide/getting-started/" rel="noopener noreferrer"&gt;Getting Started - agents-cli&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://google.github.io/agents-cli/guide/lifecycle/" rel="noopener noreferrer"&gt;The Lifecycle - agents-cli&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Google ADK Graph Workflow official documentation&lt;/strong&gt;:

&lt;ul&gt;
&lt;li&gt;  &lt;a href="https://cloud.google.com/blog/topics/developers-practitioners/graph-workflows-in-adk-everything-you-need-to-know" rel="noopener noreferrer"&gt;Graph Workflows in ADK: Everything You Need to Know (Google Cloud Blog)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;a href="https://adk.dev/graphs/" rel="noopener noreferrer"&gt;Graph-based agent workflows (adk.dev/graphs)&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>architecture</category>
      <category>go</category>
    </item>
  </channel>
</rss>
