Kicking the tires on ADK for Go 2.0
(google.golang.org/adk/v2, released 2026-06-30).
Eighteen runnable programs, each a step up in ADK 2.0 features:
| Command | What it is |
|---|---|
go run . |
hello agent — one Gemini agent + one add tool, one hard-coded turn |
go run ./hitlgraph console |
workflow graph + human-in-the-loop — LLM drafts a post, a human approves/edits it, then it publishes |
go run ./durablehitl submit/resume |
durable, search-grounded HITL — the pause survives a full process restart (state in SQLite) |
go run ./fanout "topic" |
fan-out / fan-in — 3 grounded drafters run in parallel, an editor picks the best, then a human approves |
go run ./schemahitl console |
schema-validated HITL — the human's reply must match a JSON ResponseSchema (approve/reject/edit) |
go run ./serve web -port 8791 api |
serve over HTTP — the same graph as a REST API; pause/resume driven by curl |
go run ./a2a demo |
A2A — expose an agent over the agent-to-agent protocol and call it from a client |
go run ./claude console |
Claude instead of Gemini — the same HITL graph on claude-opus-4-8 via a custom model.LLM |
go run ./claudetools |
tool calling on Claude — the add function tool, driven by Claude through the same adapter |
go run ./claudesearch "topic" |
search-grounded Claude — geminitool.GoogleSearch{} auto-mapped to Anthropic's web_search |
go run ./a2amesh "question" |
cross-provider mesh — a Claude orchestrator delegates to a Gemini specialist over A2A |
go run ./adk46 "peak" |
ADK × ADK 🏔️ — use the Agent Development Kit to plan a hike in the Adirondacks |
go run ./adk46web |
the High Peaks Trip Desk — the adk46 planner behind a hand-built website (SSE-streamed scouts, trail-register HITL) |
go run ./adk46er bag/list/next |
durable 46er tracker — log peaks (SQLite), see progress, ask a Claude mentor for your next |
go run ./rangerguide "question" |
ranger↔guide mesh — a Claude guide consults a Gemini "park ranger" over A2A |
go run ./whichpeak "profile" |
Trailhead Oracle — a search-grounded Claude pick for which peak to hike today |
go run ./eval |
LLM-as-judge eval harness — score an agent's answers against rubrics (the Dev UI "Evals" tab is a 501 stub in Go v2.0.0) |
go run ./loopcritic "peak" |
self-critique loop — a LoopAgent where a safety critic keeps sending the plan back until it passes, then exit_loops |
export GOOGLE_API_KEY=... # free key: https://aistudio.google.com/apikey
go run .Expected: the agent calls the add tool for 2 + 3 and reports 5.
A two-node graph built on the v2 workflow engine:
Start ─▶ draft ─▶ review
(LLM) (HITL pause + decision)
- draft —
workflow.NewAgentNode(llmAgent, …)drops a Gemini agent into the graph as a single-turn node; it turns your typed topic into a post draft. - review — a re-entry node (
workflow.ResumeOrRequestInput+NodeConfig{RerunOnResume:&true}). On the first pass it shows the draft and returnsworkflow.ErrNodeInterrupted, which suspends the whole workflow until you answer; then it re-runs with the draft still in hand plus your reply and acts on it —yes/Enter approves,nodiscards, anything else is your edit.
The graph is wired with workflow.Chain(Start, draft, review) and wrapped as an
agent via workflowagent.New. The console launcher (full.NewLauncher()) drives
the pause/resume and renders prompts.
export GOOGLE_API_KEY=...
go run ./hitlgraph console
# User -> the launch of ADK for Go 2.0
# Agent -> (draft appears, workflow PAUSES)
# User -> yes # approve as-is — or type an edit, or "no" to discard
# Agent -> ✅ Published (approved as-is): ...Same draft → review → publish graph, but with two upgrades that compose:
- Search-grounded draft — the
draftagent getsgeminitool.GoogleSearch{}, so it drafts from live info. - Durable state — sessions are stored in SQLite via
database.NewSessionService(sqlite.Open("adk_sessions.db")), so a paused workflow outlives the process that started it.
It runs as two separate processes sharing the DB file:
export GOOGLE_API_KEY=...
# Process 1: search + draft, then PAUSE and exit. State is saved to SQLite.
go run ./durablehitl submit "the launch of ADK for Go 2.0"
# Process 2 (brand new process): reload the paused session and finish.
go run ./durablehitl resume "ADK for Go 2.0 is here 🚀 ...your edited post..."How resume works across processes (no launcher involved):
- The
reviewnode'sRequestInputis recorded in the event log as a long-runningFunctionCallnamedworkflow.WorkflowInputFunctionCallName("adk_request_input") — persisted in SQLite. resumecallssessionSvc.Get(...), scans the persisted events for that call with no matchingFunctionResponseyet, and replies with agenai.FunctionResponse{ID, Name, Response: {"payload": <text>}}.runner.Runroutes that response to the waiting node by ID and the workflow continues intopublish. (This is exactly what the console launcher does internally incmd/launcher/console/hitl.go— just split across two processes.)
Uses the pure-Go SQLite driver github.com/glebarez/sqlite (no cgo), the same
one ADK uses in its own tests. The adk_sessions.db file is gitignored.
| Pattern | Primitive | Upstream example |
|---|---|---|
Static chain handoff (durablehitl, fanout) |
emit RequestInput + return ErrNodeInterrupted; reply → next node |
examples/workflow/hitl_simple |
Single-node re-entry (hitlgraph, schemahitl) |
workflow.ResumeOrRequestInput(...) + NodeConfig{RerunOnResume:&true} |
examples/workflow/hitl_rerun |
| Dynamic orchestrator | workflow.RunNode(...) + ctx.ResumedInput(id) |
examples/workflow/dynamic/hitl |
Best-of-N with a human gate. Three search-grounded drafters (distinct voices)
run in parallel, a JoinNode barrier gathers them, an LLM editor picks the
best, then a human approves:
┌─▶ drafter_punchy ─┐
Start ───┼─▶ drafter_technical ─┼─▶ gather ─▶ format ─▶ editor ─▶ review
└─▶ drafter_concise ─┘ (Join) (func) (LLM) (HITL)
go run ./fanout "the AI walled gardens" # or: go run ./fanout (prompts for a topic)workflow.NewEdgeBuilder()withAddFanOut(Start, …)/AddFanIn(join, …)/Add(a, b)expresses the barrier (workflow.Chaincan't).- The
JoinNodefires once after all predecessors finish and hands its successor amap[nodeName]output— the formatter looks drafts up by node name. - A single-turn
AgentNode's output propagates viaEvent.Output. - Unlike the
consoledemos,fanoutuses its own runner + formatter (not the generic launcher) so the three parallel drafts and the pick print as a clean labeled report instead of one concatenated blob.reviewis a re-entry node:yes/Enter approves,nodiscards, anything else is your edit.
The pause carries a JSON ResponseSchema, so the human's reply must be a
structured object — {"decision":"approve|reject|edit","text":"…"} — validated
by the engine before resuming (a mismatch yields workflow.ErrInvalidResumeResponse
and keeps the node waiting for a corrected retry).
- Schema type:
*jsonschema.Schemafromgithub.com/google/jsonschema-go/jsonschema(setType:"object",Properties,Enum []any,Required []string). - A validated reply is delivered as a
map[string]any(a JSON object decoded intoany) — readm["decision"].(string),m["text"].(string). - Uses the re-entry pattern (
ResumeOrRequestInput+NodeConfig{RerunOnResume:&true}) so the node keeps its draft input and receives the decision in one place. - In
console, type a full JSON object on one line; a bare word gets wrapped as{"payload": …}and fails the object schema.
The same full.NewLauncher() that runs console also serves REST — the mode is
web api (there is no rest keyword). Flags are positional: -port is a
web flag (before api).
go run ./serve web -port 8791 api # REST under http://localhost:8791/api
# create session → run (pauses) → resume with a FunctionResponse:
SID=$(curl -s -X POST .../api/apps/served_review/users/u/sessions -d '{}' | jq -r .id)
curl -s -X POST .../api/run -d '{"appName":"served_review","userId":"u","sessionId":"'$SID'",
"newMessage":{"role":"user","parts":[{"text":"<topic>"}]}}' # → requestedInput.interruptId
curl -s -X POST .../api/run -d '{"appName":"served_review","userId":"u","sessionId":"'$SID'",
"newMessage":{"role":"user","parts":[{"functionResponse":{"id":"<interruptId>",
"name":"adk_request_input","response":{"payload":"<approved text>"}}}]}}' # → event.outputPOST /api/run is non-streaming: it returns the whole turn's events as one JSON
array and returns at the pause, which is what makes HITL curl-drivable. Wire a
SQLite SessionService into launcher.Config (as serve/ does) or the web
launcher defaults to in-memory (sessions vanish on restart).
Expose an agent over the A2A protocol and call it from a separate process.
export GOOGLE_API_KEY=... # server runs the LLM; the client does not
go run ./a2a server 8792 & # serves an agent card + JSON-RPC /invoke
go run ./a2a client http://127.0.0.1:8792 "one line about Go agents"
# or all-in-one: go run ./a2a demo "…"- Server (
server/adka2a/v2, packageadka2a):adka2a.NewExecutor(ExecutorConfig{RunnerConfig: runner.Config{…}}), then servea2asrv.NewStaticAgentCardHandler(card)ata2asrv.WellKnownAgentCardPathanda2asrv.NewJSONRPCHandler(a2asrv.NewHandler(executor))at/invoke. - Client (
agent/remoteagent/v2, packageremoteagent):remoteagent.NewA2A(A2AConfig{AgentCardProvider: remoteagent.NewAgentCardProvider(baseURL)})returns a plainagent.Agentyou run throughrunner.New/Runlike any local agent. - Note the import-path/package-name mismatch: use the
/v2paths, whose packages are namedadka2a/remoteagent(notv2). Needsgithub.com/a2aproject/a2a-go/v2.
ADK is Google's framework but it's model-agnostic — an agent takes any
model.LLM, and model/gemini is just one implementation. claudemodel/ is a
second one, backed by the official
github.com/anthropics/anthropic-sdk-go
(defaults to claude-opus-4-8). claude/ is the hitlgraph demo with exactly
one line changed:
model := claudemodel.NewModel("") // instead of gemini.NewModel(ctx, "...", ...)Everything else — the graph, the AgentNode, the re-entry HITL review, the
console launcher — is untouched ADK.
export ANTHROPIC_API_KEY=... # or `ant auth login` (the Go SDK reads the profile)
go run ./claude consoleThe adapter implements ADK's tiny model interface —
GenerateContent(ctx, *LLMRequest, stream) iter.Seq2[*LLMResponse, error] — by
translating the genai-shaped request into an Anthropic Messages call and shaping
the reply back into a genai.Content:
- genai
Config.SystemInstruction→ AnthropicSystem - genai
Contents(rolemodel↔assistant) → AnthropicMessages - Anthropic
TextBlocks → text parts on amodel-rolegenai.Content
Tool calling works too (go run ./claudetools — the add tool on Claude):
- genai function declarations (
ParametersJsonSchema) → Anthropic tool defs - a genai
FunctionCallpart → an Anthropictool_useblock, and aFunctionResponsepart → atool_resultblock, with the tool-use ID threaded so Anthropic pairs each result to its call across the loop - an Anthropic
tool_usein the reply → a genaiFunctionCall, so the ADK runner executes the Go tool and loops until Claude produces the final text
Search grounding works too (go run ./claudesearch "topic"): the adapter
detects a geminitool.GoogleSearch{} tool in the request and maps it to
Anthropic's own web_search server tool (with pause_turn handling). So the
same agent — declaring Gemini's GoogleSearch — is web-grounded on either
provider; on Claude it runs live searches and answers with cited facts.
Scope: text, system instruction, function tools, and web-search grounding. Other Gemini-specific server tools (code execution, Maps, …) have no Claude equivalent and are ignored.
A Claude orchestrator delegates a factual question to a Gemini specialist over the A2A protocol — two different providers' agents in one system:
Claude orchestrator (claude-opus-4-8)
│ calls the "gemini_specialist" tool
▼
agenttool ──A2A/JSON-RPC──▶ Gemini specialist (gemini-3.5-flash), served over A2A
▲ answers the sub-question
└──────────── answer ────────┘ → Claude synthesizes the reply
export GOOGLE_API_KEY=... # Gemini specialist (+ Anthropic creds for Claude)
go run ./a2amesh "what is the tallest volcano in the solar system?"It composes the whole toolkit: claudemodel + its tool calling (the
agent-as-tool uses the Parameters JSON-Schema path — the blocker fix), plus the
A2A server/remoteagent wiring. Claude emits a tool_use for gemini_specialist;
ADK routes it over A2A to the Gemini agent; the answer returns as a tool_result;
Claude synthesizes. (If the free-tier Gemini quota is exhausted, Claude degrades
gracefully — it reports the specialist was unreachable rather than failing.)
A little word-play: use the Agent Development Kit to bag the ADK (the Adirondacks) — the 46 High Peaks. A team of Adirondack scout agents fans out to research a peak in parallel (each grounded on live web search), a "head guide" agent synthesizes a trip brief, and you — the hiker — approve, edit, or scrap it.
🗺️ route_scout ─┐
Start ─ ⛅ sky_watcher ─┼─ gather ─ format ─ 🏔️ head_guide ─ review (you)
🎒 pack_master ─┘ (Join) (func) (LLM) (HITL)
export ANTHROPIC_API_KEY=... # runs entirely on Claude
go run ./adk46 "Mount Marcy — a day hike this weekend"It's the whole toolkit wearing an Adirondack hat: fan-out/fan-in,
search-grounded Claude (each scout's geminitool.GoogleSearch{} → Anthropic
web_search), an LLM synthesizer, and a HITL approval — and the output is
genuinely useful (it'll catch a summit snow forecast or a closed trail).
Live at adkadk.life — deployed on Cloud Run
(Dockerfile at the repo root; per-IP + daily rate limits guard the API spend).
The same graph (extracted into the shared tripgraph/ package), behind a
hand-built site instead of a console:
export ANTHROPIC_API_KEY=...
go run ./adk46web # → http://localhost:8746 (46, naturally)- Pick a peak off the skyline — all 46 High Peaks as an elevation-sorted range profile (Marcy 5,344 ft → Couchsachraga 3,820 ft); tap a summit or type your own trip line.
- Watch the expedition live —
POST /api/planholds a server-sent-event stream open over the whole run; each scout's card fills in the moment that scout'ssession.Eventarrives, then the head guide's brief lands on a pinned paper card. - Sign the trail register — the HITL pause rendered as a ledger: file it
(stamped FILED), pencil in edits (your line in ballpoint), or scrap
it. The answer goes back as the same
FunctionResponsethe console sends. - One Go binary: the frontend (hand-rolled HTML/CSS/JS, WPA-park-poster ×
trail-journal aesthetic — no framework) is
go:embedded next to the API.
Three companions round out the ADK × ADK corner, each foregrounding a different capability:
adk46er— a durable 46er tracker:bag "Marcy",listyour progress toward 46 (persisted in SQLite), andnextasks a Claude mentor which peak to do next given what you've bagged.rangerguide— the cross-provider A2A mesh, themed: a Claude "trail guide" consults a Gemini "park ranger" (regulations) over A2A, then advises.whichpeak— a search-grounded "Trailhead Oracle": give it your fitness and time; it checks the forecast and recommends a peak for today.
ADK 2.0's Dev UI has an Evals tab, but in Go v2.0.0 the backend's eval REST
endpoints are stubs (controllers.Unimplemented → HTTP 501) and there's no public
eval package. So eval/ is a DIY harness in the spirit of the upstream
examples/web/agents/llmauditor.go (an LLM critic + reviser).
For each case it produces a recommendation (running the agent-under-test, or a
planted ForceOutput), then a panel of judge agents (different Claude models)
scores it against the case rubric and returns JSON {pass, score, rationale}; the
panel majority-votes. The demo evaluates an Adirondack peak recommender against
hiker-profile rubrics.
Two things keep it honest rather than a rubber stamp — both came out of an adversarial review of the harness itself (see the review-workflow pattern above), which found that a naïve 5/5 pass rate proves nothing:
- a negative control — a planted, dangerous recommendation the judges must fail, so an always-pass judge is caught instead of rewarded; and
- a judge panel (
opus-4-8+sonnet-5+haiku-4-5) with majority vote, so one lenient/noisy judge can't decide a case.
export ANTHROPIC_API_KEY=... # all agents run on Claude
go run ./eval # exits non-zero if the panel contests a labeled verdictIt has teeth: a sample run rejected the negative control 3/3, and flagged a real
borderline — a peak recommended for a 2-hour window that actually needs 3–4 hours.
parseVerdict / vote are unit-tested.
The counterpart to eval/: there a judge grades an agent from the outside;
here a critic lives inside the agent and it self-corrects until it passes.
Built on ADK's loop agent (agent/workflowagents/loopagent), which runs its
sub-agents in sequence and repeats up to MaxIterations — or until a sub-agent
escalates:
┌──────────────── loop (≤ 4 iterations) ─────────────────┐
│ ✍️ planner → drafts / revises the trip plan │
│ 🔍 safety_critic → PASS? call exit_loop (escalate→stop)│
│ FAIL? numbered critique → next round │
└─────────────────────────────────────────────────────────┘
The critic holds the tool/exitlooptool tool; calling exit_loop sets
Actions().Escalate = true, which the loop detects to stop early. That tool call
runs through the Claude model.LLM adapter's tool-calling path — so this also
exercises claudemodel's function calling.
export ANTHROPIC_API_KEY=...
go run ./loopcritic "Algonquin Peak, day hike, this weekend"A sample run: draft v1 is a rough sketch → the critic rejects it with 5 numbered
fixes ("I will not approve a sketch") → draft v2 addresses every one (exact
route, real distance/gain, weather thresholds, gear, a noon turnaround + Wright
Peak bailout) → the critic approves and exit_loops. Also serves in the Dev UI
(go run ./loopcritic web -port 8793 webui -api_server_address http://localhost:8793/api api).
| Piece | Package | What it does |
|---|---|---|
| Model | model/gemini |
gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{APIKey}) |
| Tool | tool/functiontool |
wraps a func(agent.Context, In) (Out, error); schema inferred from Go types |
| Agent | agent/llmagent |
llmagent.New(Config{Name, Model, Instruction, Tools}) |
| Session | session |
session.InMemoryService() — conversation state |
| Runner | runner |
runner.New(...), then r.Run(...) returns an iter.Seq2[*session.Event, error] |
- Graph-based workflow engine (
google.golang.org/adk/v2/workflow) — nodes + edges, a scheduler, state persistence, and resumption across process restarts. - Human-in-the-loop as a first-class primitive (pause / resume).
- LLM agent modes: Chat, Task, SingleTurn.
- Unified
agent.Context— replaces 1.x'sToolContext/CallbackContext(breaking change). Don't mixgoogle.golang.org/adk(v1) and.../adk/v2imports.
The original four (search grounding, fan-out/fan-in, durable resume, HTTP serving) plus schema-validated pauses and A2A are all done above. Further:
- Chain A2A: have the
serve/HTTP graph call thea2a/agent as aremoteagentnode — a multi-agent system split across processes. - Swap the console for the web Dev UI (
go run ./serve web -port 8791 webui) to click through the HITL pause in a browser. - Add a durable, schema-validated pause served over HTTP (combine
schemahitl+serve+ the SQLiteSessionService). - Structured payloads: use
RequestInput.Payloadto ship the whole draft object (not just a string) to the UI, and a richerResponseSchemafor the reply.