Building a Gemini Agent in Go: Raw SDK, Genkit and ADK Compared
One retro-game appraiser agent built three ways in Go, with the GenAI SDK, Genkit and Agent Development Kit, plus how to run each on Cloud Run or Google's agent platform.
This article is published in English.
Building AI-enabled applications and autonomous agents in Go can happen at three different levels of abstraction. This walk-through takes apart the mechanics of an agent, settles on one concrete use case, a Retro Game Appraiser, and then builds it three times:
- A low-level agent loop written directly against the Go GenAI SDK.
- A structured, flow-oriented pipeline built with Genkit.
- A modular multi-agent system with sessions, built on Google's Agent Development Kit (ADK).
It ends with the runtimes available for deploying Go agents to the cloud in a dependable way.
What makes something an agent
The term gets used casually, yet in AI engineering it has a fairly exact architectural meaning: an autonomous system with three parts: an LLM, a set of tools such as callable functions or APIs, and a harness that executes everything within a feedback loop.
A model without tools is just a text generator. It can only answer from its fixed training weights or from whatever sits in the prompt. Giving it agency, meaning the ability to inspect external state, test hypotheses and change things in the real world, requires connecting it to executable tools.
One architectural detail is easy to overlook. The model never runs code or calls external APIs itself. The local harness, here a Go program, sits in the middle. When the model decides it needs outside information or an action, it emits a structured tool-call request. The harness runs the matching Go function, captures the output and feeds the result back into the next model turn. The model then looks at the new context and either calls another tool or writes its final answer.
Not every tool runs locally. Gemini offers built-in tools such as Google Search grounding, which execute server-side. Once declared in the request configuration, the API resolves search queries and attaches grounding metadata to the response with no local round trip. For custom domain logic, though, the Go harness owns everything: declaring the schema, dispatching arguments and returning serialised results.
The use case: a Retro Game Appraiser
Anyone who collects retro video games knows a familiar pain. At a retro market it's easy to buy a copy of a beloved game and discover at home that it's already on the shelf. On top of that, the hobby is known for volatile prices, several condition tiers (loose cartridge, complete in box or CIB, factory sealed) and frequent counterfeit listings.
Checking an asking price with Gemini in real time while standing at a stall turns out to be surprisingly useful. Plenty of buyers are happy to pay a fair premium for the in-person experience and for supporting local sellers, but nobody wants to be scammed or to overpay for a fake cartridge.
The appraiser agent covers both sides: it cross-references the personal inventory and it judges fair market value against live web data.
How a collector uses it
The collector types ordinary questions, such as:
- A query asking whether a given title, say Chrono Trigger, is already in the collection.
- A mint complete-in-box EarthBound for SNES spotted at $350, with the question of whether it is already owned and whether the price beats current market values.
- A request for the price paid for Castlevania: Symphony of the Night, and whether its market value has risen since.
Tool contracts
To answer without inventing inventory or prices, the agent leans on two sources of truth:
search_catalogis a local Go function that queries the collector's own database. It looks up keywords in titles and platforms, then reports ownership, condition, acquisition date and purchase price.google_searchis a server-side grounding tool. It searches marketplaces and auction trackers for current fair values, recent verified sales and condition benchmarks.
Reasoning strategy
For a possible purchase, the agent works through a fixed sequence. It checks the local catalog first, to see whether the game is owned and in what condition. It then searches the web for current prices on that platform and condition tier. Finally it combines the findings into an appraisal: asking price against market average, upgrade opportunities such as swapping a loose cartridge for a boxed copy, and a clear recommendation.
Version one: the Go GenAI SDK
Building directly on the SDK (google.golang.org/genai) is the lowest abstraction level, mapping one-to-one onto the Gemini API's wire protocol. With no framework between your code and the API, you control tool dispatch, conversation history and the loop's stopping criteria yourself.
It works for small projects and utility scripts, for anyone studying how function calling and grounding really work, and for any case with highly specific loop requirements that standard frameworks don't cover. The result compiles into one self-contained Go binary with no framework dependencies, so it can be deployed on ordinary web or API hosting such as Google Cloud Run, Kubernetes or a plain virtual machine.
The complete, runnable implementation:
package main
import (
"bufio"
"context"
"fmt"
"log"
"os"
"os/signal"
"strings"
"syscall"
"google.golang.org/genai"
)
// GameItem represents a collectible item in the user's personal inventory.
type GameItem struct {
Title string `json:"title"`
Platform string `json:"platform"`
Year int `json:"year"`
Condition string `json:"condition"` // e.g. "Loose Cartridge", "CIB (Complete in Box)", "Mint"
PricePaid float64 `json:"price_paid"`
Notes string `json:"notes"`
}
// localCatalog simulates an inventory database for retro games.
var localCatalog = []GameItem{
{
Title: "Chrono Trigger",
Platform: "Super Nintendo (SNES)",
Year: 1995,
Condition: "CIB (Complete in Box)",
PricePaid: 210.00,
Notes: "Includes original map and registration card.",
},
{
Title: "EarthBound",
Platform: "Super Nintendo (SNES)",
Year: 1994,
Condition: "Loose Cartridge",
PricePaid: 180.00,
Notes: "Authentic board verified; label in excellent shape.",
},
{
Title: "Castlevania: Symphony of the Night",
Platform: "Sony PlayStation",
Year: 1997,
Condition: "CIB (Black Label)",
PricePaid: 135.00,
Notes: "Original soundtrack disc included.",
},
}
// searchCatalogTool searches the local collection for matching games.
func searchCatalogTool(args map[string]any) map[string]any {
query, _ := args["query"].(string)
queryLower := strings.ToLower(strings.TrimSpace(query))
var matches []GameItem
for _, item := range localCatalog {
if strings.Contains(strings.ToLower(item.Title), queryLower) ||
strings.Contains(strings.ToLower(item.Platform), queryLower) {
matches = append(matches, item)
}
}
if len(matches) == 0 {
return map[string]any{
"found": false,
"message": fmt.Sprintf("No items matching %q found in your collection.", query),
}
}
return map[string]any{
"found": true,
"count": len(matches),
"results": matches,
}
}
func main() {
ctx := context.Background()
// Initialise GenAI client for Gemini Enterprise
client, err := genai.NewClient(ctx, &genai.ClientConfig{
Project: os.Getenv("GOOGLE_CLOUD_PROJECT"),
Location: "global",
Backend: genai.BackendEnterprise,
})
if err != nil {
log.Fatalf("failed to create client: %v", err)
}
// 1. Declare custom function schema for collection lookup
catalogToolDecl := &genai.FunctionDeclaration{
Name: "search_catalog",
Description: "Search the collector's personal inventory for owned games by title or platform.",
Parameters: &genai.Schema{
Type: genai.TypeObject,
Properties: map[string]*genai.Schema{
"query": {
Type: genai.TypeString,
Description: "Game title or platform to search (e.g. 'EarthBound', 'SNES').",
},
},
Required: []string{"query"},
},
}
// 2. Configure model tools: custom function declaration + Google Search grounding
config := &genai.GenerateContentConfig{
SystemInstruction: &genai.Content{
Parts: []*genai.Part{
{Text: "You are an expert Retro Game Appraiser. When evaluating purchases, check the user's " +
"collection catalog first to see if they already own the item, then check current market " +
"prices using Google Search to evaluate whether the deal is fair, overpriced, or a bargain."},
},
},
Tools: []*genai.Tool{
{
FunctionDeclarations: []*genai.FunctionDeclaration{catalogToolDecl},
},
{
GoogleSearch: &genai.GoogleSearch{},
},
},
}
// 3. Graceful shutdown on Ctrl+C (SIGINT) or SIGTERM
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigChan
fmt.Println("\nGoodbye!")
os.Exit(0)
}()
model := "gemini-3.8-flash"
var contents []*genai.Content
fmt.Println("Retro Game Appraiser (SDK Agent)")
fmt.Println("Type your question below, or 'exit' (Ctrl+C / Ctrl+D) to quit.")
fmt.Println("-----------------------------------------------------------------")
scanner := bufio.NewScanner(os.Stdin)
for {
fmt.Print("\nUser: ")
if !scanner.Scan() {
fmt.Println("\nGoodbye!")
break
}
input := strings.TrimSpace(scanner.Text())
if input == "" {
continue
}
if strings.EqualFold(input, "exit") {
fmt.Println("Goodbye!")
break
}
contents = append(contents, &genai.Content{
Role: "user",
Parts: []*genai.Part{genai.NewPartFromText(input)},
})
// 4. The Agent Loop: model generation -> tool dispatch -> feedback -> until final answer
for {
resp, err := client.Models.GenerateContent(ctx, model, contents, config)
if err != nil {
log.Printf("error generating content: %v", err)
break
}
if len(resp.Candidates) == 0 || resp.Candidates[0].Content == nil {
log.Println("received empty response candidate from model")
break
}
// Append the model's response to the conversation history
modelContent := resp.Candidates[0].Content
contents = append(contents, modelContent)
// Check if the model requested any client-side tool executions
funcCalls := resp.FunctionCalls()
if len(funcCalls) == 0 {
fmt.Printf("\nAppraiser: %s\n", resp.Text())
break
}
// Execute each requested tool and prepare response parts
var responseParts []*genai.Part
for _, call := range funcCalls {
fmt.Printf("[Harness] Executing tool: %s(args=%v)\n", call.Name, call.Args)
var result map[string]any
switch call.Name {
case "search_catalog":
result = searchCatalogTool(call.Args)
default:
result = map[string]any{"error": fmt.Sprintf("unsupported tool: %s", call.Name)}
}
responseParts = append(responseParts, genai.NewPartFromFunctionResponse(call.Name, result))
}
// Return tool execution results as a user turn
contents = append(contents, &genai.Content{
Role: "user",
Parts: responseParts,
})
}
}
}
To run it, configure a Google Cloud project and sign in with Application Default Credentials using gcloud auth application-default login:
export GOOGLE_CLOUD_PROJECT="your-gcp-project-id"
go run main.go
An interactive chat opens in the terminal. The conversation can span several turns and ends when you type exit, press Ctrl+C or send EOF with Ctrl+D:
Retro Game Appraiser (SDK Agent)
Type your question below, or 'exit' (Ctrl+C / Ctrl+D) to quit.
-----------------------------------------------------------------
User: I found a copy of EarthBound for SNES in mint Complete-in-Box (CIB) condition for $350. Do I already own it, and is $350 a good deal compared to current market prices?
[Harness] Executing tool: search_catalog(args=map[query:EarthBound])
Appraiser: Here is your collection check and appraisal for **EarthBound (SNES)**:
1. **Current Collection Status**:
- You currently own **EarthBound** on Super Nintendo as a **Loose Cartridge**, purchased for **$180.00**.
2. **Market Price Appraisal**:
- Verified market sales for an authentic, **Complete-in-Box (CIB)** copy of EarthBound typically range between **$1,200.00 and $1,500.00** depending on the condition of the box, tray, and original player's guide.
3. **Recommendation**:
- At **$350.00**, a genuine Mint CIB copy is an **exceptional deal** (more than 70% below prevailing market value).
- **Caution**: Because EarthBound is one of the most heavily counterfeited SNES titles, inspect the box printing, registration card, and PCB board carefully before completing the transaction. If verified authentic, this is an outstanding opportunity to upgrade your loose copy to CIB.
User: exit
Goodbye!
The SDK version makes the control flow explicit and easy to follow. With strong models and large token windows, writing the plumbing, tool dispatch loops and supporting scaffolding yourself is deceptively easy.
That ease is a trap, though. Every line of home-grown framework code becomes a permanent maintenance cost and a possible home for subtle bugs. Writing an agent framework from scratch is at best a distraction from shipping features and at worst a pile of technical debt that pulls effort away from the actual business goal. The best code is no code, and the next best is whatever solves the problem with the least custom code.
Why move up to a framework
The raw SDK loop is hard to beat for learning low-level mechanics or for bespoke loops, yet production applications want a higher level of abstraction, for several reasons:
- Automatic schema reflection. Writing JSON schemas by hand with
&genai.Schema{...}is tedious and error-prone. Production frameworks derive schemas from native Go structs and doc comments. - Observability and tracing. Production use calls for OpenTelemetry traces, per-tool latency breakdowns and token consumption metrics out of the box.
- Prompt management. Prompts hardcoded as Go strings get in the way of collaborating with prompt engineers and rule out versioning templates separately from binary releases.
- Session persistence and state. Keeping multi-turn histories across stateless HTTP requests needs thread-safe, decoupled storage.
- Model portability. The Go SDK is Gemini-specific, whereas frameworks let you change providers or try local models without touching business logic.
The Go ecosystem has two main open-source frameworks for these needs: Genkit and the Agent Development Kit.
Version two: Genkit
Genkit sits at a medium level of abstraction and brings software-engineering discipline and structured observability to AI applications. Everything is organised around flows, which are strongly typed, observable pipelines, and tools, which are type-safe Go functions with automatic schema generation.
It suits one-off generation jobs such as command-line tools, batch processing and webhook endpoints, plus deterministic workflows and single-domain agents like a multimodal photo-restoration pipeline. It supports several model providers through plugins. Since a Genkit app is simply a standard Go HTTP server, it runs on any backend platform, with Google Cloud Run the preferred target for easy container hosting and automatic scaling.
The appraiser written with Genkit Go:
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/firebase/genkit/go/ai"
"github.com/firebase/genkit/go/genkit"
"github.com/firebase/genkit/go/plugins/googlegenai"
"google.golang.org/genai"
)
// GameItem represents a collectible item in the user's personal inventory.
type GameItem struct {
Title string `json:"title"`
Platform string `json:"platform"`
Year int `json:"year"`
Condition string `json:"condition"`
PricePaid float64 `json:"price_paid"`
Notes string `json:"notes"`
}
// localCatalog simulates an inventory database for retro games.
var localCatalog = []GameItem{
{
Title: "Chrono Trigger",
Platform: "Super Nintendo (SNES)",
Year: 1995,
Condition: "CIB (Complete in Box)",
PricePaid: 210.00,
Notes: "Includes original map and registration card.",
},
{
Title: "EarthBound",
Platform: "Super Nintendo (SNES)",
Year: 1994,
Condition: "Loose Cartridge",
PricePaid: 180.00,
Notes: "Authentic board verified; label in excellent shape.",
},
{
Title: "Castlevania: Symphony of the Night",
Platform: "Sony PlayStation",
Year: 1997,
Condition: "CIB (Black Label)",
PricePaid: 135.00,
Notes: "Original soundtrack disc included.",
},
}
type CatalogRequest struct {
Query string `json:"query" jsonschema:"description=The game title or platform to search in the inventory"`
}
type CatalogResponse struct {
Found bool `json:"found"`
Message string `json:"message,omitempty"`
Count int `json:"count,omitempty"`
Results []GameItem `json:"results,omitempty"`
}
type AppraiserRequest struct {
Prompt string `json:"prompt" jsonschema:"description=The collector's question or purchase offer to evaluate"`
}
type AppraiserResponse struct {
Appraisal string `json:"appraisal"`
}
func main() {
ctx := context.Background()
// 1. Initialise Genkit with Vertex AI plugin
g := genkit.Init(ctx,
genkit.WithPlugins(&googlegenai.VertexAI{
ProjectID: os.Getenv("GOOGLE_CLOUD_PROJECT"),
Location: "global",
}),
)
// 2. Define strongly-typed tool with automatic schema generation
catalogTool := genkit.DefineTool(
g,
"search_catalog",
"Search the collector's personal inventory for owned games by title or platform.",
func(ctx *ai.ToolContext, req CatalogRequest) (CatalogResponse, error) {
queryLower := strings.ToLower(strings.TrimSpace(req.Query))
queryWords := strings.Fields(queryLower)
var matches []GameItem
for _, item := range localCatalog {
itemText := strings.ToLower(item.Title + " " + item.Platform)
allMatch := true
for _, word := range queryWords {
if !strings.Contains(itemText, word) {
allMatch = false
break
}
}
if allMatch {
matches = append(matches, item)
}
}
if len(matches) == 0 {
return CatalogResponse{
Found: false,
Message: fmt.Sprintf("No items matching %q found in personal collection.", req.Query),
}, nil
}
return CatalogResponse{
Found: true,
Count: len(matches),
Results: matches,
}, nil
},
)
// 3. Define structured appraisal flow with typed request and response
appraiserFlow := genkit.DefineFlow(
g,
"appraise_game",
func(ctx context.Context, req AppraiserRequest) (AppraiserResponse, error) {
resp, err := genkit.Generate(ctx, g,
ai.WithModelName("vertexai/gemini-3.8-flash"),
ai.WithSystem(
"You are an expert Retro Game Appraiser. Assist collectors by evaluating prospective purchases, "+
"cross-referencing their personal inventory, and assessing fair market valuations. "+
"Always search the collection catalog using search_catalog before providing purchase recommendations.",
),
ai.WithConfig(&genai.GenerateContentConfig{
ThinkingConfig: &genai.ThinkingConfig{IncludeThoughts: true},
Tools: []*genai.Tool{
{
GoogleSearch: &genai.GoogleSearch{},
},
},
}),
ai.WithPrompt(req.Prompt),
ai.WithTools(catalogTool),
)
if err != nil {
return AppraiserResponse{}, fmt.Errorf("appraisal generation failed: %w", err)
}
return AppraiserResponse{Appraisal: resp.Text()}, nil
},
)
// 4. Mount flow directly using Genkit's built-in HTTP handler
mux := http.NewServeMux()
mux.Handle("POST /api/appraise", genkit.Handler(appraiserFlow))
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
server := &http.Server{
Addr: ":" + port,
Handler: mux,
}
// Graceful shutdown on Ctrl+C (SIGINT) or SIGTERM
serverCtx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
log.Printf("Retro Game Appraiser (Genkit) listening on :%s", port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("server failed: %v", err)
}
}()
<-serverCtx.Done()
log.Println("\nShutting down server gracefully...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Fatalf("server forced shutdown: %v", err)
}
log.Println("Server exited cleanly.")
}
Running the flow
Start the Genkit server with your Google Cloud project configured:
export GOOGLE_CLOUD_PROJECT="your-gcp-project-id"
export PORT=8080
go run main.go
From another terminal, send an appraisal request:
curl -s -X POST http://localhost:8080/api/appraise \
-H "Content-Type: application/json" \
-d '{"data": {"prompt": "I found a copy of EarthBound for SNES for $350. Do I own it, and is it a good deal?"}}' | jq .
The flow executes, calls the catalog tool and returns a structured appraisal payload:
{
"appraisal": "### 1. Catalog Check\n**Yes, you already own it.**\n* **Title:** *EarthBound* (SNES, 1994)\n* **Status in Collection:** Loose Cartridge\n* **Condition/Notes:** Authentic board verified; label in excellent shape.\n* **Price Paid:** $180\n\n---\n\n### 2. Market Appraisal & Deal Analysis\n* **Loose Cartridge:** The current going market rate for an authentic loose copy ranges between **$320 and $380**. At **$350**, it is priced right at **fair market value**—neither an overpriced listing nor a significant bargain.\n* **Complete in Box (CIB) / Boxed with Guide:** If this listing happens to include the original big box and strategy guide with scratch-and-sniff cards, $350 would be an extraordinary steal (CIB copies regularly sell for **$1,500–$2,500+**).\n\n---\n\n### 3. Recommendation\n* **Pass (if Loose):** Since you already have an authentic copy in excellent condition, paying retail market price ($350) for a duplicate loose cart does not offer strong value or upside.\n* **Buy immediately (if Complete/Boxed):** Only pull the trigger if it includes the original packaging or represents a major condition upgrade/variant.\n* **Buyer Beware:** If you do ever consider another copy, always inspect the PCB (printed circuit board) screws and chips, as *EarthBound* is one of the most frequently counterfeited games on the SNES."
}
Genkit removes the hand-written dispatch loop. It unmarshals arguments into native Go structs, runs the tool, returns the payload to the model and emits OpenTelemetry spans for each step automatically. It's the sweet spot whenever the workload follows a structured, deterministic flow.
Version three: the Agent Development Kit
Where Genkit targets structured application pipelines, Google's ADK works at a high level of abstraction. It was designed for autonomous conversational agents, multi-agent orchestration and complex architectures that need long-term memory and enterprise RAG.
The kit also standardises agent lifecycles, subagent delegation and the agent-to-agent (A2A) communication protocol, and it can use multiple model providers through adapters, as Genkit does.
For deployment, the Gemini Enterprise Agent Platform is the preferred target when you'd like Google to run session storage, enterprise connectors and memory so you don't have to code your own database layers. Teams running their own state stores or containerised microservices are better served by Google Cloud Run.
The appraiser on ADK v2:
package main
import (
"context"
"fmt"
"log"
"os"
"strings"
"google.golang.org/genai"
"google.golang.org/adk/v2/agent"
"google.golang.org/adk/v2/agent/llmagent"
"google.golang.org/adk/v2/cmd/launcher"
"google.golang.org/adk/v2/cmd/launcher/full"
"google.golang.org/adk/v2/model/gemini"
"google.golang.org/adk/v2/tool"
"google.golang.org/adk/v2/tool/functiontool"
"google.golang.org/adk/v2/tool/geminitool"
)
// GameItem represents a collectible item in the user's personal inventory.
type GameItem struct {
Title string `json:"title"`
Platform string `json:"platform"`
Year int `json:"year"`
Condition string `json:"condition"`
PricePaid float64 `json:"price_paid"`
Notes string `json:"notes"`
}
// localCatalog simulates an inventory database for retro games.
var localCatalog = []GameItem{
{
Title: "Chrono Trigger",
Platform: "Super Nintendo (SNES)",
Year: 1995,
Condition: "CIB (Complete in Box)",
PricePaid: 210.00,
Notes: "Includes original map and registration card.",
},
{
Title: "EarthBound",
Platform: "Super Nintendo (SNES)",
Year: 1994,
Condition: "Loose Cartridge",
PricePaid: 180.00,
Notes: "Authentic board verified; label in excellent shape.",
},
{
Title: "Castlevania: Symphony of the Night",
Platform: "Sony PlayStation",
Year: 1997,
Condition: "CIB (Black Label)",
PricePaid: 135.00,
Notes: "Original soundtrack disc included.",
},
}
type CatalogRequest struct {
Query string `json:"query" jsonschema:"The game title or platform to search in the inventory."`
}
type CatalogResponse struct {
Found bool `json:"found"`
Message string `json:"message,omitempty"`
Count int `json:"count,omitempty"`
Results []GameItem `json:"results,omitempty"`
}
func main() {
ctx := context.Background()
// 1. Initialise Gemini Model adapter for Gemini Enterprise
model, err := gemini.NewModel(ctx, "gemini-3.8-flash", &genai.ClientConfig{
Project: os.Getenv("GOOGLE_CLOUD_PROJECT"),
Location: "global",
Backend: genai.BackendEnterprise,
})
if err != nil {
log.Fatalf("failed to create Gemini model: %v", err)
}
// 2. Wrap collection lookup as an ADK Function Tool
catalogTool, err := functiontool.New(functiontool.Config{
Name: "search_catalog",
Description: "Search the collector's personal inventory for owned games by title or platform.",
}, func(ctx agent.Context, req CatalogRequest) (CatalogResponse, error) {
queryLower := strings.ToLower(strings.TrimSpace(req.Query))
var matches []GameItem
for _, item := range localCatalog {
if strings.Contains(strings.ToLower(item.Title), queryLower) ||
strings.Contains(strings.ToLower(item.Platform), queryLower) {
matches = append(matches, item)
}
}
if len(matches) == 0 {
return CatalogResponse{
Found: false,
Message: fmt.Sprintf("No items matching %q found in personal collection.", req.Query),
}, nil
}
return CatalogResponse{
Found: true,
Count: len(matches),
Results: matches,
}, nil
})
if err != nil {
log.Fatalf("failed to create catalog tool: %v", err)
}
// 3. Define autonomous LLM Agent
appraiserAgent, err := llmagent.New(llmagent.Config{
Name: "retro_game_appraiser",
Model: model,
Description: "Expert appraiser that analyzes retro video game purchases and collection inventory.",
Instruction: "You are an expert Retro Game Appraiser. Assist collectors by verifying collection " +
"status with search_catalog, assessing condition variants, and offering objective buying recommendations.",
Tools: []tool.Tool{
catalogTool,
geminitool.GoogleSearch{},
},
})
if err != nil {
log.Fatalf("failed to create appraiser agent: %v", err)
}
// 4. Configure launcher and execute
config := &launcher.Config{
AgentLoader: agent.NewSingleLoader(appraiserAgent),
}
l := full.NewLauncher()
if err = l.Execute(ctx, config, os.Args[1:]); err != nil {
log.Fatalf("run failed: %v\n\n%s", err, l.CommandLineSyntax())
}
}
Running the ADK agent
ADK ships a universal launcher, full.NewLauncher(), so there's no need to build HTTP routing, JSON marshalling or session boilerplate by hand. Command-line arguments passed to the launcher choose the interaction mode without any change to the application code.
To start an interactive terminal chat:
export GOOGLE_CLOUD_PROJECT="your-gcp-project-id"
go run main.go
The launcher starts in console mode and the appraiser can be questioned directly:
User: Do I have Chrono Trigger in my collection?
Agent: Yes, you have Chrono Trigger in your collection! Here are the details from your inventory:
* Title: Chrono Trigger
* Platform: Super Nintendo (SNES)
* Release Year: 1995
* Condition: CIB (Complete in Box)
* Price Paid: $210.00
* Notes: Includes original map and registration card.
User: What did I pay for it?
Agent: You paid $210.00 for it.
ADK manages conversational state and session continuity itself, so follow-up questions keep the full context of earlier turns.
As an alternative, there is a built-in developer web UI served together with a REST API:
go run main.go web webui api
Opening http://localhost:8080 gives an interactive chat with real-time streaming responses, session history and step-by-step visibility into tool runs, covering both the custom search_catalog function tool and the native GoogleSearch grounding.
ADK cleanly separates the agent definition (llmagent), the tool bindings (functiontool and built-ins such as geminitool.GoogleSearch) and the execution environment (launcher and runner). That makes it the framework of choice for complex multi-agent hierarchies and conversational assistants.
Where Go agents run
Writing the agent in Go is only half the journey. Afterwards it needs an execution environment that copes with long-lived streaming responses, background tool calls, secure credentials and traffic spikes without operational pain. Go compiles to single self-contained binaries with almost no runtime overhead, so agents written in it are fast and cheap to host. Depending on framework and state requirements, there are two main targets.
Cloud Run, the general-purpose backend
Google Cloud Run is the prime target for containerised Go agents, whether they're built with the raw SDK, Genkit flows or ADK runners. Multi-stage Docker builds can compile the agent into a minimal scratch or distroless image with no runtime dependencies. With no heavy interpreter or virtual machine to start, the service scales from zero quickly, avoiding the cold-start overhead that burdens heavier runtimes.
Cloud Run's main advantages for Go agents:
- Scale to zero with fast concurrency. You pay only for the CPU milliseconds used while the agent is actually working or handling tool responses, and Go's lightweight goroutines let one container instance process hundreds of agent turns concurrently with a small memory footprint.
- Long request timeouts. Requests default to a 5-minute limit, which can be extended as far as 60 minutes (3,600 seconds). That leaves multi-step reasoning loops, deep research jobs and iterative subagent swarms enough time to finish.
- Bidirectional streaming and WebSockets. HTTP/2 chunked transfer, Server-Sent Events and WebSockets are supported natively. For voice-to-voice or real-time multimodal agents on the Gemini Live API, WebSockets give continuous two-way streaming between client and server.
- Session affinity. An agent that keeps temporary in-memory caches or local context between turns can enable
--session-affinity, by client IP or cookie, to send follow-ups to the same container instance. - Stateless resilience. In production, state should live in external managed datastores such as Firestore, Redis or Cloud SQL. After a network drop mid-turn, the agent can resume through its interaction ID without losing context.
- Secretless workload identity. No hardcoded API keys or credentials are needed, because the agent authenticates to the Gemini Developer API, Vertex AI and Cloud Storage through the ambient Cloud Run service account.
Gemini Enterprise Agent Platform
Conversational ADK agents and enterprise solutions that must keep sessions, hold long-term memory and ground answers in company knowledge can use the Gemini Enterprise Agent Platform, described as the successor to Vertex AI Agent Engine, which offers a fully managed, serverless execution environment. Instead of provisioning databases and writing custom session storage adapters, teams get managed infrastructure with these features:
- Decoupled session persistence. Long-term conversation storage (
SessionServicein ADK) is separated from the ephemeral streaming execution loop (LiveSession, with automatic session-resumption handles across reconnects). - Enterprise grounding and vector search. Ready-made connectors reach enterprise knowledge bases such as Google Drive, BigQuery and intranet repositories, alongside Vertex AI Vector Search indexes, including storage-optimised tiers, for fast semantic retrieval.
- Sandboxed code execution. Isolated environments let agents write and run code, for example data analysis or Python and Go scripts, without exposing the host infrastructure.
- Agent-to-agent protocols. Standard communication lets independent enterprise agents discover capabilities, negotiate schemas and delegate tasks across organisational boundaries.
- Security, agent identity and Model Armor. Fine-grained agent IAM permissions, VPC Service Controls perimeters and Model Armor runtime protection inspect inputs and outputs for prompt injection, data exfiltration and policy violations.
Choosing between the three
For learning how function calling and grounding work, or for a small utility, the raw SDK loop is enough. For a structured, deterministic pipeline or a single-domain agent exposed as an HTTP endpoint, Genkit fits. For conversational assistants, multi-agent hierarchies, delegation and long-term memory, ADK is the better foundation. Cloud Run suits all three, while the Gemini Enterprise Agent Platform is the natural home for ADK agents that need managed sessions, memory and enterprise grounding.