Phase 1 delivers the complete durability and storage layer:
- WAL with crash recovery: Append-only journal with BLAKE3 checksums,
fsync guarantees, and proper seek-to-EOF on reopen
- Storage engine: sled-backed KVStore with scan_prefix for range queries
- Content-addressed storage: H:{hash}, V:{hash}, E:{hash} key patterns
- Ingestor: Background worker tailing WAL, writing to KV with 8-byte
aligned record headers for rkyv zero-copy deserialization
- Comprehensive tests: 31 tests covering crash recovery, round-trips,
and multi-cycle durability
New crates: stemedb-wal, stemedb-storage, stemedb-ingest
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
20 KiB
20 KiB
ADK-Go Developer Reference Guide
A practical guide for building AI agents with Google's Agent Development Kit for Go.
Quick Start
Prerequisites
- Go 1.24.4 or later
- Google API key (AI Studio) or Google Cloud project (Vertex AI)
Installation
# Create new project
mkdir my-agent && cd my-agent
go mod init example.com/my-agent
# Install ADK
go get google.golang.org/adk
Environment Setup
# Option 1: AI Studio (API Key)
export GOOGLE_API_KEY="your-api-key"
# Option 2: Vertex AI (Application Default Credentials)
export GOOGLE_CLOUD_PROJECT="your-project-id"
export GOOGLE_CLOUD_LOCATION="us-central1"
gcloud auth application-default login
Minimal Agent
package main
import (
"context"
"log"
"os"
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/cmd/launcher/adk"
"google.golang.org/adk/cmd/launcher/full"
"google.golang.org/adk/model/gemini"
"google.golang.org/adk/server/restapi/services"
"google.golang.org/genai"
)
func main() {
ctx := context.Background()
// Create model
model, err := gemini.NewModel(ctx, "gemini-3-flash-preview", &genai.ClientConfig{
APIKey: os.Getenv("GOOGLE_API_KEY"),
})
if err != nil {
log.Fatal(err)
}
// Create agent
agent, err := llmagent.New(llmagent.Config{
Name: "assistant",
Model: model,
Description: "A helpful assistant",
Instruction: "You are a helpful assistant. Be concise and accurate.",
})
if err != nil {
log.Fatal(err)
}
// Launch
l := full.NewLauncher()
cfg := &adk.Config{AgentLoader: services.NewSingleAgentLoader(agent)}
if err := l.Execute(ctx, cfg, os.Args[1:]); err != nil {
log.Fatal(err)
}
}
Running Modes
# Console mode (interactive terminal)
go run main.go
# Web UI + API
go run main.go web api webui
# Opens at http://localhost:8080
# Production (API + A2A protocol)
go run main.go web api a2a
Core Imports
import (
// Agents
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/llmagent"
"google.golang.org/adk/agent/sequentialagent"
"google.golang.org/adk/agent/parallelagent"
"google.golang.org/adk/agent/loopagent"
// Models
"google.golang.org/adk/model/gemini"
"google.golang.org/genai"
// Tools
"google.golang.org/adk/tool"
"google.golang.org/adk/tool/functiontool"
"google.golang.org/adk/tool/geminitool"
"google.golang.org/adk/tool/mcptool"
// Session & Memory
"google.golang.org/adk/session"
"google.golang.org/adk/memory"
// Launcher
"google.golang.org/adk/cmd/launcher/adk"
"google.golang.org/adk/cmd/launcher/full"
"google.golang.org/adk/server/restapi/services"
)
Creating Agents
LLM Agent Configuration
agent, err := llmagent.New(llmagent.Config{
// Required
Name: "my_agent",
Model: model,
// Recommended
Description: "What this agent does (used for routing)",
Instruction: "System prompt with behavior guidelines",
// Optional
Tools: []tool.Tool{...}, // Available tools
SubAgents: []agent.Agent{...}, // Agents this can delegate to
OutputKey: "result", // Save output to state["result"]
})
Instruction Templates
Use curly braces to inject state values:
llmagent.Config{
Instruction: `You are a travel assistant.
User preferences: {user:preferences}
Current itinerary: {itinerary}
Budget remaining: {budget}`,
}
Available Models
// Gemini models
"gemini-3-flash-preview" // Fast, efficient
"gemini-2.5-pro" // Most capable
"gemini-3-flash-preview" // Previous generation
"gemini-1.5-pro" // Long context
Tools
Built-in Tools
import "google.golang.org/adk/tool/geminitool"
agent, _ := llmagent.New(llmagent.Config{
Tools: []tool.Tool{
geminitool.GoogleSearch{}, // Web search
geminitool.CodeExecution{}, // Run code in sandbox
},
})
Custom Function Tools
import "google.golang.org/adk/tool/functiontool"
// 1. Define input/output structs with JSON tags
type CalculateInput struct {
Expression string `json:"expression" jsonschema:"Math expression to evaluate"`
}
type CalculateOutput struct {
Result float64 `json:"result"`
Error string `json:"error,omitempty"`
}
// 2. Implement the function
func calculate(ctx tool.Context, input CalculateInput) CalculateOutput {
// Your logic here
return CalculateOutput{Result: 42}
}
// 3. Create the tool
calcTool, err := functiontool.New(
functiontool.Config{
Name: "calculate",
Description: "Evaluates mathematical expressions",
},
calculate,
)
// 4. Add to agent
agent, _ := llmagent.New(llmagent.Config{
Tools: []tool.Tool{calcTool},
})
Schema Tags Reference
type Input struct {
// Required field with description
Query string `json:"query" jsonschema:"Search query to execute"`
// Optional field (omitempty makes it optional)
Limit int `json:"limit,omitempty" jsonschema:"Max results (default 10)"`
// Enum values
Format string `json:"format" jsonschema:"Output format" jsonschema_enum:"json,xml,csv"`
}
Accessing Context in Tools
func myTool(ctx tool.Context, input MyInput) MyOutput {
// Access session state
userID := ctx.Session().State().Get("user:id")
// Access artifacts
artifact, err := ctx.Artifacts().Load("document.pdf")
// Standard context operations
if ctx.Err() != nil {
return MyOutput{Error: "cancelled"}
}
return MyOutput{...}
}
MCP (Model Context Protocol) Tools
import "google.golang.org/adk/tool/mcptool"
// Stdio-based MCP server
params := mcptool.StdioServerParams{
Command: "npx",
Args: []string{"-y", "@modelcontextprotocol/server-filesystem", "/path"},
}
tools, closer, err := mcptool.FromServer(ctx, params)
defer closer.Close()
// HTTP/SSE-based MCP server
sseParams := mcptool.SseServerParams{
URL: "http://localhost:8090/sse",
Timeout: 5.0,
}
tools, closer, err := mcptool.FromServer(ctx, sseParams)
Multi-Agent Patterns
Sequential Agent (Pipeline)
Executes agents in order. Each agent's output is available to the next.
import "google.golang.org/adk/agent/sequentialagent"
// Agent 1: Research
researcher, _ := llmagent.New(llmagent.Config{
Name: "researcher",
Model: model,
Instruction: "Research the given topic thoroughly.",
Tools: []tool.Tool{geminitool.GoogleSearch{}},
OutputKey: "research", // Saves to state["research"]
})
// Agent 2: Write (uses research output)
writer, _ := llmagent.New(llmagent.Config{
Name: "writer",
Model: model,
Instruction: "Write an article based on: {research}",
OutputKey: "draft",
})
// Agent 3: Edit
editor, _ := llmagent.New(llmagent.Config{
Name: "editor",
Model: model,
Instruction: "Edit and improve: {draft}",
OutputKey: "final",
})
// Combine into pipeline
pipeline, _ := sequentialagent.New(sequentialagent.Config{
AgentConfig: agent.Config{
Name: "content_pipeline",
Description: "Research, write, and edit content",
SubAgents: []agent.Agent{researcher, writer, editor},
},
})
Parallel Agent (Concurrent Execution)
Executes agents simultaneously. Each runs in an isolated branch.
import "google.golang.org/adk/agent/parallelagent"
// Create specialized agents
webSearcher, _ := llmagent.New(llmagent.Config{
Name: "web_searcher",
Model: model,
Tools: []tool.Tool{geminitool.GoogleSearch{}},
OutputKey: "web_results",
})
docAnalyzer, _ := llmagent.New(llmagent.Config{
Name: "doc_analyzer",
Model: model,
OutputKey: "doc_analysis",
})
// Run in parallel
parallel, _ := parallelagent.New(parallelagent.Config{
AgentConfig: agent.Config{
Name: "parallel_research",
SubAgents: []agent.Agent{webSearcher, docAnalyzer},
},
})
Loop Agent (Iterative Refinement)
Repeats agents until max iterations or termination condition.
import "google.golang.org/adk/agent/loopagent"
// Writer produces drafts
writer, _ := llmagent.New(llmagent.Config{
Name: "writer",
Model: model,
Instruction: "Write or improve the draft. Current: {draft}",
OutputKey: "draft",
})
// Critic evaluates and decides if done
critic, _ := llmagent.New(llmagent.Config{
Name: "critic",
Model: model,
Instruction: `Evaluate the draft: {draft}
If excellent, respond with exactly: APPROVED
Otherwise, provide specific improvement suggestions.`,
OutputKey: "feedback",
})
// Loop until approved or max iterations
refiner, _ := loopagent.New(loopagent.Config{
AgentConfig: agent.Config{
Name: "refiner",
SubAgents: []agent.Agent{writer, critic},
},
MaxIterations: 5,
})
Hierarchical Delegation
Parent agent delegates to specialized sub-agents.
// Specialist agents
mathAgent, _ := llmagent.New(llmagent.Config{
Name: "math_expert",
Description: "Handles mathematical calculations and problems",
Model: model,
})
codeAgent, _ := llmagent.New(llmagent.Config{
Name: "code_expert",
Description: "Writes and explains code",
Model: model,
Tools: []tool.Tool{geminitool.CodeExecution{}},
})
// Router agent delegates based on query type
router, _ := llmagent.New(llmagent.Config{
Name: "router",
Model: model,
Instruction: `Route queries to the appropriate specialist.
Use math_expert for calculations.
Use code_expert for programming questions.`,
SubAgents: []agent.Agent{mathAgent, codeAgent},
})
Session & State Management
State Prefixes
| Prefix | Scope | Persistence |
|---|---|---|
| (none) | App | Persists across sessions |
user: |
User | Persists for specific user |
app: |
Application | Global app state |
temp: |
Invocation | Cleared after each turn |
Reading/Writing State
// In agent instruction (template syntax)
Instruction: "User name: {user:name}, Preferences: {user:preferences}"
// In tool function
func myTool(ctx tool.Context, input Input) Output {
state := ctx.Session().State()
// Read
name := state.Get("user:name")
// Write
state.Set("user:preferences", "dark_mode=true")
// Delete
state.Delete("temp:scratch")
}
Session Services
import "google.golang.org/adk/session"
// In-memory (development)
sessionSvc := session.InMemoryService()
// Create session
sess, err := sessionSvc.Create(ctx, &session.CreateRequest{
AppName: "my_app",
UserID: "user123",
State: map[string]any{"user:name": "Alice"},
})
// Get existing session
sess, err := sessionSvc.Get(ctx, &session.GetRequest{
AppName: "my_app",
UserID: "user123",
SessionID: "session-id",
})
// List user sessions
sessions, err := sessionSvc.List(ctx, &session.ListRequest{
AppName: "my_app",
UserID: "user123",
})
Memory Service (Long-term Knowledge)
import "google.golang.org/adk/memory"
// In-memory for development
memorySvc := memory.InMemoryService()
// Store knowledge
err := memorySvc.Add(ctx, &memory.AddRequest{
AppName: "my_app",
UserID: "user123",
Content: "User prefers vegetarian restaurants",
})
// Search knowledge
results, err := memorySvc.Search(ctx, &memory.SearchRequest{
AppName: "my_app",
UserID: "user123",
Query: "food preferences",
})
Running Agents Programmatically
Using the Runner
import "google.golang.org/adk/runner"
// Create runner
r := runner.New(runner.Config{
Agent: myAgent,
SessionService: session.InMemoryService(),
MemoryService: memory.InMemoryService(),
})
// Run agent
userMsg := genai.NewContentFromText(genai.RoleUser, "Hello!")
for event, err := range r.Run(ctx, "user123", "session123", userMsg, agent.RunConfig{}) {
if err != nil {
log.Printf("Error: %v", err)
continue
}
// Process events
switch {
case event.IsFinal():
fmt.Println("Final:", event.Content())
case event.Partial:
fmt.Print(event.Text()) // Streaming output
case event.ToolCall != nil:
fmt.Printf("Calling tool: %s\n", event.ToolCall.Name)
}
}
Streaming Modes
// Streaming modes
agent.RunConfig{
StreamingMode: agent.StreamingModeNone, // Wait for complete response
StreamingMode: agent.StreamingModeSSE, // Server-sent events
}
Callbacks
Model Callbacks
agent, _ := llmagent.New(llmagent.Config{
BeforeModelCallback: func(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMRequest, error) {
// Modify request before sending to model
log.Printf("Sending to model: %v", req)
// Block certain queries
if containsForbiddenContent(req) {
return nil, errors.New("request blocked")
}
return req, nil
},
AfterModelCallback: func(ctx agent.CallbackContext, resp *model.LLMResponse) (*model.LLMResponse, error) {
// Process/modify response
log.Printf("Received: %v", resp)
return resp, nil
},
})
Tool Callbacks
agent, _ := llmagent.New(llmagent.Config{
BeforeToolCallback: func(ctx agent.CallbackContext, call *tool.Call) (*tool.Call, error) {
// Validate tool arguments
log.Printf("Tool call: %s(%v)", call.Name, call.Args)
// Block dangerous operations
if call.Name == "delete_file" {
return nil, errors.New("file deletion not allowed")
}
return call, nil
},
AfterToolCallback: func(ctx agent.CallbackContext, call *tool.Call, result *tool.Result) (*tool.Result, error) {
// Log or modify results
log.Printf("Tool result: %v", result)
return result, nil
},
})
Deployment
Local Development
# Console mode
go run main.go
# Web UI for testing
go run main.go web api webui
Cloud Run Deployment
# Build the CLI tool
go build -o adkgo ./cmd/adkgo
# Deploy
./adkgo deploy cloudrun \
-p $GOOGLE_CLOUD_PROJECT \
-r us-central1 \
-s my-agent-service \
-e ./main.go \
--a2a --api
Manual Docker Deployment
# Dockerfile
FROM golang:1.24-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o agent main.go
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /app
COPY --from=builder /app/agent .
EXPOSE 8080
ENV PORT=8080
CMD ["./agent", "web", "api", "a2a"]
# Build and run
docker build -t my-agent .
docker run -p 8080:8080 -e GOOGLE_API_KEY=$GOOGLE_API_KEY my-agent
Environment Variables
| Variable | Purpose |
|---|---|
GOOGLE_API_KEY |
AI Studio API key |
GOOGLE_CLOUD_PROJECT |
GCP project ID |
GOOGLE_CLOUD_LOCATION |
GCP region (e.g., us-central1) |
PORT |
Server port (default: 8080) |
API Endpoints
When running with web api:
| Endpoint | Method | Purpose |
|---|---|---|
/apps/{app}/users/{user}/sessions |
POST | Create session |
/apps/{app}/users/{user}/sessions |
GET | List sessions |
/apps/{app}/users/{user}/sessions/{id} |
GET | Get session |
/apps/{app}/users/{user}/sessions/{id}:run |
POST | Send message |
/apps/{app}/users/{user}/sessions/{id}:runSse |
POST | Stream response |
/.well-known/agent-card.json |
GET | A2A agent card |
Example API Call
# Create session
curl -X POST http://localhost:8080/apps/myapp/users/user1/sessions \
-H "Content-Type: application/json" \
-d '{}'
# Send message
curl -X POST http://localhost:8080/apps/myapp/users/user1/sessions/SESSION_ID:run \
-H "Content-Type: application/json" \
-d '{"message": {"role": "user", "parts": [{"text": "Hello!"}]}}'
Common Patterns
Error Handling in Tools
func myTool(ctx tool.Context, input Input) Output {
result, err := externalAPI(input.Query)
if err != nil {
// Return error message to the model
return Output{
Success: false,
Error: fmt.Sprintf("API error: %v", err),
}
}
return Output{Success: true, Data: result}
}
Tool with HTTP Client
func fetchURL(ctx tool.Context, input URLInput) FetchOutput {
client := &http.Client{Timeout: 10 * time.Second}
req, _ := http.NewRequestWithContext(ctx, "GET", input.URL, nil)
resp, err := client.Do(req)
if err != nil {
return FetchOutput{Error: err.Error()}
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
return FetchOutput{Content: string(body)}
}
Conditional Tool Availability
// Filter tools based on state
agent, _ := llmagent.New(llmagent.Config{
ToolFilter: func(ctx agent.InvocationContext, tools []tool.Tool) []tool.Tool {
userRole := ctx.Session().State().Get("user:role")
if userRole != "admin" {
// Remove admin-only tools
return filterOutAdminTools(tools)
}
return tools
},
})
Graceful Shutdown
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Handle shutdown signals
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigCh
log.Println("Shutting down...")
cancel()
}()
// Run agent
l := full.NewLauncher()
if err := l.Execute(ctx, cfg, os.Args[1:]); err != nil {
if ctx.Err() == nil {
log.Fatal(err)
}
}
}
Troubleshooting
Common Issues
| Problem | Solution |
|---|---|
module not found |
Use google.golang.org/adk, not GitHub path |
API key invalid |
Check GOOGLE_API_KEY is set correctly |
context deadline exceeded |
Increase timeout or check network |
tool not called |
Improve tool description; make it clearer |
state not persisting |
Check state prefix; use user: for persistence |
Debug Logging
import "log"
agent, _ := llmagent.New(llmagent.Config{
BeforeModelCallback: func(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMRequest, error) {
log.Printf("REQUEST: %+v", req)
return req, nil
},
AfterModelCallback: func(ctx agent.CallbackContext, resp *model.LLMResponse) (*model.LLMResponse, error) {
log.Printf("RESPONSE: %+v", resp)
return resp, nil
},
})
Web UI Debugging
Access http://localhost:8080 when running with web api webui:
- Events tab: See all events in the session
- Request/Response tab: Raw LLM communication
- Graph tab: Visualize agent flow
- State tab: Inspect session state
Resources
- GitHub: https://github.com/google/adk-go
- Documentation: https://google.github.io/adk-docs/
- Go Package: https://pkg.go.dev/google.golang.org/adk
- Examples: https://github.com/google/adk-go/tree/main/examples
- Python ADK: https://github.com/google/adk-python
- Java ADK: https://github.com/google/adk-java