From baeb770616cda5d21d1a62e6d22748e1823ccebe Mon Sep 17 00:00:00 2001 From: DABH Date: Wed, 15 Jul 2026 16:09:35 -0500 Subject: [PATCH 1/4] Add Google ADK integration page for the Go SDK Add the first Go SDK integration guide: Google ADK (go.temporal.io/sdk/contrib/googleadk). Covers worker/workflow setup, tools (function, ActivityAsTool, MCP), multi-agent systems, human-in-the-loop tool confirmation, continue-as-new, streaming, and error classification, with code sourced from the samples-go googleadk sample via snipsync. Also bootstraps Go integrations docs: a new Go integrations index, a Go "Integrations" sidebar category, and "Go" added to the IntegrationsGrid SDK type plus a Google ADK grid entry. --- docs/develop/go/integrations/google-adk.mdx | 222 ++++++++++++++++++ docs/develop/go/integrations/index.mdx | 21 ++ sidebars.js | 11 + .../IntegrationsGrid/integrations-data.json | 9 + .../IntegrationsGrid/integrations-data.ts | 2 +- 5 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 docs/develop/go/integrations/google-adk.mdx create mode 100644 docs/develop/go/integrations/index.mdx diff --git a/docs/develop/go/integrations/google-adk.mdx b/docs/develop/go/integrations/google-adk.mdx new file mode 100644 index 0000000000..3b96df0ae9 --- /dev/null +++ b/docs/develop/go/integrations/google-adk.mdx @@ -0,0 +1,222 @@ +--- +id: google-adk +title: Google ADK integration +sidebar_label: Google ADK +toc_max_heading_level: 2 +keywords: + - ai + - agents + - google + - adk + - durable execution + - ai workflows +tags: + - Google ADK + - Go SDK + - Temporal SDKs +description: Run Google ADK agents with durable execution using the Temporal Go SDK and the googleadk contrib integration. +--- + +Temporal's integration with [Google ADK](https://google.github.io/adk-docs/) (`adk-go`) gives your agents +[Durable Execution](/temporal#durable-execution): the agent's orchestration loop runs inside a Temporal Workflow, each +LLM call becomes a durable Temporal Activity, and any tool that does I/O runs as an Activity too — so every step is +retried, timed out, recorded in Workflow history, and replayable after crashes or restarts. + +You keep building agents the native ADK way — `llmagent.New(...)` with a `model.LLM`, `tool.Tool`s / `tool.Toolset`s and +`SubAgents`, wrapped in `runner.New(...)` and driven by `r.Run(...)`. You change two things: + +1. Use `googleadk.NewModel("")` as your agent's `Model`. It is a `model.LLM` whose calls dispatch to the + `InvokeModel` Activity; the real model is reconstructed **worker-side**, never in the Workflow. +2. Pass `googleadk.NewContext(workflowCtx)` to `r.Run`, which installs Temporal-deterministic time, UUID, and + task-fan-out providers so the agent loop replays deterministically. + +Tools run **in-workflow by default** (the idiomatic Temporal model: the Workflow is deterministic, and anything touching +the network, clock, or disk goes through an Activity). Opt a tool into an Activity with `googleadk.ActivityAsTool`, or +use `googleadk.NewMCPToolset` for MCP. + +:::info + +The `googleadk` contrib module is new. It depends on determinism seams (`platform.WithTimeProvider`, +`WithUUIDProvider`, `WithTaskRunner`) that merged into `google.golang.org/adk/v2` after its latest tagged release, so +`go.mod` pins `adk/v2` to a `main`-branch pseudo-version until a release ships that includes them. + +::: + +Code snippets in this guide are taken from the +[Google ADK plugin samples](https://github.com/temporalio/samples-go/tree/main/googleadk). Refer to the samples for the +complete, runnable code. + +## Prerequisites + +- This guide assumes you are already familiar with Google ADK. If you aren't, refer to the + [Google ADK documentation](https://google.github.io/adk-docs/) for more details. +- If you are new to Temporal, read [Understanding Temporal](/evaluate/understanding-temporal) or take the + [Temporal 101](https://learn.temporal.io/courses/temporal_101/) course. +- Set up your local development environment by following the + [Set up your local development environment](/develop/go/set-up-your-local-go) guide. Leave the Temporal development + server running if you want to test your code locally. +- Provide model credentials **worker-side** — for Gemini, set `GEMINI_API_KEY` (or `GOOGLE_API_KEY`) in the worker's + environment. Credentials are captured in the worker's `ModelFactory` and never cross the Activity boundary into the + Workflow. + +## Install the plugin + +Install the `googleadk` contrib module: + +```bash +go get go.temporal.io/sdk/contrib/googleadk +``` + +```go +import "go.temporal.io/sdk/contrib/googleadk" +``` + +## Run an agent with Durable Execution + +An agent has two halves: the **worker** registers the real model behind the `InvokeModel` Activity, and the **workflow** +builds a vanilla ADK agent and drives it. + +### Configure the Worker + +Build the worker-side registry with `googleadk.NewActivities` and register it. The real Gemini model lives here, behind +the Activity boundary; the API key is read worker-side. Disable the model SDK's own retries so Temporal's `RetryPolicy` +is the single source of truth. + + + + +`Config.Models` is optional for providers ADK's registry already knows (for example `gemini-*`): when a model name is +absent, `InvokeModel` falls back to `model.NewLLM`. Supply a factory to inject credentials, disable the model SDK's own +retries, or override the default. + +### Define the Workflow + +Build the agent the ordinary ADK way, using `googleadk.NewModel` for the model and passing `googleadk.NewContext(ctx)` +to `r.Run`. The `get_weather` tool is an ordinary Temporal Activity exposed to the agent with `googleadk.ActivityAsTool`. + + + + +The tool itself is an ordinary Temporal Activity — register it on the worker as usual, and expose it to the agent with +`ActivityAsTool` (its parameter schema is inferred from the argument type): + + + + +### Start the Workflow + +Start the Workflow like any other and read its result: + + + + +## Tools + +- **Function tools run in-workflow by default.** Ordinary `functiontool.New(...)` tools run on Temporal's deterministic + dispatcher inside the Workflow — no Activity overhead — and their session-state mutations propagate normally. Their + code must be deterministic and replay-safe: no direct network, clock, randomness, or goroutines. +- **Opt a tool into an Activity when it does I/O.** `googleadk.ActivityAsTool(myActivity, ...)` exposes an existing + `func(context.Context, TArgs) (TResults, error)` Temporal Activity to the agent as a tool (shown in the Hello World + Workflow above); its call dispatches the Activity, so it is retried, timed out, and visible in the UI. +- **MCP, statelessly.** `googleadk.NewMCPToolset(...)` is a workflow-side proxy that lists remote tools via the + `ListMcpTools` Activity and executes calls via `CallMcpTool`. The live, stateful `mcptoolset.New(...)` runs worker-side + (registered in `Config.MCPToolsets`), never in the Workflow. + +## Multi-agent systems + +Build a coordinator agent with specialist `SubAgents`; ADK wires the parent/child relationship and exposes the built-in +`transfer_to_agent` tool automatically. The entire tree — including the transfer hop — runs in the Workflow; only the +model calls and any Activity-backed tools leave it. + + + + +## Human-in-the-loop tool confirmation + +A sensitive tool calls ADK's `ctx.RequestConfirmation(hint, payload)`, which ends the turn with an +`adk_request_confirmation` function call. The Workflow detects pending confirmations with +`googleadk.PendingConfirmations`, durably waits for the human's decision (delivered as a Temporal signal), and resumes +the agent with `googleadk.ConfirmationResponse`. Because the wait is durable, the Workflow can sit idle for days and +survive worker restarts — when the approval signal arrives, the agent resumes exactly where it paused. + + + + +## Continue-as-new for long conversations + +A conversation's history lives in the ADK session. To keep a Workflow's history bounded, snapshot the session with +`googleadk.ExportSession` and [continue-as-new](/develop/go/workflows/continue-as-new); rebuild it on the next run with +`googleadk.ImportSession`. `SessionSnapshot` is JSON-serializable (session-scoped state plus the full event history), so +every value in session state and every tool result must be JSON-encodable. + + + + +## Streaming + +`googleadk.NewModel(name, googleadk.WithStreaming(topic, 0))` drives the model in streaming mode: the `InvokeModel` +Activity calls the model with `stream=true`, heartbeats, and publishes each chunk to a per-run +[`workflowstreams`](https://pkg.go.dev/go.temporal.io/sdk/contrib/workflowstreams) topic for external (UI) consumers, +then returns the aggregated final response into the Workflow so replay stays deterministic. + +Call `googleadk.StreamServer(ctx)` once near the top of the Workflow that drives `r.Run`, and set +`agent.RunConfig{StreamingMode: agent.StreamingModeSSE}`: + +```go +func StreamingAgentWorkflow(ctx workflow.Context, q string) (string, error) { + if err := googleadk.StreamServer(ctx); err != nil { // required when streaming + return "", err + } + topic := "run-" + workflow.GetInfo(ctx).WorkflowExecution.ID + root, _ := llmagent.New(llmagent.Config{ + Model: googleadk.NewModel("gemini-2.0-flash", googleadk.WithStreaming(topic, 0)), + // ... + }) + // ... build the runner, set agent.RunConfig{StreamingMode: agent.StreamingModeSSE}, and drive r.Run +} +``` + +External consumers read chunks with `workflowstreams.NewClient(c, workflowID, ...).Subscribe(...)`. The bidirectional +`RunLive` path (hard-coded goroutines/channels) is **not** supported. + +## Error handling + +Model, tool, and MCP failures surface as Temporal `ApplicationError`s tagged `googleadk.ModelError`, `.ToolError`, and +`.McpError`. Classify them with `googleadk.IsNonRetryable(err)` rather than string-matching. For model calls, the +upstream HTTP status drives retryability (`408`/`409`/`429`/`5xx` are retryable; other `4xx` are not). + +:::tip + +Disable your model client's own retries in the `ModelFactory`. `InvokeModel` already runs under Temporal's +`RetryPolicy`; leaving the model SDK's retries on retries a transient failure twice over. Let Temporal own retries. + +::: + +## Composing with other plugins + +The Temporal-side Activities use the default JSON data converter and ship no client/worker interceptor, so this +integration composes with Temporal interceptor- or converter-based plugins (for example +[`sdk-go/contrib/opentelemetry`](https://pkg.go.dev/go.temporal.io/sdk/contrib/opentelemetry)) without conflict. ADK +emits its own OpenTelemetry spans; register your tracing interceptor on the worker as usual. + +## Testing without a live LLM + +The plugin ships test helpers so you can unit-test agent Workflows with no network: `FakeModel` (with `TextResponse` / +`FunctionCallResponse` builders) and `FakeMCPServer`. Register them through the same `googleadk.Config` your production +worker uses. + +## Supported and not-yet-supported + +- **Supported:** single- and multi-agent (`SubAgents`) trees, in-workflow function tools, `ActivityAsTool`, stateless + MCP, Gemini built-in tools (executed server-side inside `InvokeModel`), human-in-the-loop tool confirmation, + continue-as-new session-state carry, the in-memory session service, and SSE streaming. +- **Not yet:** `RunLive` (bidirectional streaming), sub-agent-as-child-workflow, live memory/artifact tools that require + in-workflow network I/O, and database/Vertex session services. These raise or are documented rather than silently + degrading. + +## Samples + +The [Google ADK plugin samples](https://github.com/temporalio/samples-go/tree/main/googleadk) demonstrate a basic agent +with a tool, a [multi-agent](https://github.com/temporalio/samples-go/tree/main/googleadk/multiagent) system, durable +[human-in-the-loop](https://github.com/temporalio/samples-go/tree/main/googleadk/humanintheloop) tool approval, and a +[continue-as-new chat](https://github.com/temporalio/samples-go/tree/main/googleadk/chat). diff --git a/docs/develop/go/integrations/index.mdx b/docs/develop/go/integrations/index.mdx new file mode 100644 index 0000000000..1e144329ab --- /dev/null +++ b/docs/develop/go/integrations/index.mdx @@ -0,0 +1,21 @@ +--- +id: index +title: Integrations +sidebar_label: Integrations +toc_max_heading_level: 2 +keywords: + - integrations + - ai frameworks + - agent frameworks +tags: + - Integrations + - AI Frameworks + - Agent Frameworks +description: Integrations with other tools and services. +--- + +import IntegrationsGrid from '@site/src/components/IntegrationsGrid'; + +The following integrations are available for the Temporal Go SDK. + + diff --git a/sidebars.js b/sidebars.js index 967d06928f..bc2ca1d65b 100644 --- a/sidebars.js +++ b/sidebars.js @@ -232,6 +232,17 @@ module.exports = { }, ], }, + { + type: 'category', + label: 'Integrations', + className: 'sidebar-integrations', + collapsed: true, + link: { + type: 'doc', + id: 'develop/go/integrations/index', + }, + items: ['develop/go/integrations/google-adk'], + }, ], }, { diff --git a/src/components/IntegrationsGrid/integrations-data.json b/src/components/IntegrationsGrid/integrations-data.json index a527217a48..26ed7230bc 100644 --- a/src/components/IntegrationsGrid/integrations-data.json +++ b/src/components/IntegrationsGrid/integrations-data.json @@ -62,6 +62,15 @@ "sdk": "Python", "href": "https://adk.dev/integrations/temporal/" }, + { + "name": "Google ADK", + "description": "Run Google ADK agents with durable execution using the Temporal Go SDK.", + "tags": [ + "Agent framework" + ], + "sdk": "Go", + "href": "/develop/go/integrations/google-adk" + }, { "name": "Grafana Cloud", "description": "Export Temporal Cloud metrics to Grafana Cloud with a serverless integration and pre-built dashboard.", diff --git a/src/components/IntegrationsGrid/integrations-data.ts b/src/components/IntegrationsGrid/integrations-data.ts index 537a282c89..3b3729c443 100644 --- a/src/components/IntegrationsGrid/integrations-data.ts +++ b/src/components/IntegrationsGrid/integrations-data.ts @@ -1,6 +1,6 @@ import integrationsData from "./integrations-data.json"; -export type SDK = "Java" | "Python" | "TypeScript" | "Ruby"; +export type SDK = "Go" | "Java" | "Python" | "TypeScript" | "Ruby"; export type Integration = { name: string; From 9a3a07b1bdb472c66619f10e1b5428b720bc06f9 Mon Sep 17 00:00:00 2001 From: David Hyde Date: Fri, 17 Jul 2026 15:24:09 -0500 Subject: [PATCH 2/4] IntegrationsGrid: add Go as an SDK option (#4914) * IntegrationsGrid: add Go as an SDK option The Integrations page UI had no Go filter pill and could not render a Go card icon: ALL_SDKS and SDK_BLOCK_NAMES omitted Go (the SDK union gains Go in the base branch). Add Go to both, mapped to the existing goLangBlock icon that the SDK guide cards and release notes already use, and update the valid-values list in COMPONENTS.md. The markdown-pipeline handler is data-driven and needs no change. * go: surface Integrations on the dev-guide landing page, match Java's page format Add the Integrations section to develop/go/index.mdx (mirroring the Java dev guide) and align the Go integrations index frontmatter with the Java page: SDK-suffixed title, SDK keywords/tags, and a pointer to the Plugin system guide for building your own integrations. * go: sentence-case the technical resources heading Vale (Temporal.Headings) flags 'Temporal Go Technical Resources' now that the new Integrations section pulls it into the diff. Sentence-case it, matching the style guide and the Java/TypeScript dev guide pages. The heading slug is unchanged. --- COMPONENTS.md | 2 +- docs/develop/go/index.mdx | 6 +++++- docs/develop/go/integrations/index.mdx | 13 +++++++------ src/components/IntegrationsGrid/index.tsx | 3 ++- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/COMPONENTS.md b/COMPONENTS.md index ab7811c73f..690e8e9dea 100644 --- a/COMPONENTS.md +++ b/COMPONENTS.md @@ -155,7 +155,7 @@ For example, the Python SDK integrations page pre-filters to Python: ``` -Valid SDK values are `"Java"`, `"Python"`, `"TypeScript"`, and `"Ruby"`. +Valid SDK values are `"Go"`, `"Java"`, `"Python"`, `"TypeScript"`, and `"Ruby"`. ### Filter behavior diff --git a/docs/develop/go/index.mdx b/docs/develop/go/index.mdx index 1d8f1844fb..e3291fa6e4 100644 --- a/docs/develop/go/index.mdx +++ b/docs/develop/go/index.mdx @@ -89,7 +89,11 @@ From there, you can dive deeper into any of the Temporal primitives to start bui - [Testing](/develop/go/best-practices/testing-suite) - [Data handling](/develop/go/data-handling) -## Temporal Go Technical Resources +## [Integrations](/develop/go/integrations) + +- [Google ADK integration](/develop/go/integrations/google-adk) + +## Temporal Go technical resources - [Go SDK Quickstart - Setup Guide](/develop/go/set-up-your-local-go) - [Go API Documentation](https://pkg.go.dev/go.temporal.io/sdk) diff --git a/docs/develop/go/integrations/index.mdx b/docs/develop/go/integrations/index.mdx index 1e144329ab..a8b37e1574 100644 --- a/docs/develop/go/integrations/index.mdx +++ b/docs/develop/go/integrations/index.mdx @@ -1,21 +1,22 @@ --- id: index -title: Integrations +title: Integrations - Go SDK sidebar_label: Integrations -toc_max_heading_level: 2 +description: This section covers integrations with the Go SDK +toc_max_heading_level: 4 keywords: + - Go SDK - integrations - ai frameworks - - agent frameworks tags: + - Go SDK + - Temporal SDKs - Integrations - - AI Frameworks - - Agent Frameworks -description: Integrations with other tools and services. --- import IntegrationsGrid from '@site/src/components/IntegrationsGrid'; The following integrations are available for the Temporal Go SDK. +You can also use the Temporal Go SDK's [Plugin system](/develop/plugins-guide) to build your own integrations. diff --git a/src/components/IntegrationsGrid/index.tsx b/src/components/IntegrationsGrid/index.tsx index 707af4bd92..b19649e926 100644 --- a/src/components/IntegrationsGrid/index.tsx +++ b/src/components/IntegrationsGrid/index.tsx @@ -10,12 +10,13 @@ import integrations, { type SDK, type Integration } from "./integrations-data"; import SdkSvg from "../elements/SdkSvgs/SdkSvg"; import styles from "./IntegrationsGrid.module.css"; -const ALL_SDKS: SDK[] = ["Java", "Python", "Ruby", "TypeScript"]; +const ALL_SDKS: SDK[] = ["Go", "Java", "Python", "Ruby", "TypeScript"]; const LANGUAGE_AGNOSTIC = "Language-agnostic"; type SdkFilter = SDK | typeof LANGUAGE_AGNOSTIC; const ALL_SDK_FILTERS: SdkFilter[] = [...ALL_SDKS, LANGUAGE_AGNOSTIC]; const SDK_BLOCK_NAMES: Record = { + Go: "goLangBlock", Java: "javaBlock", Python: "pythonBlock", Ruby: "rubyBlock", From e0166a528270982f79a306e47e8b822130420408 Mon Sep 17 00:00:00 2001 From: DABH Date: Mon, 20 Jul 2026 13:06:24 -0500 Subject: [PATCH 3/4] googleadk: install with @latest now that contrib/googleadk v0.1.0 is tagged --- docs/develop/go/integrations/google-adk.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/develop/go/integrations/google-adk.mdx b/docs/develop/go/integrations/google-adk.mdx index 3b96df0ae9..ac6993b9da 100644 --- a/docs/develop/go/integrations/google-adk.mdx +++ b/docs/develop/go/integrations/google-adk.mdx @@ -64,7 +64,7 @@ complete, runnable code. Install the `googleadk` contrib module: ```bash -go get go.temporal.io/sdk/contrib/googleadk +go get go.temporal.io/sdk/contrib/googleadk@latest ``` ```go From f2abc7361db4d766e6cdb61bb2c20c373bd09d8c Mon Sep 17 00:00:00 2001 From: DABH Date: Mon, 20 Jul 2026 13:48:44 -0500 Subject: [PATCH 4/4] googleadk: splice sample snippets now that samples-go#504 is merged Populate the page's seven snipsync blocks from the merged temporalio/samples-go googleadk samples (run via snipsync with the markers normalized to the repo's spacing convention), so the page ships with code instead of waiting for the next scheduled snipsync run. --- docs/develop/go/integrations/google-adk.mdx | 404 +++++++++++++++++++- 1 file changed, 397 insertions(+), 7 deletions(-) diff --git a/docs/develop/go/integrations/google-adk.mdx b/docs/develop/go/integrations/google-adk.mdx index ac6993b9da..cee828b798 100644 --- a/docs/develop/go/integrations/google-adk.mdx +++ b/docs/develop/go/integrations/google-adk.mdx @@ -82,7 +82,37 @@ Build the worker-side registry with `googleadk.NewActivities` and register it. T the Activity boundary; the API key is read worker-side. Disable the model SDK's own retries so Temporal's `RetryPolicy` is the single source of truth. - + +[googleadk/worker/main.go](https://github.com/temporalio/samples-go/blob/main/googleadk/worker/main.go) +```go +w := worker.New(c, adk.TaskQueue, worker.Options{}) + +w.RegisterWorkflow(adk.AgentWorkflow) +// Register GetWeather under the tool name the ActivityAsTool dispatches, so the +// agent's get_weather call resolves to this activity. +w.RegisterActivityWithOptions(adk.GetWeather, activity.RegisterOptions{Name: adk.WeatherToolName}) + +// Register the integration's model Activity. The real Gemini model lives here, +// behind the Activity boundary; the API key is read worker-side from the env +// and never crosses into the workflow. Disable the model SDK's own retries so +// Temporal's RetryPolicy is the single source of truth. +acts, err := googleadk.NewActivities(googleadk.Config{ + Models: map[string]googleadk.ModelFactory{ + adk.ModelName: func(ctx context.Context, name string) (model.LLM, error) { + // nil config reads GEMINI_API_KEY / GOOGLE_API_KEY from the env. + return gemini.NewModel(ctx, name, nil) + }, + }, +}) +if err != nil { + log.Fatalln("Unable to build googleadk activities", err) +} +acts.Register(w) + +if err := w.Run(worker.InterruptCh()); err != nil { + log.Fatalln("Unable to start worker", err) +} +``` `Config.Models` is optional for providers ADK's registry already knows (for example `gemini-*`): when a model name is @@ -94,20 +124,100 @@ retries, or override the default. Build the agent the ordinary ADK way, using `googleadk.NewModel` for the model and passing `googleadk.NewContext(ctx)` to `r.Run`. The `get_weather` tool is an ordinary Temporal Activity exposed to the agent with `googleadk.ActivityAsTool`. - + +[googleadk/workflow.go](https://github.com/temporalio/samples-go/blob/main/googleadk/workflow.go) +```go +func AgentWorkflow(ctx workflow.Context, question string) (string, error) { + weatherTool, err := googleadk.ActivityAsTool(GetWeather, googleadk.ActivityToolOptions{ + Name: WeatherToolName, + Description: "Get the current weather for a city.", + }) + if err != nil { + return "", err + } + + // Build the agent the ordinary ADK way. NewModel is a model.LLM that carries + // only the model name in-workflow; the real Gemini client lives worker-side. + root, err := llmagent.New(llmagent.Config{ + Name: "assistant", + Description: "a helpful weather assistant", + Model: googleadk.NewModel(ModelName), + Instruction: "Answer the user's question. Use the get_weather tool when asked about the weather.", + Tools: []tool.Tool{weatherTool}, + }) + if err != nil { + return "", err + } + + r, err := runner.New(runner.Config{ + AppName: "weather", + Agent: root, + SessionService: session.InMemoryService(), + AutoCreateSession: true, + }) + if err != nil { + return "", err + } + + // NewContext bridges the workflow.Context into the context ADK reads its + // determinism/executor seams from. Pass it straight to Run. + adkCtx := googleadk.NewContext(ctx) + msg := genai.NewContentFromText(question, genai.RoleUser) + + var answer string + for ev, err := range r.Run(adkCtx, "user-1", "session-1", msg, agent.RunConfig{}) { + if err != nil { + return "", err + } + if ev != nil && ev.Content != nil { + for _, p := range ev.Content.Parts { + if p != nil && p.Text != "" { + answer = p.Text + } + } + } + } + return answer, nil +} + +``` The tool itself is an ordinary Temporal Activity — register it on the worker as usual, and expose it to the agent with `ActivityAsTool` (its parameter schema is inferred from the argument type): - + +[googleadk/workflow.go](https://github.com/temporalio/samples-go/blob/main/googleadk/workflow.go) +```go +func GetWeather(ctx context.Context, in GetWeatherInput) (GetWeatherOutput, error) { + return GetWeatherOutput{City: in.City, Conditions: "sunny, 72°F"}, nil +} + +``` ### Start the Workflow Start the Workflow like any other and read its result: - + +[googleadk/starter/main.go](https://github.com/temporalio/samples-go/blob/main/googleadk/starter/main.go) +```go +question := "What's the weather in San Francisco?" +we, err := c.ExecuteWorkflow(context.Background(), workflowOptions, adk.AgentWorkflow, question) +if err != nil { + log.Fatalln("Unable to execute workflow", err) +} + +log.Println("Started workflow", "WorkflowID", we.GetID(), "RunID", we.GetRunID()) + +// Synchronously wait for the workflow completion. +var answer string +if err := we.Get(context.Background(), &answer); err != nil { + log.Fatalln("Unable to get workflow result", err) +} +log.Println("Agent answer:", answer) +``` ## Tools @@ -128,7 +238,89 @@ Build a coordinator agent with specialist `SubAgents`; ADK wires the parent/chil `transfer_to_agent` tool automatically. The entire tree — including the transfer hop — runs in the Workflow; only the model calls and any Activity-backed tools leave it. - + +[googleadk/multiagent/workflow.go](https://github.com/temporalio/samples-go/blob/main/googleadk/multiagent/workflow.go) +```go +func MultiAgentWorkflow(ctx workflow.Context, question string) (string, error) { + weatherTool, err := googleadk.ActivityAsTool(GetWeather, googleadk.ActivityToolOptions{ + Name: WeatherToolName, + Description: "Get the current weather for a city.", + }) + if err != nil { + return "", err + } + + // The weather specialist owns the get_weather tool. + weather, err := llmagent.New(llmagent.Config{ + Name: "weather", + Description: "answers questions about the current weather in a city", + Model: googleadk.NewModel(WeatherModelName), + Instruction: "You are a weather specialist. Use the get_weather tool to answer weather questions.", + Tools: []tool.Tool{weatherTool}, + }) + if err != nil { + return "", err + } + + // The jokes specialist just tells jokes. + jokes, err := llmagent.New(llmagent.Config{ + Name: "jokes", + Description: "tells a light-hearted joke", + Model: googleadk.NewModel(JokesModelName), + Instruction: "You are a comedian. Respond with a short, friendly joke.", + }) + if err != nil { + return "", err + } + + // The coordinator delegates to whichever specialist fits the question. ADK + // wires the parent/child relationship from SubAgents and exposes the built-in + // transfer_to_agent tool automatically. + coordinator, err := llmagent.New(llmagent.Config{ + Name: "coordinator", + Description: "routes the user's request to the right specialist", + Model: googleadk.NewModel(CoordinatorModelName), + Instruction: "You are a router. Delegate weather questions to the weather agent " + + "and requests for a joke to the jokes agent. Do not answer directly.", + SubAgents: []agent.Agent{weather, jokes}, + }) + if err != nil { + return "", err + } + + r, err := runner.New(runner.Config{ + AppName: "multiagent", + Agent: coordinator, + SessionService: session.InMemoryService(), + AutoCreateSession: true, + }) + if err != nil { + return "", err + } + + adkCtx := googleadk.NewContext(ctx) + msg := genai.NewContentFromText(question, genai.RoleUser) + + var answer string + for ev, err := range r.Run(adkCtx, "user-1", "session-1", msg, agent.RunConfig{}) { + if err != nil { + return "", err + } + if ev == nil || ev.Content == nil { + continue + } + // Keep the last non-empty text produced by any agent in the tree; after a + // transfer_to_agent hop this is the specialist's answer. + for _, p := range ev.Content.Parts { + if p != nil && p.Text != "" { + answer = p.Text + } + } + } + return answer, nil +} + +``` ## Human-in-the-loop tool confirmation @@ -139,7 +331,97 @@ A sensitive tool calls ADK's `ctx.RequestConfirmation(hint, payload)`, which end the agent with `googleadk.ConfirmationResponse`. Because the wait is durable, the Workflow can sit idle for days and survive worker restarts — when the approval signal arrives, the agent resumes exactly where it paused. - + +[googleadk/humanintheloop/workflow.go](https://github.com/temporalio/samples-go/blob/main/googleadk/humanintheloop/workflow.go) +```go +func ApprovalWorkflow(ctx workflow.Context, request string) (Result, error) { + delTool, err := functiontool.New[DeleteArgs, map[string]any]( + functiontool.Config{ + Name: DeleteToolName, + Description: "Delete a named resource. Requires human confirmation before it runs.", + }, + deleteResource, + ) + if err != nil { + return Result{}, err + } + + root, err := llmagent.New(llmagent.Config{ + Name: "assistant", + Description: "an assistant that can delete resources with human approval", + Model: googleadk.NewModel(ModelName), + Instruction: "Use the delete_resource tool when the user asks to delete something.", + Tools: []tool.Tool{delTool}, + }) + if err != nil { + return Result{}, err + } + + r, err := runner.New(runner.Config{ + AppName: "hitl", + Agent: root, + SessionService: session.InMemoryService(), + AutoCreateSession: true, + }) + if err != nil { + return Result{}, err + } + + adkCtx := googleadk.NewContext(ctx) + msg := genai.NewContentFromText(request, genai.RoleUser) + + var res Result + // Drive the run in passes: each Run call is one pass over the same session. A + // pass either completes (no pending confirmation) or pauses awaiting a human. + for { + var events []*session.Event + for ev, err := range r.Run(adkCtx, "user-1", "session-1", msg, agent.RunConfig{}) { + if err != nil { + return Result{}, err + } + if ev == nil { + continue + } + events = append(events, ev) + if ev.Content != nil { + for _, p := range ev.Content.Parts { + if p != nil && p.Text != "" { + res.Answer = p.Text + } + } + } + } + + pending := googleadk.PendingConfirmations(events) + if len(pending) == 0 { + // The agent finished without (further) confirmations needed. + return res, nil + } + + // The agent paused. Durably wait for the human's decision to arrive as a + // Temporal signal. This is the whole point: the workflow can sit here for + // as long as it takes — across worker restarts — without losing state. + // + // This handles one pending confirmation per pass — the recommended + // pattern (see the googleadk.ConfirmationResponse docs): resuming + // several decisions at once can re-dispatch the approved tool calls in + // an order that is not replay-stable. Any other pending confirmations + // simply surface again on the next pass. + var decision googleadk.ConfirmationDecision + workflow.GetSignalChannel(ctx, googleadk.ConfirmationSignalName).Receive(ctx, &decision) + res.Approved = decision.Confirmed + + // Match the decision to the pending confirmation and resume the run with + // it as the next message. ADK re-dispatches (or blocks) the original tool + // call based on Confirmed. + if decision.FunctionCallID == "" { + decision.FunctionCallID = pending[0].FunctionCallID + } + msg = googleadk.ConfirmationResponse(decision) + } +} + +``` ## Continue-as-new for long conversations @@ -149,7 +431,115 @@ A conversation's history lives in the ADK session. To keep a Workflow's history `googleadk.ImportSession`. `SessionSnapshot` is JSON-serializable (session-scoped state plus the full event history), so every value in session state and every tool result must be JSON-encodable. - + +[googleadk/chat/workflow.go](https://github.com/temporalio/samples-go/blob/main/googleadk/chat/workflow.go) +```go +func ChatWorkflow(ctx workflow.Context, in ChatInput) error { + // A fresh in-memory session service, kept in a local so we can Export it later. + svc := session.InMemoryService() + + adkCtx := googleadk.NewContext(ctx) + + // Resume a prior conversation if this run was continued-as-new. + if in.Snapshot != nil { + if _, err := googleadk.ImportSession(adkCtx, svc, in.Snapshot); err != nil { + return err + } + } + + root, err := llmagent.New(llmagent.Config{ + Name: "assistant", + Description: "a friendly conversational assistant", + Model: googleadk.NewModel(ModelName), + Instruction: "You are a helpful assistant. Answer the user, using the conversation history for context.", + }) + if err != nil { + return err + } + + r, err := runner.New(runner.Config{ + AppName: AppName, + Agent: root, + SessionService: svc, + AutoCreateSession: true, + }) + if err != nil { + return err + } + + turns := 0 + // One agent turn runs at a time: serialize concurrent Updates so they can't + // interleave on the shared ADK session. + busy := false + + err = workflow.SetUpdateHandlerWithOptions( + ctx, + SendMessageUpdateName, + func(ctx workflow.Context, text string) (string, error) { + if err := workflow.Await(ctx, func() bool { return !busy }); err != nil { + return "", err + } + busy = true + defer func() { busy = false }() + + // Build the ADK context from this Update handler's own workflow.Context so + // the model Activity is scheduled on the handler's coroutine. + turnCtx := googleadk.NewContext(ctx) + var answer string + msg := genai.NewContentFromText(text, genai.RoleUser) + for ev, err := range r.Run(turnCtx, UserID, SessionID, msg, agent.RunConfig{}) { + if err != nil { + return "", err + } + if ev == nil || ev.Content == nil { + continue + } + for _, p := range ev.Content.Parts { + if p != nil && p.Text != "" { + answer = p.Text + } + } + } + turns++ + return answer, nil + }, + workflow.UpdateHandlerOptions{ + Validator: func(ctx workflow.Context, text string) error { + if text == "" { + return fmt.Errorf("message must not be empty") + } + return nil + }, + }, + ) + if err != nil { + return err + } + + // Serve messages until Temporal suggests continue-as-new (history getting large) + // or the demo turn cap is reached. + if err := workflow.Await(ctx, func() bool { + return workflow.GetInfo(ctx).GetContinueAsNewSuggested() || (in.MaxTurns > 0 && turns >= in.MaxTurns) + }); err != nil { + return err + } + + // Let any in-flight Update finish so its turn is captured in the snapshot. + if err := workflow.Await(ctx, func() bool { return workflow.AllHandlersFinished(ctx) }); err != nil { + return err + } + + snap, err := googleadk.ExportSession(adkCtx, svc, AppName, UserID, SessionID) + if err != nil { + return err + } + return workflow.NewContinueAsNewError(ctx, ChatWorkflow, ChatInput{ + Snapshot: snap, + MaxTurns: in.MaxTurns, + }) +} + +``` ## Streaming