From e9174a1efe57bcf977667b2bafd37d3264019c91 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein <2359976+ToniCorinne@users.noreply.github.com> Date: Tue, 30 Jun 2026 09:27:36 -0600 Subject: [PATCH 1/6] Add ADK Go v2.0.0 documentation and graph workflow examples (#1870) * adding go getting started to 2.0 docs * updating graphs docs for go; adding in workflow example go snippets * Updating HiTL docs for Go 2.0 * updating Go getting started guide for v2 * Updating Routes docs for Go v2 * Updating data handling docs for Go 2.0 * Updating dynamic workflows docs for Go 2.0 * fix(docs): set ADK Go 2.0 GA date on the 2.0 landing page * fix(docs): standardize ADK Go 2.0 install/import on /v2 @ v2.0.0 * updating announcement banner * routes snippets updates * 2.0 install instructions clean-up * updating collaboration docs for go v2 + new example snippet --------- Co-authored-by: Karol Piotrowicz --- .gitignore | 4 + docs/2.0/index.md | 145 +++- docs/agents/llm-agents.md | 25 + docs/agents/workflow-agents/index.md | 6 +- docs/agents/workflow-agents/loop-agents.md | 6 +- .../agents/workflow-agents/parallel-agents.md | 6 +- .../workflow-agents/sequential-agents.md | 6 +- docs/get-started/go.md | 35 +- docs/get-started/installation.md | 24 +- docs/graphs/data-handling.md | 595 ++++++++------ docs/graphs/dynamic.md | 743 +++++++++++------- docs/graphs/human-input.md | 271 ++++--- docs/graphs/index.md | 258 +++--- docs/graphs/routes.md | 536 +++++++++---- docs/release-notes.md | 7 + docs/workflows/collaboration.md | 67 +- examples/go/go.mod | 5 +- examples/go/go.sum | 6 +- .../go/snippets/graphs/data-handling/main.go | 412 ++++++++++ examples/go/snippets/graphs/dynamic/main.go | 501 ++++++++++++ .../go/snippets/graphs/human-input/main.go | 336 ++++++++ examples/go/snippets/graphs/index/main.go | 200 +++++ examples/go/snippets/graphs/routes/main.go | 399 ++++++++++ .../snippets/workflows/collaboration/main.go | 156 ++++ overrides/main.html | 6 +- tools/go-snippets/files_to_test.txt | 6 + tools/go-snippets/runner.sh | 15 +- 27 files changed, 3826 insertions(+), 950 deletions(-) create mode 100644 examples/go/snippets/graphs/data-handling/main.go create mode 100644 examples/go/snippets/graphs/dynamic/main.go create mode 100644 examples/go/snippets/graphs/human-input/main.go create mode 100644 examples/go/snippets/graphs/index/main.go create mode 100644 examples/go/snippets/graphs/routes/main.go create mode 100644 examples/go/snippets/workflows/collaboration/main.go diff --git a/.gitignore b/.gitignore index c2a6bb499d..4e568a03f7 100644 --- a/.gitignore +++ b/.gitignore @@ -199,3 +199,7 @@ docs/site # Ignore a local build but keep the directory /site/* !/site/.gitkeep + +# Compiled Go example binaries (accidentally committed build artifacts) +/examples/go/index +/examples/go/main diff --git a/docs/2.0/index.md b/docs/2.0/index.md index c008000585..ceb3c2f024 100644 --- a/docs/2.0/index.md +++ b/docs/2.0/index.md @@ -5,10 +5,14 @@ hide: # Welcome to ADK 2.0 +
+ Supported in ADKPython v2.0.0Go v2.0.0 +
+ ADK 2.0 introduces powerful tools for building sophisticated AI agents, and helps you structure agents to execute challenging tasks with more control, -predictability, and reliability. ADK 2.0 is available for Python and includes -the following key features: +predictability, and reliability. ADK 2.0 is available for Python and Go and +includes the following key features: - [**Graph-based workflows**](/graphs/): Build deterministic agent workflows with more control over how tasks are routed and executed. @@ -28,6 +32,10 @@ to build agents with ADK 2.0! ADK Python 2.0 is released for general availability as of May 19, 2026. +!!! tip "ADK Go v2.0.0 GA release" + + ADK Go 2.0 is released for general availability as of June 30, 2026. + ## ADK Python 1.x compatibility ADK 2.0 is designed to be compatible with agents developed with ADK 1.x @@ -47,7 +55,7 @@ architecture, your Agents, Tools, and Functions are evaluated as individual following breaking changes and migration steps to ensure a smooth transition for your production applications. -### Event Schema & Custom Session Databases +### Event Schema & Custom Session Storage ADK 2.0 introduces new fields `node_info` and `output` to the core ***Event*** schema to track graph state and workflow outputs. @@ -123,8 +131,8 @@ so the framework can evaluate them against your configured ***RetryConfig***, such as `RetryConfig(max_attempts=3)`. Never catch ***BaseException*** unless you are explicitly re-raising the exception. -If you encounter additional ADK 1.0 to ADK 2.0 incompatibilities, report them -through the +If you encounter additional ADK Python 1.0 to ADK 2.0 incompatibilities, report +them through the [issue tracker](https://github.com/google/adk-python/issues/new?template=bug_report.md&labels=v2). @@ -176,6 +184,119 @@ To install the latest version of ADK 1.x, follow these steps: source .venv/bin/activate ``` +## ADK Go 1.x compatibility + +ADK Go 2.0 is designed to be compatible with agents developed with ADK Go 1.x +releases. However, there are a few breaking changes you should be aware of +before upgrading an ADK Go 1.x project to ADK Go 2.0. + +!!! warning "Breaking changes: ADK Go 1.x to 2.0 incompatibilities" + + There are several known incompatibilities and breaking changes introduced + with ADK Go v2.0.0. Before upgrading, review these changes and take + mitigation steps, if necessary. + +The ADK Go 2.0 release introduces the Workflow Runtime, transitioning ADK Go +from a hierarchical agent executor to a graph-based execution engine. In this +new architecture, your Agents, Tools, and Functions are evaluated as individual +*nodes* within a workflow graph. If you are upgrading from ADK Go 1.x, review +the following breaking changes and migration steps. + +### Module import path + +ADK Go 2.0 uses a new major version module path. You must update all import +paths in your Go source files and your `go.mod` file. + +* **1.x import path:** `google.golang.org/adk` +* **2.0 import path:** `google.golang.org/adk/v2` + +**Migration action:** Run `go get google.golang.org/adk/v2` and update +all import statements in your source files from `google.golang.org/adk/...` to +`google.golang.org/adk/v2/...`. + +### Agent Execution: Agent interface changes + +In ADK Go 1.x, agents implemented the `agent.Agent` interface by providing a +`Run` method. In ADK Go 2.0, agents are evaluated as individual *nodes* within +the new Workflow Graph engine. + +* **Execution driver custom overrides:** Custom agent types that override + internal execution behavior may no longer work as expected. The Workflow + Graph engine manages execution scheduling and event emission, and custom + implementations that bypass these mechanisms are silently ignored. + +**Migration action:** Move custom execution logic into standardized +`BeforeAgentCallback` and `AfterAgentCallback` hooks to safely inject custom +logic into the execution lifecycle. + +### Event Construction: `session.NewEvent` signature change + +`session.NewEvent` now requires a `context.Context` as its first argument: + +```go +// Before (ADK Go 1.x) +ev := session.NewEvent(ctx.InvocationID()) +// or +ev := session.NewEventWithContext(ctx, ctx.InvocationID()) + +// After (ADK Go 2.0) +ev := session.NewEvent(ctx, ctx.InvocationID()) +``` + +The event ID and timestamp are now obtained through the `platform` package, +so a time or UUID provider installed on `ctx` controls them. This lets workflow +engines produce deterministic, replay-safe events. The previous +parameterless-context form and the temporary `NewEventWithContext` helper are +removed. + +**Migration action:** Pass the context already in scope as the first argument +to `session.NewEvent`. Any `context.Context` works — the `ctx` of an agent, +tool, or callback (which embed `context.Context`), a request context, or in +tests, `t.Context()`. If a helper that calls `NewEvent` does not yet receive a +context, add a `ctx context.Context` parameter and thread it down from the +caller. Avoid creating a new `context.Background()` mid-call-chain; reserve +that for `main`, `init`, and top-level test setup. + +### Event Schema & Custom Session Storage + +ADK Go 2.0 adds five new fields to the core ***Event*** struct to support +graph routing, workflow state, and human-in-the-loop pausing: + +| Go field | Serialized name | Purpose | +|---|---|---| +| `IsolationScope string` | `isolationScope` (`json:"isolationScope,omitempty"`) | Restricts which agent contexts see this event in LLM prompt history. | +| `Routes []string` | `Routes` (no JSON tag) | Routing keys emitted by a node to drive conditional edge dispatch. | +| `RequestedInput *RequestInput` | `RequestedInput` (no JSON tag) | Signals that a workflow node is pausing for human input. | +| `Output any` | `Output` (no JSON tag) | Generic data output from a workflow node. | +| `NodeInfo *NodeInfo` | `nodeInfo` (`json:"nodeInfo,omitempty"`) | Workflow-node metadata identifying which node emitted the event. | + +* **Custom session storage:** If you have implemented a custom + `session.Service`, such as storing sessions in your own SQL or NoSQL + databases with rigid schemas, your underlying database schema must be + updated to accommodate all five new fields. Inserting a 2.0 ***Event*** + into a rigid 1.x database table causes insertion or deserialization + failures. *However, if your custom session service stores events as + serialized JSON blobs, you do not need to update your schema.* + +**Migration action:** Update your database schemas and downstream client +validators to expect and store the five new fields on all Event payloads. +Pay particular attention to `Routes`, `RequestedInput`, and `Output`, which +have no JSON struct tags and therefore serialize under their Go field names +exactly as shown above. + +If you encounter additional ADK Go 1.0 to ADK 2.0 incompatibilities, report +them through the +[issue tracker](https://github.com/google/adk-go/issues/new?template=bug_report.md&labels=v2). + +### Installing ADK Go 1.x {#install-go} + +If you want to continue using ADK Go 1.x and are not yet ready to upgrade to +ADK Go 2.0, pin your dependency to the 1.x release line: + +```shell +go get google.golang.org/adk@v1 +``` + ## Next steps Read the developer guides for building agents with ADK 2.0 features: @@ -186,8 +307,14 @@ Read the developer guides for building agents with ADK 2.0 features: Check out these ADK 2.0 code samples for testing and inspiration: -- [**Workflow samples**](https://github.com/google/adk-python/tree/v2/contributing/workflow_samples) -- [**Collaborative task samples**](https://github.com/google/adk-python/tree/v2/contributing/task_samples) +=== "Python" + + - [**Workflow samples**](https://github.com/google/adk-python/tree/main/contributing/samples/workflows) + - [**Collaborative task samples**](https://github.com/google/adk-python/tree/main/contributing/samples/multi_agent) + +=== "Go" + + - [**All workflow agents samples**](https://github.com/google/adk-go/tree/main/examples/workflow) + - [**Collaborative task sample**](https://github.com/google/adk-go/tree/main/examples/multiagent/collaboration) -Thanks for checking out ADK 2.0! We look forward to your -[feedback](https://github.com/google/adk-python/issues/new?template=feature_request.md&labels=v2)! +Thanks for checking out ADK 2.0! We look forward to your feedback — let us know on [ADK Go](https://github.com/google/adk-go/issues/new) or [ADK Python](https://github.com/google/adk-python/issues/new). diff --git a/docs/agents/llm-agents.md b/docs/agents/llm-agents.md index df52911f82..6ceb0c3351 100644 --- a/docs/agents/llm-agents.md +++ b/docs/agents/llm-agents.md @@ -544,6 +544,28 @@ Control whether the agent receives the prior conversation history. .build(); ``` +!!! note "Go v2.0.0: agent execution modes" + + ADK Go v2.0.0 introduces an explicit `Mode` field on `llmagent.Config` that + controls how the agent runs when used inside a graph-based or dynamic + workflow. Three modes are available: + + - **`ModeChat`** (default for an agent used as a sub-agent): The agent + participates in a multi-turn conversation with the user and is reachable + from peer agents via `transfer_to_agent`. + - **`ModeSingleTurn`** (default for an agent used as a node in a + workflow): The agent completes its task in a single turn without + chatting with the user. + - **`ModeTask`**: A task agent that chats with the user to accomplish a + task — in contrast to `ModeSingleTurn`, it can interact with the user + across turns to complete the work. + + When you wrap an `llmagent` with `workflow.NewAgentNode`, the workflow + engine automatically sets the mode to `ModeSingleTurn` if no mode is + specified — equivalent to Python's `mode="single_turn"` on an agent used + as a workflow node. For more information on composing agents in graph-based + workflows, see [Graph-based agent workflows](/graphs/). + ### Planner
@@ -795,3 +817,6 @@ the following: planning (`planner`), controlling agent transfer (`disallow_transfer_to_parent`, `disallow_transfer_to_peers`), and system-wide instructions (`global_instruction`). See [Custom agent workflows](/agents/custom-agents/). +* **Graph-based workflows:** Compose LLM agents as steps in deterministic, + graph-based pipelines using [Graph-based agent workflows](/graphs/). In Go v2.0.0, use + `workflow.NewAgentNode` to wrap any LLM agent as a workflow node. diff --git a/docs/agents/workflow-agents/index.md b/docs/agents/workflow-agents/index.md index ef65bea433..0004855e90 100644 --- a/docs/agents/workflow-agents/index.md +++ b/docs/agents/workflow-agents/index.md @@ -12,11 +12,11 @@ how and when other agents run, defining the control flow of a process. !!! note "Alternative: graph-based workflows" - Starting in ADK 2.0, template workflows have been superseded + Starting in ADK 2.0 for Python and Go, template workflows have been superseded by more flexible workflow structures, including - [graph-based workflows](/workflows/graphs/) and - [dynamic workflows](/workflows/dynamic/). + [graph-based workflows](/graphs/) and + [dynamic workflows](/graphs/dynamic/). These workflow architectures provide more control, flexibility and capability to evolve your agent workflows over time. diff --git a/docs/agents/workflow-agents/loop-agents.md b/docs/agents/workflow-agents/loop-agents.md index 89ba8ff1d3..9a692893c1 100644 --- a/docs/agents/workflow-agents/loop-agents.md +++ b/docs/agents/workflow-agents/loop-agents.md @@ -16,11 +16,11 @@ the ***LoopAgent*** object you define. !!! note "Alternative: graph-based workflows" - Starting in ADK 2.0, templated workflows have been superseded + Starting in ADK 2.0 for Python and Go, templated workflows have been superseded by more flexible workflow structures, including - [graph-based workflows](/workflows/graphs/) and - [dynamic workflows](/workflows/dynamic/). + [graph-based workflows](/graphs/) and + [dynamic workflows](/graphs/dynamic/). ### Example scenario diff --git a/docs/agents/workflow-agents/parallel-agents.md b/docs/agents/workflow-agents/parallel-agents.md index a1e41bf118..b80217bed6 100644 --- a/docs/agents/workflow-agents/parallel-agents.md +++ b/docs/agents/workflow-agents/parallel-agents.md @@ -23,11 +23,11 @@ ultimately managed by the ***ParallelAgent*** object you define. !!! note "Alternative: graph-based workflows" - Starting in ADK 2.0, templated workflows have been superseded + Starting in ADK 2.0 for Python and Go, templated workflows have been superseded by more flexible workflow structures, including - [graph-based workflows](/workflows/graphs/) and - [dynamic workflows](/workflows/dynamic/). + [graph-based workflows](/graphs/) and + [dynamic workflows](/graphs/dynamic/). ### How it works diff --git a/docs/agents/workflow-agents/sequential-agents.md b/docs/agents/workflow-agents/sequential-agents.md index 2b490b1f8a..9d9e6cf649 100644 --- a/docs/agents/workflow-agents/sequential-agents.md +++ b/docs/agents/workflow-agents/sequential-agents.md @@ -16,11 +16,11 @@ object you define. !!! note "Alternative: graph-based workflows" - Starting in ADK 2.0, templated workflows have been superseded + Starting in ADK 2.0 for Python and Go, templated workflows have been superseded by more flexible workflow structures, including - [graph-based workflows](/workflows/graphs/) and - [dynamic workflows](/workflows/dynamic/). + [graph-based workflows](/graphs/) and + [dynamic workflows](/graphs/dynamic/). ### Example scenario diff --git a/docs/get-started/go.md b/docs/get-started/go.md index 31a1bffe80..e1ba724d55 100644 --- a/docs/get-started/go.md +++ b/docs/get-started/go.md @@ -3,8 +3,15 @@ This guide shows you how to get up and running with Agent Development Kit for Go. Before you start, make sure you have the following installed: -* Go 1.24.4 or later -* ADK Go v0.2.0 or later +* Go 1.25 or later +* ADK Go v2.0.0 or later + +!!! tip "What's new in ADK Go 2.0" + + ADK Go 2.0 introduces graph-based workflow agents, parallel and loop + execution primitives, and Human-in-the-Loop tool confirmation. See the + [ADK 2.0 release page](/2.0/) for the full list of new features and + migration guidance. ## Create an agent project @@ -48,13 +55,13 @@ import ( "log" "os" - "google.golang.org/adk/agent" - "google.golang.org/adk/agent/llmagent" - "google.golang.org/adk/cmd/launcher" - "google.golang.org/adk/cmd/launcher/full" - "google.golang.org/adk/model/gemini" - "google.golang.org/adk/tool" - "google.golang.org/adk/tool/geminitool" + "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/geminitool" "google.golang.org/genai" ) @@ -94,11 +101,13 @@ func main() { ### Configure project and dependencies -Use the `go mod` command to initialize the project modules and install the -required packages based on the `import` statement in your agent code file: +Initialize your module, add ADK Go 2.0 as a pinned dependency, then let `go mod +tidy` resolve the remaining packages based on the `import` statements in your +agent code file: ```console go mod init my-agent/main +go get google.golang.org/adk/v2 go mod tidy ``` @@ -178,4 +187,6 @@ upper left corner and type a request. Now that you have ADK installed and your first agent running, try building your own agent with our build guides: -* [Build your agent](/tutorials/) +* [Build your agent](/tutorials/) +* [Build graph-based workflows](/graphs/) +* [ADK Go workflow agents](/agents/workflow-agents/) diff --git a/docs/get-started/installation.md b/docs/get-started/installation.md index e90b25ad0c..b87886b334 100644 --- a/docs/get-started/installation.md +++ b/docs/get-started/installation.md @@ -56,6 +56,8 @@ across supported languages. For a guided introduction, start with the === "Go" + **Prerequisites:** Go 1.25 or later is required for ADK Go v2.0.0. + **Create a new Go module** If you are starting a new project, you can create a new Go module: @@ -64,18 +66,30 @@ across supported languages. For a guided introduction, start with the go mod init example.com/my-agent ``` - **Install ADK** + **Install ADK Go v2.0.0** - To add the ADK to your project, run the following command: + To add ADK Go v2.0.0 to your project, run the following command: ```shell - go get google.golang.org/adk + go get google.golang.org/adk/v2 ``` - This will add the ADK as a dependency to your `go.mod` file. + This will add ADK Go v2.0.0 as a dependency to your `go.mod` file. (Optional) Verify your installation by checking your `go.mod` file for the - `google.golang.org/adk` entry. + `google.golang.org/adk/v2` entry. + + ??? tip "Still using ADK Go v1.x?" + + If you are not yet ready to upgrade to v2.0.0, you can continue using + the v1.x release line: + + ```shell + go get google.golang.org/adk@v1 + ``` + + See the [ADK 2.0 release page](/2.0/) for upgrade guidance, including + breaking changes and migration steps for ADK Go 1.x projects. === "Java" diff --git a/docs/graphs/data-handling.md b/docs/graphs/data-handling.md index b2858b660e..ff02684041 100644 --- a/docs/graphs/data-handling.md +++ b/docs/graphs/data-handling.md @@ -1,235 +1,386 @@ # Data handling for agent workflows
- Supported in ADKPython v2.0.0 + Supported in ADKPython v2.0.0Go v2.0.0
Structuring and managing data between agents and graph-based nodes is critical for building reliable processes with ADK. This guide explains data handling within graph-based workflows and collaboration agents, including how information -is transmitted and received between graph nodes using ***Events***. It covers -the essential parameters for events, data, content, and state, and explains how -to implement structured data transfer for both function and agent nodes using -data format schemas and specific instruction syntax. - -## Workflow graph Events - -Within a graph-based workflow, you pass data using ***Events***. All execution -*nodes* in a workflow graph consume and emit Events. This section covers the -basics of transmitting and receiving data between nodes in a ***Workflow***. -Events have specific parameters for transmitting different types of data between -nodes. The key parameters for node data handling are as follows: - -- **`output`**: Parameter for passing information between *nodes*. -- **`message`**: Data intended as a response to a user. -- **`state`**: Data automatically persisted across nodes via ***Events*** - throughout an ADK session. - -Events also carry additional information about the workflow, including the -source node of the Event. - -### Node input and output with Events - -Each node in a graph receives and transmits data through the ***Event*** class. -Use the ***yield*** syntax to hand off data to the next node, as shown in the -following code snippet: - -```python -from google.adk import Event - -def my_function_node(node_input: str): - output_value = node_input.upper() - return Event(output=output_value) # "THE RESULT" -``` - -Use the ***return*** syntax when outputting ***Event*** data that does not -require additional processing. When emitting data that requires additional -processing, or if you are generating more than one data item, you can use more -than one ***yield*** command. Each ***yield*** call adds to a list of data -objects on the Event which is passed to the next node of a graph. A ***return*** -or ***yield*** command without a parameter passes a `None` value to the next -node. - -### Event `output` parameter - -The ***output*** parameter of an ***Event*** is the standard way to pass data to -the next node of a graph. The next node receives a ***node input*** object -containing the data, as shown in the following code sample: - -```python -def my_function_node_1(): - return Event(output="The Result") - -def my_function_node_2(node_input: str): - output_value = node_input.lower() - return Event(output=output_value) # "the result" -``` - -You can pass longer, structured data in a serializable format, as shown in this -code sample: - -```python -def my_function_node_3(): - yield Event( - output={ - "city_name": "Paris", - "city_time": "10:10 AM", - }, +is transmitted and received between graph nodes. It covers the essential +parameters for passing data, content, and state, and explains how to implement +structured data transfer for both function and agent nodes using data format +schemas and specific instruction syntax. + +## Workflow data flow + +Within a graph-based workflow, nodes pass data to downstream steps through +events. A step writes its output to a named event field, and the next step +receives it as its typed input. + +=== "Python" + + In Python, data is exchanged between graph nodes using ***Events***. The key + parameters for node data handling are: + + - **`output`**: Parameter for passing information between *nodes*. + - **`message`**: Data intended as a response to a user. + - **`state`**: Data automatically persisted across nodes via ***Events*** + throughout an ADK session. + +=== "Go" + + In ADK Go v2.0.0, the data-passing mechanism depends on which agent style + you use: + + **workflow package** (`FunctionNode`, `AgentNode`, `DynamicNode`): nodes + communicate through `session.Event` fields, mirroring Python closely: + + - **`Event.Output`**: the node's return value, set automatically by the + framework when a `FunctionNode` returns a non-`*genai.Content` value. + The successor node receives this as its typed `input` parameter. + - **`Event.Routes`**: routing keys set explicitly by an emitting node to + select which conditional edge to follow — the Go equivalent of + Python's `Event(route=...)`. + - **`Event.NodeInfo`**: scheduler metadata (`path`, `MessageAsOutput`, + `OutputFor`). Set by the workflow engine; nodes do not set this + directly. + + **Prebuilt workflow agents** (`sequentialagent`, `parallelagent`, + `loopagent`): these agents communicate through session state: + + - **`OutputKey`** on `llmagent.Config`: the framework writes the agent's + final text response to `state[OutputKey]` after each turn. + - **`ctx.Session().State().Set` / `.Get`**: write or read arbitrary + values from state inside custom code. + - **`{key}` in `Instruction`**: the framework substitutes `state["key"]` + into the prompt before calling the model. + + State keys may carry a prefix that controls their lifetime and scope: + + | Prefix constant | Prefix string | Scope | + |---|---|---| + | `session.KeyPrefixApp` | `"app:"` | Shared across all users and sessions for the app | + | `session.KeyPrefixUser` | `"user:"` | Tied to the user, shared across their sessions | + | `session.KeyPrefixTemp` | `"temp:"` | Discarded after the current invocation ends | + | *(none)* | — | Persists for the lifetime of the session | + +### Node output + +Each step in a workflow produces output for its successor. + +=== "Python" + + Use the ***return*** or ***yield*** syntax to hand off data to the next node: + + ```python + from google.adk import Event + + def my_function_node(node_input: str): + output_value = node_input.upper() + return Event(output=output_value) # "THE RESULT" + ``` + + Use the ***return*** syntax when outputting ***Event*** data that does not + require additional processing. When emitting data that requires additional + processing, or if you are generating more than one data item, you can use + more than one ***yield*** command. Each ***yield*** call adds to a list of + data objects on the Event which is passed to the next node of a graph. A + ***return*** or ***yield*** command without a parameter passes a `None` value + to the next node. + +=== "Go" + + **workflow package**: a `FunctionNode` simply returns a typed Go value. + The framework automatically wraps the return value in a `session.Event` + and sets `Event.Output`. The successor node receives this value as its + typed `input` parameter — no manual event construction needed: + + ```go + --8<-- "examples/go/snippets/graphs/data-handling/main.go:event-output" + ``` + + **Prebuilt workflow agents**: use `OutputKey` on `llmagent.Config` to + save an agent's text response to session state, then reference it with + `{key}` in downstream agents' `Instruction` templates: + + ```go + --8<-- "examples/go/snippets/graphs/data-handling/main.go:output-key" + ``` + +### Node output: passing structured data + +=== "Python" + + You can pass longer, structured data in a serializable format: + + ```python + def my_function_node_3(): + yield Event( + output={ + "city_name": "Paris", + "city_time": "10:10 AM", + }, + ) + ``` + + !!! warning "Caution: Event.output limitation" + + Nodes are only allowed to emit a single ***Event.output*** data payload + per execution. This limitation means that while you can use more than + one ***yield*** in a node, having two or more ***yield*** commands with + an ***Event.output*** results in a runtime error. + +=== "Go" + + **workflow package**: a `FunctionNode` can return any JSON-serializable + Go struct. The framework serializes it into `Event.Output` and + deserializes it back into the successor node's typed `input` parameter. + There is no single-payload restriction — each node has exactly one typed + return value: + + ```go + --8<-- "examples/go/snippets/graphs/data-handling/main.go:structured-output" + ``` + + **Prebuilt workflow agents**: use multiple `OutputKey` values, one per + agent, to store individual fields in session state. Downstream agents + read each field independently via `{key}` in their `Instruction`. + +### Routing output + +=== "Python" + + Use the `route` parameter of an ***Event*** to drive conditional edge + dispatch: + + ```python + def router(node_input: str): + return Event(route="BUG") + ``` + +=== "Go" + + **workflow package**: an emitting `FunctionNode` constructs a + `session.Event` directly, sets `Event.Routes` to the desired route keys, + and sets `Event.Output` to forward the payload to the successor. The + workflow engine reads `Event.Routes` at dispatch time to select the + matching edge: + + ```go + --8<-- "examples/go/snippets/graphs/data-handling/main.go:routing-output" + ``` + +### User-facing messages + +=== "Python" + + Use the ***message*** parameter of an ***Event*** to send a response to a + user rather than pass data to the next node: + + ```python + async def user_message(node_input: str): + """Tell user research process is starting.""" + yield Event(message="Beginning research process...") + ``` + +=== "Go" + + **workflow package**: to emit a user-visible message without advancing + the node's typed output, set `Event.Content` on an intermediate event + emitted via the `emit` callback in an `EmittingFunctionNode`. The + terminal return value (or `nil`) controls `Event.Output`. + + **Prebuilt workflow agents**: any `llmagent` step automatically emits its + model response as a user-facing event. For non-LLM steps, write a custom + `Run` function on an `agent.Agent` that yields events whose + `LLMResponse.Content` contains the text. + +### Session state and state scopes + +Session state persists data across turns within a session. It is the primary +data-sharing mechanism for the prebuilt workflow agents, and is also available +inside tools and callbacks regardless of which agent style you use. + +=== "Python" + + Use the ***state*** parameter of an ***Event*** to maintain values across + nodes. Nodes can modify state values, and the modified state values are + available to downstream nodes: + + ```python + async def init_state_node(attempts: int = 0): + yield Event( + state={ + "attempts": attempts, + }, + ) + + async def task_attempt_node(node_input: Content, attempts: int): + yield Event( + state={ + "attempts": attempts + 1, + }, + ) + + async def read_state_node(ctx: Context): + print(f"attempts state: {ctx.state}") # attempts state: attempts: 1 + + root_agent = Workflow( + name="root_agent", + edges=[("START", init_state_node, task_attempt_node, read_state_node)], + ) + ``` + + !!! warning "Caution: `state` property data limitations" + + The state parameter *should not be used to persist large amounts of + data* between nodes. Use artifacts or other data persistence mechanisms, + such as database Tools, to persist large data resources during the life + cycle of a Workflow. + +=== "Go" + + State is written with `ctx.Session().State().Set(key, value)` and read + with `.Get(key)`. The `session` package defines prefix constants that map + to the same lifetime scopes as Python's state parameter. This pattern + applies to prebuilt workflow agents and to tools and callbacks in any + agent style: + + ```go + --8<-- "examples/go/snippets/graphs/data-handling/main.go:state-scopes" + ``` + + !!! warning "Caution: state data limitations" + + Session state is a lightweight key-value store. Do not use it to persist + large payloads such as file contents or binary data. Use ADK artifacts + or external storage tools instead. + + !!! tip "workflow package: prefer Event.Output over state" + + For the `workflow` package (`FunctionNode`, `AgentNode`, `DynamicNode`), + pass data between nodes by returning typed values — the framework sets + `Event.Output` automatically. Only use `State().Set` when you need to + share values with tools, callbacks, or agent `Instruction` templates. + +## Constrain node data with schemas + +You can set input and output data schemas to constrain the data formats +accepted and produced by any agent node. + +=== "Python" + + Use `input_schema` and `output_schema` with a class that extends + ***BaseModel*** to constrain any agent's input and output: + + ```python + from google.adk import Agent + from pydantic import BaseModel + + class FlightSearchInput(BaseModel): + origin: str # Airport code "SFO" + destination: str # Airport code "CDG" + departure_date: date # date(2026, 3, 15) + passengers: int = 1 # Number of passengers + + class FlightSearchOutput(BaseModel): + flights: list[Flight] + cheapest_price: float + + flight_searcher = Agent( + name="flight_searcher", + instruction="Search for available flights.", + input_schema=FlightSearchInput, + output_schema=FlightSearchOutput, + tools=[search_flights_api], + mode="single_turn", + ... + ) + + assistant = Agent( + name="assistant", + instruction="You help users plan trips.", + sub_agents=[flight_searcher], + ... ) -``` - -!!! warning "Caution: Event.output limitation" - - Nodes are only allowed to emit a single ***Event.output*** data payload - per execution. This limitation means that while you can more than one - ***yield*** in a node, having two or more ***yield*** commands with an - ***Event.output*** results in a runtime error. - -### Event `message` parameter - -The ***message*** parameter of an ***Event*** is used to pass data intended as -a user response. In general, you should not use the ***message*** parameter in -your agent code unless it is specifically to provide information to a user or -request information from a user. The following code example show how to provide -information to a user during workflow execution: - -```python -async def user_message(node_input: str): - """Tell user research process is starting.""" - yield Event(message="Beginning research process...") -``` - -### Event `state` parameter - -The ***state*** parameter of an ***Event*** is used to maintain a small set of -data values during an entire ADK session. Values in the state parameter -automatically persist between Nodes and are meant for guiding the execution of -more complex workflows. Nodes can modify state values, and the modified state -values are available to downstream Nodes.The following code example shows how -state is persisted across nodes: - -```python -async def init_state_node(attempts: int = 0): - yield Event( - state={ - "attempts": attempts, - }, - ) - -async def task_attempt_node(node_input: Content, attempts: int): - yield Event( - state={ - "attempts": attempts + 1, - }, - ) - -async def read_state_node(ctx: Context): - print(f"attempts state: {ctx.state}") # attempts state: attempts: 1 - -root_agent = Workflow( - name="root_agent", - edges=[("START", init_state_node, task_attempt_node, read_state_node)], -) -``` - -!!! warning "Caution: `state` property data limitations" - - The state parameter *should not be used to persist large amounts of data* between - nodes. Use artifacts or other data persistence mechanisms, such as database - Tools, to persist large data resources during the life cycle of a Workflow. - -## Constrain node data input and output with schemas - -You can set input and output data schemas to constrain the input and output data -formats of any node, including ***FunctionNodes*** and **Agents**. The following -parameters are optional settings for any node. You can set both or either one of -these parameters on any workflow node as required by your agent project. - -- **`input_schema`**: Set the expected input schema using a class that - extends ***BaseModel***. -- **`output_schema`**: Set the required output schema using a class that - extends ***BaseModel***. - -The code example below shows how to set both input and output schemas for a -subagent. - -```python -from google.adk import Agent -from pydantic import BaseModel - -class FlightSearchInput(BaseModel): - origin: str # Airport code "SFO" - destination: str # Airport code "CDG" - departure_date: date # date(2026, 3, 15) - passengers: int = 1 # Number of passengers - -class FlightSearchOutput(BaseModel): - flights: list[Flight] - cheapest_price: float - -flight_searcher = Agent( - name="flight_searcher", - instruction="Search for available flights.", - input_schema=FlightSearchInput, - output_schema=FlightSearchOutput, - tools=[search_flights_api], - mode="single_turn", - ... -) - -assistant = Agent( - name="assistant", - instruction="You help users plan trips.", - sub_agents=[flight_searcher], - ... -) -``` + ``` + +=== "Go" + + **workflow package**: use `workflow.NewAgentNodeTyped[Input, Output]` to + attach schemas to an agent node. The generic type parameters are reflected + into `*jsonschema.Schema` automatically — no hand-built schema construction + needed. The node's `Event.Output` carries the structured result to the + successor — no `OutputKey` or state write is needed: + + ```go + --8<-- "examples/go/snippets/graphs/data-handling/main.go:input-output-schema" + ``` + + **Prebuilt workflow agents**: set `InputSchema` and `OutputSchema` on + `llmagent.Config`. `OutputSchema` forces the model to reply with a JSON + object matching the schema (the agent cannot use tools when `OutputSchema` + is set). Use `OutputKey` to save the JSON string to state for downstream + agents to reference via `{key}` in their `Instruction`. ## Access structured data in agents -When you pass structured data into an agent from subagent or a workflow node, -such as a Function Node, you can use specific syntax to add that data into the -agent's instructions. Specifically, you can use the curly braces `{ }` to select -the input schema properties, or `< >` to specify the input schema properties, -the `from` keyword, and the name of the node providing the data. The following -code snippet shows two ways to include data passed through an agent -***input schema***: - -```python -class CityTime(BaseModel): - time_info: str # time information - city: str # city name - -def lookup_time_function(city: str): - """Simulate returning the current time in the specified city.""" - return Event(output=CityTime(time_info='10:10 AM', city=city)) - -city_report_agent = Agent( - name="city_report_agent", - model="gemini-flash-latest", - input_schema=CityTime, - - # data selection based on class and parameter - # instruction=""" - # Return a sentence in the following format: - # It is {CityTime.time_info} in {CityTime.city} right now. - # """, - - # more restrictive data selection based on source node name - instruction=""" - Return a sentence in the following format: - It is in - right now. - """, -) - -root_agent = Workflow( - name="root_agent", - edges=[ - (START, city_generator_agent, lookup_time_function, city_report_agent) - ], -) -``` - -For a complete, but simplified version of this workflow, see +=== "Python" + + Use the curly-brace `{ }` syntax to select properties from the input + schema, or `< >` to select a property and also qualify it by the name + of the source node: + + ```python + class CityTime(BaseModel): + time_info: str # time information + city: str # city name + + def lookup_time_function(city: str): + """Simulate returning the current time in the specified city.""" + return Event(output=CityTime(time_info='10:10 AM', city=city)) + + city_report_agent = Agent( + name="city_report_agent", + model="gemini-flash-latest", + input_schema=CityTime, + + # data selection based on class and parameter + # instruction=""" + # Return a sentence in the following format: + # It is {CityTime.time_info} in {CityTime.city} right now. + # """, + + # more restrictive data selection based on source node name + instruction=""" + Return a sentence in the following format: + It is in + right now. + """, + ) + + root_agent = Workflow( + name="root_agent", + edges=[ + (START, city_generator_agent, lookup_time_function, city_report_agent) + ], + ) + ``` + +=== "Go" + + In ADK Go v2.0.0, a `FunctionNode` returns a typed struct and the + framework serializes it into `Event.Output`. The successor `AgentNode` + receives the struct as its user content — the fields are available to the + agent's `Instruction` without any `{key}` template syntax. This is the + direct equivalent of Python's `input_schema=CityTime` with + `{CityTime.time_info}` template placeholders: the struct fields are + delivered as typed input rather than looked up by name from state. + + ```go + --8<-- "examples/go/snippets/graphs/data-handling/main.go:structured-output" + ``` + +For a complete example of this workflow, see [Graph-based agent workflows](/graphs/#get-started). diff --git a/docs/graphs/dynamic.md b/docs/graphs/dynamic.md index bb342f8c84..d176affd09 100644 --- a/docs/graphs/dynamic.md +++ b/docs/graphs/dynamic.md @@ -1,7 +1,7 @@ # Dynamic agent workflows
- Supported in ADKPython v2.0.0 + Supported in ADKPython v2.0.0Go v2.0.0
The ADK framework provides a programmatic way to define workflows as a more @@ -14,20 +14,21 @@ manage. Dynamic workflows in ADK allow you to put aside graph-based path structures and use the full power of your chosen programming language to build workflows. With -Dynamic workflows, you can create workflows with simple decorators, invoke -workflow nodes as functions, and build complex routing logic. Here are some of -the benefits of dynamic workflows in ADK: +dynamic workflows, you can create workflows with simple decorators (Python) or +constructor functions (Go), invoke workflow nodes as functions, and build +complex routing logic. Here are some of the benefits of dynamic workflows in ADK: - **Flexible Control Flow:** Define execution order dynamically using loops, conditionals, and recursion which are difficult or impossible to represent in static graphs. - **Programmatic Experience:** Use familiar constructs like `while` loops - and `async/await` instead of graph-based routing. + and `async/await` (Python) or `for` loops and `workflow.RunNode` (Go) + instead of graph-based routing. - **Automatic Checkpointing:** Dynamic workflows track each node execution. Successful sub-nodes are automatically skipped when resuming the workflow, making complex logic durable and resumable by default. - **Encapsulation:** Wrap business logic into *parent* nodes that - internally compose lower-level nodes, keeping the overall workflow graph + internally compose lower-level nodes, keeping the overall workflow clean and manageable. ## Get started @@ -35,155 +36,244 @@ the benefits of dynamic workflows in ADK: The following dynamic workflow code example shows how to define a basic workflow containing a single node with a function: -```python -from google.adk import Context -from google.adk import Workflow -from google.adk.workflow import node -from typing import Any - -@node(name="hello_node") -def my_node(node_input: Any): - return "Hello World" - -# define a dynamic workflow node -@node(rerun_on_resume=True) -async def my_workflow(ctx: Context, node_input: str) -> str: - # run_node executes a node and returns its output - result = await ctx.run_node(my_node, node_input="hello") - return result - -# Run the workflow -root_agent = Workflow( - name="root_agent", - edges=[("START", my_workflow)], -) -``` - -This example uses the [***@node***](#node) annotation for convenience and to -keep the written code as simple as possible. This annotation generates wrappers -that allow the code to be run in the context of an ADK dynamic workflow. +=== "Python" + + ```python + from google.adk import Context + from google.adk import Workflow + from google.adk.workflow import node + from typing import Any + + @node(name="hello_node") + def my_node(node_input: Any): + return "Hello World" + + # define a dynamic workflow node + @node(rerun_on_resume=True) + async def my_workflow(ctx: Context, node_input: str) -> str: + # run_node executes a node and returns its output + result = await ctx.run_node(my_node, node_input="hello") + return result + + # Run the workflow + root_agent = Workflow( + name="root_agent", + edges=[("START", my_workflow)], + ) + ``` + + This example uses the [***@node***](#node) annotation for convenience and to + keep the written code as simple as possible. This annotation generates wrappers + that allow the code to be run in the context of an ADK dynamic workflow. + +=== "Go" + + In Go, `workflow.NewFunctionNode` replaces the `@node` decorator and + `workflow.NewDynamicNode` replaces the `@node(rerun_on_resume=True)` async + orchestrator. `workflow.RunNode` is the direct equivalent of + `ctx.run_node()`. `workflowagent.New` with `workflow.Chain` replaces + `Workflow(edges=[...])`. + + Resume behaviour after a human-in-the-loop pause is controlled by + `NodeConfig.RerunOnResume` — see [Nodes](#node) below for details. + + ```go + --8<-- "examples/go/snippets/graphs/dynamic/main.go:get-started" + ``` ## Building blocks: nodes and workflows Nodes and workflows represent the basic building blocks of ADK's dynamic -workflows. These classes provide the functionality required to wrap your code so -it can be integrated into code-based workflows in ADK. - -### Nodes and @node {#node} - -A dynamic workflow in ADK is composed of *nodes*, which are classes derived -from ***BaseNode***. A simple version of a usable workflow node is a -***FunctionNode***, which allows you to wrap code with functionality required to -run within a ***Workflow***. For convenience, the ADK framework provides the -***@node*** annotation which generates the node wrapper, keeping boilerplate -wrapper code to a minimum: - -```python -@node(name="hello_node") -def my_function_node(node_input: Any): - return "Hello World" -``` - -The following code snippet shows the equivalent code *without* the -***@node*** annotation: - -```python -# base function -def my_function_node(node_input: Any): - return "Hello World" - -# FunctionNode wrapper with options -success_node = FunctionNode( - my_function_node, - name="hello", - rerun_on_resume=True, -) -``` - -Creating the node wrapper code yourself can be useful if you are wrapping -functions from an external library, need to create multiple nodes from the same -function with different configurations, or if you are managing node references -in a registry for advanced orchestration. +workflows. These types and functions provide the functionality required to +wrap your code so it can be integrated into code-based workflows in ADK. + +### Nodes {#node} + +A dynamic workflow in ADK is composed of *nodes*. A simple version of a +usable workflow node wraps a plain function with the metadata required to +run within a workflow. + +=== "Python" + + In Python, the ***@node*** annotation generates the node wrapper, keeping + boilerplate to a minimum: + + ```python + @node(name="hello_node") + def my_function_node(node_input: Any): + return "Hello World" + ``` + + The following code snippet shows the equivalent code *without* the + ***@node*** annotation: + + ```python + # base function + def my_function_node(node_input: Any): + return "Hello World" + + # FunctionNode wrapper with options + success_node = FunctionNode( + my_function_node, + name="hello", + rerun_on_resume=True, + ) + ``` + + Creating the node wrapper code yourself can be useful if you are wrapping + functions from an external library, need to create multiple nodes from the + same function with different configurations, or if you are managing node + references in a registry for advanced orchestration. + +=== "Go" + + In Go, `workflow.NewFunctionNode[IN, OUT]` wraps a plain function as a + workflow node, inferring input and output types from the generic parameters. + There is no decorator syntax; the node is a value that you pass as a child + to `workflow.RunNode` inside a dynamic orchestrator: + + ```go + --8<-- "examples/go/snippets/graphs/dynamic/main.go:building-blocks-nodes" + ``` + + `NodeConfig` holds the same options as Python's `@node` arguments. + The most important field is `RerunOnResume *bool`, which controls what + happens when a workflow resumes after a human-in-the-loop pause: + + - **`&true` (re-entry mode)**: the interrupted node is re-run from the + beginning on resume. Use this for dynamic orchestrator nodes that call + `workflow.RunNode` in a loop — the body re-executes and already-completed + child activations are skipped automatically (checkpointing). This mirrors + Python's `@node(rerun_on_resume=True)`. + - **`&false` (handoff mode)**: the resume payload is routed directly to + the node's successor as input, bypassing the interrupted node entirely. + Use this for leaf nodes that simply emit a pause event and expect the + human response to flow to the next step. + - **`nil`**: the default depends on node type. `workflow.NewDynamicNode` + automatically sets `nil → &true` (re-entry mode), because an + orchestrator body must be re-entered on resume to deliver cached child + results. `workflow.NewFunctionNode` and other leaf node constructors + leave `nil` as-is, which the engine treats as handoff (`&false`). + Explicit `&false` is always respected on any node type. + + ```go + // NewDynamicNode: nil RerunOnResume is automatically set to &true. + // Passing &rerun explicitly is equivalent and makes the intent clear. + rerun := true + orchestratorNode := workflow.NewDynamicNode[string, string]("my_workflow", + myOrchestratorfn, + workflow.NodeConfig{RerunOnResume: &rerun}, // re-entry: node body re-runs on resume + ) + + // NewFunctionNode: nil RerunOnResume stays nil → engine treats as handoff. + handoffNode := workflow.NewFunctionNode("leaf_node", + myLeafFn, + workflow.NodeConfig{}, // nil RerunOnResume → handoff for FunctionNode + ) + ``` + ### Workflows -In an ADK dynamic workflow, you use the ***Workflow*** class as a primary -container for orchestrating nodes. You use a node to define a dynamic workflow -with code that manages running nodes and the execution logic (order and paths) -for those nodes, as shown in the following code sample: - -```python -@node(rerun_on_resume=True) -async def my_workflow(ctx): - # run_node executes a node and returns its output - result = await ctx.run_node(my_function_node, node_input="Hello") - result_formatted = await ctx.run_node(my_formatting_node, node_input=result) - return result_formatted - -# Run the workflow -root_agent = Workflow( - name="root_agent", - edges=[("START", my_workflow)], -) -``` +In an ADK dynamic workflow, you use a dynamic node as the primary +orchestrator for nodes. A dynamic node manages running child nodes and the +execution logic (order and paths) for those nodes. + +=== "Python" + + ```python + @node(rerun_on_resume=True) + async def my_workflow(ctx): + # run_node executes a node and returns its output + result = await ctx.run_node(my_function_node, node_input="Hello") + result_formatted = await ctx.run_node(my_formatting_node, node_input=result) + return result_formatted + + # Run the workflow + root_agent = Workflow( + name="root_agent", + edges=[("START", my_workflow)], + ) + ``` + +=== "Go" + + `workflow.NewDynamicNode` creates an orchestrator whose body calls + `workflow.RunNode` for each child step. `workflowagent.New` with + `workflow.Chain(workflow.Start, myWorkflow)` is the equivalent of + `Workflow(edges=[("START", my_workflow)])`: + + ```go + --8<-- "examples/go/snippets/graphs/dynamic/main.go:building-blocks-workflow" + ``` ## Data handling When using dynamic workflows with ADK, passing data is simpler than -[graph-based workflows](/graphs/) because, with a workflow, -the ***Context*** class's ***run_node()*** method returns the node's output -directly. This eliminates the need to directly handle session state or complex -routing outputs for data transfer. The following code example shows how you can -pass string data between an agent node and a function node: - -```python -from google.adk import Context -from google.adk.workflow import node - -@node(rerun_on_resume=True) -async def editorial_workflow(ctx: Context, user_request: str): - # Agent Node generates output - raw_draft = await ctx.run_node(draft_agent, user_request) - - # Function Node formats text - formatted_text = await ctx.run_node(format_function_node, raw_draft) - - return formatted_text -``` - -You can also pass specific data schemas using defined class and configure input -and output schemas, similar to graph-based workflow nodes, as shown in the -following code example: - -```python -from google.adk import Agent -from google.adk import Context -from google.adk.workflow import node -from pydantic import BaseModel - -class CityTime(BaseModel): - time_info: str # time information - city: str # city name - -@node -def city_time_function(city: str): - """Simulate returning the current time in a specified city.""" - return CityTime(time_info="10:10 AM", city=city) - -city_report_agent = Agent( - name="city_report_agent", - model="gemini-flash-latest", - input_schema=CityTime, - instruction="""output the data provided by the previous node.""", -) - -@node # workflow node -async def city_workflow(ctx: Context): - city_time = await ctx.run_node(city_time_function, "Paris") - report_text = await ctx.run_node(city_report_agent, city_time) - - return report_text -``` +[graph-based workflows](/graphs/) because `workflow.RunNode` returns the +child node's output directly as a typed Go value — eliminating the need to +manually read and write session state keys for data transfer. + +=== "Python" + + ```python + from google.adk import Context + from google.adk.workflow import node + + @node(rerun_on_resume=True) + async def editorial_workflow(ctx: Context, user_request: str): + # Agent Node generates output + raw_draft = await ctx.run_node(draft_agent, user_request) + + # Function Node formats text + formatted_text = await ctx.run_node(format_function_node, raw_draft) + + return formatted_text + ``` + + You can also pass specific data schemas using a defined class and configure + input and output schemas, similar to graph-based workflow nodes: + + ```python + from google.adk import Agent + from google.adk import Context + from google.adk.workflow import node + from pydantic import BaseModel + + class CityTime(BaseModel): + time_info: str # time information + city: str # city name + + @node + def city_time_function(city: str): + """Simulate returning the current time in a specified city.""" + return CityTime(time_info="10:10 AM", city=city) + + city_report_agent = Agent( + name="city_report_agent", + model="gemini-flash-latest", + input_schema=CityTime, + instruction="""output the data provided by the previous node.""", + ) + + @node # workflow node + async def city_workflow(ctx: Context): + city_time = await ctx.run_node(city_time_function, "Paris") + report_text = await ctx.run_node(city_report_agent, city_time) + + return report_text + ``` + +=== "Go" + + In Go, `workflow.NewAgentNode` wraps an `agent.Agent` so it can be + invoked via `workflow.RunNode` inside a dynamic orchestrator. The output + of each `RunNode` call is returned as a typed value — no session state + reads are required: + + ```go + --8<-- "examples/go/snippets/graphs/dynamic/main.go:data-handling" + ``` For more information on data handling between workflow nodes, see [Data handling for agent workflows](/graphs/data-handling/). @@ -198,138 +288,199 @@ the techniques that you can use for routing. ### Sequence route You can create sequential task processing with dynamic workflows in ADK, just -as you can with graph-based workflows. The following code snippet shows a -dynamic workflow with an agent, a function node, and a second agent: +as you can with graph-based workflows. + +=== "Python" + + The following code snippet shows a dynamic workflow with an agent, a + function node, and a second agent: + + ```python + @node # workflow node + async def city_workflow(ctx: Context): + city = await ctx.run_node(city_generator_agent) + city_time = await ctx.run_node(city_time_function, city) + report_text = await ctx.run_node(city_report_agent, city_time) -```python -@node # workflow node -async def city_workflow(ctx: Context): - city = await ctx.run_node(city_generator_agent) - city_time = await ctx.run_node(city_time_function, city) - report_text = await ctx.run_node(city_report_agent, city_time) + return report_text + ``` - return report_text -``` +=== "Go" + + Call `workflow.RunNode` sequentially inside a `NewDynamicNode` body — + each call awaits the child before the next one starts. The + [data handling example above](#data-handling) demonstrates exactly this + pattern: `cityWorkflow` calls `workflow.RunNode` for `cityTimeNode` and + then `cityReportNode` in order, passing each node's typed output to the + next. ### Loop route For workflows where you want to use an iterative loop for a task, dynamic -workflows offer much more flexibility to define the routing logic you need. The -following code example shows how to use dynamic workflows to construct a -workflow loop for generating, reviewing, and updating code: - -```python -from google.adk import Context -from google.adk import Event -from google.adk.agents import LlmAgent -from google.adk.workflow import node - -coder_agent = LlmAgent( - name="generator_agent", - model="gemini-flash-latest", - instruction="Write python code for user request.", - output_schema=str, -) - -@node(name="lint_reviewer") -async def compile_lint_check(ctx: Context, code: str): - # Simulate API call or lint check - class Response: - findings = "" - return Response() - -fixer_agent = LlmAgent( - name="fixer_agent", - model="gemini-flash-latest", - instruction="""Refactor current code {code}. - Based on compile & lint review: {findings}""", - output_schema=str, -) - -@node # workflow node -async def code_workflow(ctx: Context, user_request: str): - code = await ctx.run_node(coder_agent, user_request) - check_resp = await ctx.run_node(compile_lint_check, code) - - while check_resp.findings: - yield Event(state={"code": code, "findings": check_resp.findings}) - code = await ctx.run_node(fixer_agent, {"code": code, "findings": check_resp.findings}) - - check_resp = await ctx.run_node(compile_lint_check, code) - - return code -``` +workflows offer much more flexibility to define the routing logic you need. + +=== "Python" + + The following code example shows how to use dynamic workflows to construct + a workflow loop for generating, reviewing, and updating code: + + ```python + from google.adk import Context + from google.adk import Event + from google.adk.agents import LlmAgent + from google.adk.workflow import node + + coder_agent = LlmAgent( + name="generator_agent", + model="gemini-flash-latest", + instruction="Write python code for user request.", + output_schema=str, + ) + + @node(name="lint_reviewer") + async def compile_lint_check(ctx: Context, code: str): + # Simulate API call or lint check + class Response: + findings = "" + return Response() + + fixer_agent = LlmAgent( + name="fixer_agent", + model="gemini-flash-latest", + instruction="""Refactor current code {code}. + Based on compile & lint review: {findings}""", + output_schema=str, + ) + + @node # workflow node + async def code_workflow(ctx: Context, user_request: str): + code = await ctx.run_node(coder_agent, user_request) + check_resp = await ctx.run_node(compile_lint_check, code) + + while check_resp.findings: + yield Event(state={"code": code, "findings": check_resp.findings}) + code = await ctx.run_node(fixer_agent, {"code": code, "findings": check_resp.findings}) + + check_resp = await ctx.run_node(compile_lint_check, code) + + return code + ``` + +=== "Go" + + In Go, the loop is a plain `for` loop inside the dynamic node body. The + lint check node returns an empty string when there are no findings, + which signals the loop to exit: + + ```go + --8<-- "examples/go/snippets/graphs/dynamic/main.go:loop-route" + ``` ### Parallel execution routes -Dynamic workflows in ADK can support parallel execution, and you can use -standard asynchronous libraries, such as `asyncio`, to build this -functionality. The following code example shows how to build a workflow node -that supports parallel execution using `@node` and `asyncio.gather`: +Dynamic workflows in ADK can support parallel execution. + +=== "Python" + + In Python, you can use `asyncio.gather` to build parallel execution: + + ```python + import asyncio + from typing import Any + from google.adk import Context + from google.adk.workflow import BaseNode, node + + + @node(rerun_on_resume=True) + async def parallel_supervisor( + ctx: Context, node_input: list[Any], real_node: BaseNode + ): + """Runs a worker node in parallel for each item in the input list.""" + tasks = [] + for item in node_input: + # ctx.run_node returns a future. Append instead of awaiting immediately. + tasks.append(ctx.run_node(real_node, item)) -```python -import asyncio -from typing import Any -from google.adk import Context -from google.adk.workflow import BaseNode, node + # Collect all results in parallel + results = await asyncio.gather(*tasks) + return results + ``` + !!! tip "Tip: Resuming parallel nodes" -@node(rerun_on_resume=True) -async def parallel_supervisor( - ctx: Context, node_input: list[Any], real_node: BaseNode -): - """Runs a worker node in parallel for each item in the input list.""" - tasks = [] - for item in node_input: - # ctx.run_node returns a future. Append instead of awaiting immediately. - tasks.append(ctx.run_node(real_node, item)) + The workflow framework ensures that if a dynamic workflow is resumed, + only failed or interrupted worker nodes are re-executed, including + parallel worker nodes. - # Collect all results in parallel - results = await asyncio.gather(*tasks) - return results -``` +=== "Go" -!!! tip "Tip: Resuming parallel nodes" + In Go, `workflow.NewParallelWorker` wraps a child node and runs it + concurrently for each element of a list input, collecting results into a + single output slice. The `maxConcurrency` parameter caps how many + concurrent activations may run simultaneously; `0` means unlimited: - The workflow framework ensures that if a dynamic workflow is resumed, only - failed or interrupted worker nodes are re-executed, including parallel worker - nodes. + ```go + --8<-- "examples/go/snippets/graphs/dynamic/main.go:parallel-route" + ``` + + !!! tip "Tip: Resuming parallel nodes" + + The workflow framework ensures that if a dynamic workflow is resumed, + only failed or interrupted worker nodes are re-executed, including + parallel worker nodes managed by `NewParallelWorker`. ## Human input Dynamic workflows in ADK can also include human input or human in the loop -(HITL) steps. You build human input into workflows by yielding a -***RequestInput*** from a node, which pauses the workflow and waits for user -input. The following code example shows how to build a human input node and -include it in a workflow: +(HITL) steps. + +=== "Python" + + You build human input into workflows by yielding a ***RequestInput*** from + a node, which pauses the workflow and waits for user input. The following + code example shows how to build a human input node and include it in a + workflow: + + ```python + from typing import Any + from google.adk import Context + from google.adk.events import RequestInput + from google.adk.workflow import node + + + @node(rerun_on_resume=False) + async def get_user_approval(ctx: Context, node_input: Any): + """Yields a RequestInput to pause the workflow and wait for user input.""" + yield RequestInput(message="Please approve this request (Yes/No)") -```python -from typing import Any -from google.adk import Context -from google.adk.events import RequestInput -from google.adk.workflow import node + @node(rerun_on_resume=True) + async def handle_process(ctx: Context, node_input: Any): + """The orchestrator calling the interactive step.""" + user_response = await ctx.run_node(get_user_approval) -@node(rerun_on_resume=False) -async def get_user_approval(ctx: Context, node_input: Any): - """Yields a RequestInput to pause the workflow and wait for user input.""" - yield RequestInput(message="Please approve this request (Yes/No)") + if user_response.lower() == "yes": + return "Approved" + return "Denied" + ``` + !!! important "Important: Parent nodes with `ctx.run_node`" -@node(rerun_on_resume=True) -async def handle_process(ctx: Context, node_input: Any): - """The orchestrator calling the interactive step.""" - user_response = await ctx.run_node(get_user_approval) + Parent nodes in dynamic workflows that call `ctx.run_node` must set + `rerun_on_resume=True` to handle interruptions properly. - if user_response.lower() == "yes": - return "Approved" - return "Denied" -``` +=== "Go" -!!! important "Important: Parent nodes with `ctx.run_node`" + In Go, use `workflow.NewEmittingFunctionNode` with + `workflow.ResumeOrRequestInput` to implement the re-entry HITL pattern. + On the first pass `ResumeOrRequestInput` emits a `session.RequestInput` + event and returns `ErrNodeInterrupted`, pausing the workflow. After the + human replies, the node is re-run from the top (`RerunOnResume: &true`) + and `ResumeOrRequestInput` returns the human's reply directly: - Parent nodes in dynamic workflows that call `ctx.run_node` must set - `rerun_on_resume=True` to handle interruptions properly. + ```go + --8<-- "examples/go/snippets/graphs/dynamic/main.go:human-input" + ``` ## Advanced features @@ -349,48 +500,58 @@ or re-run workflow. #### Custom execution IDs In some rare cases, you may need to have stable identifiers, such as when -processing a reorderable list, you can supply a custom ID when running a node. -In general, you should avoid this due to the impacts to workflow task retries -and process resumes. Specifically, these IDs are used to check node states and -skip execution if a node was already run. If you provide custom IDs, make sure -they are deterministic for workflow re-runs and logically remain the same for -the input. The following example code shows how to add such an identifier when -executing node in a workflow: +processing a reorderable list. In general, you should avoid this due to the +impacts to workflow task retries and process resumes. Specifically, these IDs +are used to check node states and skip execution if a node was already run. If +you provide custom IDs, make sure they are deterministic for workflow re-runs +and logically remain the same for the input. !!! warning "Warning: Custom execution IDs" - Avoid creating custom execution IDs. Since execution IDs are used to determine - the execution order of nodes, custom execution IDs can cause problems when the - system attempts to re-run those nodes in your workflow. - -```python -from google.adk import Context -from google.adk.workflow import node -from pydantic import BaseModel -from typing import Any -import asyncio - -class Order(BaseModel): - order_id: str - cart_items: list[Product] - -@node(rerun_on_resume=True) -async def process_all_orders(ctx: Context, node_input: Any): - orders = await get_orders() - - process_tasks = [] - for order in orders: - # Use run_id to provide a custom identifier. - # Custom run_ids must contain at least one non-numeric character - # to avoid collision with auto-generated sequential numeric IDs. - task = ctx.run_node(process_order, order, run_id=f"order-{order.order_id}") - process_tasks.append(task) - - results = await asyncio.gather(*process_tasks) - return results -``` - -By default, auto-generated run IDs are sequential integers starting from -`"1"` (represented as strings). Custom `run_id` values must contain at -least one non-numeric character to avoid collisions with these -auto-generated IDs. + Avoid creating custom execution IDs. Since execution IDs are used to + determine the execution order of nodes, custom execution IDs can cause + problems when the system attempts to re-run those nodes in your workflow. + +=== "Python" + + ```python + from google.adk import Context + from google.adk.workflow import node + from pydantic import BaseModel + from typing import Any + import asyncio + + class Order(BaseModel): + order_id: str + cart_items: list[Product] + + @node(rerun_on_resume=True) + async def process_all_orders(ctx: Context, node_input: Any): + orders = await get_orders() + + process_tasks = [] + for order in orders: + # Use run_id to provide a custom identifier. + # Custom run_ids must contain at least one non-numeric character + # to avoid collision with auto-generated sequential numeric IDs. + task = ctx.run_node(process_order, order, run_id=f"order-{order.order_id}") + process_tasks.append(task) + + results = await asyncio.gather(*process_tasks) + return results + ``` + + By default, auto-generated run IDs are sequential integers starting from + `"1"` (represented as strings). Custom `run_id` values must contain at + least one non-numeric character to avoid collisions with these + auto-generated IDs. + +=== "Go" + + In Go, pass `workflow.WithRunID("order-x")` as a trailing option to + `workflow.RunNode`. The ID must contain at least one non-numeric character + to avoid collision with the auto-generated sequential counter IDs: + + ```go + --8<-- "examples/go/snippets/graphs/dynamic/main.go:custom-execution-ids" + ``` diff --git a/docs/graphs/human-input.md b/docs/graphs/human-input.md index dd9e4c2236..ca3d0a42d3 100644 --- a/docs/graphs/human-input.md +++ b/docs/graphs/human-input.md @@ -1,7 +1,7 @@ # Human input for agent workflows
- Supported in ADKPython v2.0.0 + Supported in ADKPython v2.0.0Go v2.0.0
Being able to request human input for data input, decision verification, or @@ -13,110 +13,195 @@ the input process more predictable and reliable. ## Get started -You can implement a human input node in a graph using the ***RequestInput*** -class and a text prompt for the user. The following code example shows how to -add a human input node to an Workflow graph: +=== "Python" -```python -from google.adk.events import RequestInput -from google.adk import Workflow + You can implement a human input node in a graph using the ***RequestInput*** + class and a text prompt for the user. The following code example shows how to + add a human input node to a Workflow graph: -def step1(): # Human input step - yield RequestInput(message="Enter a number:") + ```python + from google.adk.events import RequestInput + from google.adk import Workflow -def step2(node_input): - return node_input * 2 + def step1(): # Human input step + yield RequestInput(message="Enter a number:") -root_agent = Workflow( - name="root_agent", - edges=[('START', step1, step2)], -) -``` + def step2(node_input): + return node_input * 2 -In this code example, `step1` pauses the execution of the agent until the -system receives an input from a user. Once the system receives input from the -user, that input is passed to the next node. + root_agent = Workflow( + name="root_agent", + edges=[('START', step1, step2)], + ) + ``` + + In this code example, `step1` pauses the execution of the agent until the + system receives an input from a user. Once the system receives input from the + user, that input is passed to the next node. + +=== "Go" + + In ADK Go v2.0.0, a HITL graph node is built with + `workflow.NewEmittingFunctionNode` and `workflow.ResumeOrRequestInput`. + This is the direct equivalent of Python's `RequestInput` node: + + - On the **first pass**, `workflow.ResumeOrRequestInput` emits a + `session.RequestInput` event (surfaced as `Event.RequestedInput`) and + returns `ErrNodeInterrupted`, pausing the workflow. + - After the human replies, the node is **re-invoked from the top** + (`RerunOnResume: &true`) and `ResumeOrRequestInput` returns the reply + payload, which flows as typed input to the next node via `event.Output`. + + ```go + --8<-- "examples/go/snippets/graphs/human-input/main.go:graph-hitl-get-started" + ``` ## Configuration options -Human input nodes can use the ***RequestInput*** class with the following -configuration options: +=== "Python" + + Human input nodes can use the ***RequestInput*** class with the following + configuration options: + + - **`message`:** Text provided to the user to explain the human input + request. + - **`payload`:** Structured data to be used as part of the human input + request. + - **`response_schema`:** A data structure the human response must conform to. + + !!! note "Note: Response schema input limitations" + + For the **response_schema** setting, the ***RequestInput*** class does not + automatically reformat human responses to fit a specified data structure. The + human response must be provided in the specified format. For a better user + experience, consider providing a user interface to collect structured data + or use an Agent node to conform unstructured data to the format required. + +=== "Go" + + `session.RequestInput` carries the following fields, which map directly to + Python's `RequestInput` parameters: + + - **`InterruptID`** (`string`): A unique identifier for this pause point. + Use a stable prefix plus a UUID to avoid collision across workflow runs. + Equivalent to the implicit interrupt ID in Python. + - **`Message`** (`string`): Human-readable prompt displayed to the user. + Equivalent to Python's `message` parameter. + - **`Payload`** (`any`): Optional structured data sent alongside the + prompt so the client can render additional context. Equivalent to + Python's `payload` parameter. + + `workflow.NodeConfig.RerunOnResume` controls what happens on resume: -- **`message`:** Text provided to the user to explain the human input - request. -- **`payload`:** Structured data to be used as part of the human input - request. -- **`response_schema`:** A data structure the human response must conform to. + - **`&true`**: the node body is re-run from the top; `ResumeOrRequestInput` + returns the human's reply on the second pass. Required for nodes that + use `ResumeOrRequestInput`. + - **`&false`** or **`nil`** (leaf default): the reply is routed to the + node's successor as input, bypassing the interrupted node. -!!! note "Note: Response schema input limitations" + !!! note "Note: Structured response from the client" - For the **response_schema** setting, the ***RequestInput*** class does not - automatically reformat human responses to fit a specified data structure. The - human response must be provided in the specified format. For a better user - experience, consider providing a user interface to collect structured data - or use an Agent node to conform unstructured data to the format required. + ADK Go does not automatically parse or validate the structure of the + human's reply payload. If your workflow needs structured feedback, + include a UI or a downstream agent node to validate the response before + acting on it. ## Human input examples -The following code examples demonstrate more detailed human input requests, -including the use of ***message***, ***payload*** and ***response schema*** -parameters. - -### Request input with response schema - -The following code sample shows how to construct a ***RequestInput*** object in -a workflow node, including a ***response schema***: - -```python -async def initial_prompt(ctx: Context): - """Ask the user for itinerary information""" - input_message = """ - This is an interactive concierge workflow tasked with making you a great - itinerary for you in your city of choice. If you give some details about - yourself or what you are generally looking for I can better personalize - your itinerary. - For example, input your: - City (Required), - Age, - Hobby, - Example of attraction you liked - """ - yield RequestInput(message=input_message, response_schema=str) -``` - -### Request input with data payload - -The following code sample shows how to construct a ***RequestInput*** object in -a workflow node, including a ***payload*** and ***response schema***. In this -example, the `ActivitiesList` is expected to be completed by an agent node that -composes a list of activities, and the `get_user_feedback()` node requests -feedback for the user. - -```python -class ActivitiesList(BaseModel): - """Itinerary should be a list of dictionaries for each activity. Each - activity has a name and a description""" - itinerary: List[Dict[str, str]] - -class UserFeedback(BaseModel): - """Expected response structure from the user.""" - user_response: str - -async def get_user_feedback(node_input: ActivitiesList): - """ - Retrieves the user's thoughts on the agents initial itinerary in order to - either expand on, change the list, or exit the loop - """ - message = ( - f""" - Here is your recommended base itinerary:\n{node_input}\n\n - Which of these items appeal to you (if any)? +The following code examples demonstrate more detailed human input requests. + +### Request input with a message and payload + +=== "Python" + + The following code sample shows how to construct a ***RequestInput*** object + in a workflow node, including a ***payload*** and ***response schema***. In + this example, the `ActivitiesList` is expected to be completed by an agent + node that composes a list of activities, and the `get_user_feedback()` node + requests feedback from the user. + + ```python + class ActivitiesList(BaseModel): + """Itinerary should be a list of dictionaries for each activity. Each + activity has a name and a description""" + itinerary: List[Dict[str, str]] + + class UserFeedback(BaseModel): + """Expected response structure from the user.""" + user_response: str + + async def get_user_feedback(node_input: ActivitiesList): + """ + Retrieves the user's thoughts on the agents initial itinerary in order to + either expand on, change the list, or exit the loop """ - ) - - yield RequestInput( - message=message, - payload=node_input, - response_schema=UserFeedback, - ) -``` + message = ( + f""" + Here is your recommended base itinerary:\n{node_input}\n\n + Which of these items appeal to you (if any)? + """ + ) + + yield RequestInput( + message=message, + payload=node_input, + response_schema=UserFeedback, + ) + ``` + +=== "Go" + + The following code sample shows a three-node graph: a builder node generates + a structured itinerary, a HITL node sends it as `Payload` alongside the + prompt, and a final node acts on the user's feedback. The `Payload` field + lets the client render the full itinerary for the user before they respond: + + ```go + --8<-- "examples/go/snippets/graphs/human-input/main.go:graph-hitl-with-payload" + ``` + +## Tool-confirmation: approval prompts in LLM agents + +Tool-confirmation is a separate, LLM-agent–level mechanism for yes/no +approval prompts. Unlike graph HITL nodes, tool-confirmation works inside an +`llmagent` tool function rather than as a standalone graph node. It is useful +when you want an LLM agent to pause and ask for approval before executing a +specific tool call. + +=== "Python" + + The following code sample shows how to construct a ***RequestInput*** object + in a workflow node, including a ***response schema***: + + ```python + async def initial_prompt(ctx: Context): + """Ask the user for itinerary information""" + input_message = """ + This is an interactive concierge workflow tasked with making you a great + itinerary for you in your city of choice. If you give some details about + yourself or what you are generally looking for I can better personalize + your itinerary. + For example, input your: + City (Required), + Age, + Hobby, + Example of attraction you liked + """ + yield RequestInput(message=input_message, response_schema=str) + ``` + +=== "Go" + + Set `RequireConfirmation: true` in `functiontool.Config` for a static + yes/no approval before a tool executes, or call `ctx.RequestConfirmation` + from inside the tool for a custom hint message: + + ```go + --8<-- "examples/go/snippets/graphs/human-input/main.go:simple-hitl" + ``` + + For a custom hint with manual re-entry handling: + + ```go + --8<-- "examples/go/snippets/graphs/human-input/main.go:hitl-with-hint" + ``` diff --git a/docs/graphs/index.md b/docs/graphs/index.md index 1421fa75f6..579c2cc8a1 100644 --- a/docs/graphs/index.md +++ b/docs/graphs/index.md @@ -1,7 +1,7 @@ # Graph-based agent workflows
- Supported in ADKPython v2.0.0 + Supported in ADKPython v2.0.0Go v2.0.0
Graph-based agent workflows in ADK let you build agents with more precise control, @@ -33,63 +33,92 @@ provide the following advantages: - **Enhance reliability:** Improve the predictability of your agents by relying on structured node definitions rather than prompts alone. +!!! note "Workflow styles in ADK" + + ADK offers three complementary ways to compose multi-step work: + + - **Graph-based workflows** (this section): a declarative graph of nodes + and edges with explicit routing — best for deterministic, structured + processes. + - **[Dynamic workflows](/graphs/dynamic/):** programmatic orchestration + in your own code (loops, conditionals, recursion) — best when the + control flow is too complex or iterative for a static graph. + - **[Prebuilt workflow agents](/agents/workflow-agents/)** (sequential, + parallel, loop): higher-level building blocks for common patterns + without assembling a graph yourself. + ## Get started This section describes how to get started with graph-based agents. The following example shows how to create a sequential graph-based agent workflow that -generates a city name, looks up the current time in that city with code +generates a city name, looks up the current time in that city with a code function, and the final agent reports the information. -```python -from google.adk import Agent -from google.adk import Workflow -from google.adk import Event -from pydantic import BaseModel - -city_generator_agent = Agent( - name="city_generator_agent", - model="gemini-flash-latest", - instruction="""Return the name of a random city. - Return only the name, nothing else.""", - output_schema=str, -) - -class CityTime(BaseModel): - time_info: str # time information - city: str # city name - -def lookup_time_function(node_input: str): - """Simulate returning the current time in the specified city.""" - return CityTime(time_info="10:10 AM", city=node_input) - -city_report_agent = Agent( - name="city_report_agent", - model="gemini-flash-latest", - input_schema=CityTime, - instruction="""Output following line: - It is {CityTime.time_info} in {CityTime.city} right now.""", - output_schema=str, -) - -def completed_message_function(node_input: str): - return Event( - message=f"{node_input}\n WORKFLOW COMPLETED.", +=== "Python" + + ```python + from google.adk import Agent + from google.adk import Workflow + from google.adk import Event + from pydantic import BaseModel + + city_generator_agent = Agent( + name="city_generator_agent", + model="gemini-flash-latest", + instruction="""Return the name of a random city. + Return only the name, nothing else.""", + output_schema=str, ) -root_agent = Workflow( - name="root_agent", - edges=[ - ("START", city_generator_agent, lookup_time_function, - city_report_agent, completed_message_function) - ], -) -``` - -This sample code demonstrates how you can use the ***Workflow*** class to -assemble a simple, sequential workflow and alternate between AI agent processing -and code execution. While you could perform these steps using a single agent -with a longer prompt and a tool call, the graph-based approach gives you precise -control over the task execution order and the data output from each step. + class CityTime(BaseModel): + time_info: str # time information + city: str # city name + + def lookup_time_function(node_input: str): + """Simulate returning the current time in the specified city.""" + return CityTime(time_info="10:10 AM", city=node_input) + + city_report_agent = Agent( + name="city_report_agent", + model="gemini-flash-latest", + input_schema=CityTime, + instruction="""Output following line: + It is {CityTime.time_info} in {CityTime.city} right now.""", + output_schema=str, + ) + + def completed_message_function(node_input: str): + return Event( + message=f"{node_input}\n WORKFLOW COMPLETED.", + ) + + root_agent = Workflow( + name="root_agent", + edges=[ + ("START", city_generator_agent, lookup_time_function, + city_report_agent, completed_message_function) + ], + ) + ``` + +=== "Go" + + In ADK Go v2.0.0, sequential workflows use the graph engine: + `workflow.NewFunctionNode` wraps each step, and `workflow.Chain` wires + the nodes into a sequential `edges` slice. The framework automatically + passes each node's typed return value to the next node via + `event.Output` — no session state writes are needed. The whole graph is + wrapped in `workflowagent.New`, which produces a standard `agent.Agent`. + + ```go + --8<-- "examples/go/snippets/graphs/index/main.go:sequential-get-started" + ``` + +This sample code demonstrates how you can assemble a simple, sequential +workflow and alternate between agent processing and code execution. While you +could perform these steps using a single agent with a longer prompt and a tool +call, the graph-based approach gives you precise control over the task +execution order and the data output from each step. For more information about data handling with graph-based workflows, see [Data handling with workflow nodes and agents](/graphs/data-handling/). @@ -121,52 +150,66 @@ switching between non-deterministic AI-powered agents and deterministic code as needed. The following code sample shows how the workflow graph in Figure 2 could be -translated into a graph-based agent using the ***Workflow*** class: - -```python -process_message = Agent( - name="process_message", - model="gemini-flash-latest", - instruction="""Classify user message into either "BUG", "CUSTOMER_SUPPORT", - or "LOGISTICS". If you think a message applies to more than one category, - reply with a comma separated list of categories. - """, - output_schema=str, -) - -def router(node_input: str): - routes = node_input.split(",") - routes = [route.strip() for route in routes] - return Event(route=routes) - -def response_1_bug(): - return Event(message="Handling bug...") - -def response_2_support(): - return Event(message="Handling customer support...") - -def response_3_logistics(): - return Event(message="Handling logistics...") - -root_agent = Workflow( - name="routing_workflow", - edges=[ - ("START", process_message, router), - ( router, - { - "BUG": response_1_bug, - "CUSTOMER_SUPPORT": response_2_support, - "LOGISTICS": response_3_logistics, - } - ) - ], -) -``` - -This sample code demonstrates how you can use an ***edges*** array to define a -graph with routes between a set of *nodes*, which are discrete tasks that can -include agents, Tools, your code, and even additional ***Workflows***. For -information about building advanced graphs for workflows, see +translated into a graph-based agent: + +=== "Python" + + ```python + process_message = Agent( + name="process_message", + model="gemini-flash-latest", + instruction="""Classify user message into either "BUG", "CUSTOMER_SUPPORT", + or "LOGISTICS". If you think a message applies to more than one category, + reply with a comma separated list of categories. + """, + output_schema=str, + ) + + def router(node_input: str): + routes = node_input.split(",") + routes = [route.strip() for route in routes] + return Event(route=routes) + + def response_1_bug(): + return Event(message="Handling bug...") + + def response_2_support(): + return Event(message="Handling customer support...") + + def response_3_logistics(): + return Event(message="Handling logistics...") + + root_agent = Workflow( + name="routing_workflow", + edges=[ + ("START", process_message, router), + ( router, + { + "BUG": response_1_bug, + "CUSTOMER_SUPPORT": response_2_support, + "LOGISTICS": response_3_logistics, + } + ) + ], + ) + ``` + +=== "Go" + + In ADK Go v2.0.0, conditional routing uses `workflow.NewEmittingFunctionNode` + to set `event.Routes` and `workflow.StringRoute` edges to dispatch to the + matching handler — the direct equivalent of Python's `router` function and + dict dispatch. `workflow.Concat` merges the chain and the conditional edges + into a single `edges` slice passed to `workflowagent.New`. + + ```go + --8<-- "examples/go/snippets/graphs/index/main.go:process-pipeline" + ``` + +This sample code demonstrates how you can compose a sequence of agents to +define a graph with routes between a set of *nodes*, which are discrete tasks +that can include agents, Tools, your code, and even additional workflow agents. +For information about building advanced pipelines, see [Build graph routes for workflow agents](/graphs/routes/). ## Known limitations {#known-limitations} @@ -174,8 +217,23 @@ information about building advanced graphs for workflows, see There are some known limitations with graph-based workflows. They are *not compatible* with the following ADK features: -- **Live Streaming** functionality is not compatible with graph-based - workflows. +- **Live streaming:** Not supported in graph-based workflows. - **Integrations:** Some third-party - [Integrations](/integrations/) may not be - compatible with graph-based workflows. + [integrations](/integrations/) may not be compatible with graph-based + workflows. + +!!! note "Go: graph workflow API" + + The `workflow` package in ADK Go v2.0.0 is the direct equivalent of the + Python `Workflow` class. Use `workflow.NewFunctionNode` and + `workflow.NewAgentNode` to define nodes, `workflow.Chain` or + `workflow.Concat` with `[]workflow.Edge` to wire them, and + `workflowagent.New` to wrap the graph as a runnable agent. Conditional + routing uses `workflow.StringRoute`, `workflow.IntRoute`, or + `workflow.BoolRoute` matched against `event.Routes`. Fan-in is handled by + `workflow.NewJoinNode`. + + For advanced routing patterns and fan-out/join examples, see + [Build graph routes for workflow agents](/graphs/routes/). For prebuilt + higher-level alternatives (sequential, parallel, loop), see + [Prebuilt workflow agents](/agents/workflow-agents/). diff --git a/docs/graphs/routes.md b/docs/graphs/routes.md index 00b7c0c285..a8960e6546 100644 --- a/docs/graphs/routes.md +++ b/docs/graphs/routes.md @@ -1,7 +1,7 @@ # Build graph routes for agent workflows
- Supported in ADKPython v2.0.0 + Supported in ADKPython v2.0.0Go v2.0.0
Graph-based workflows in ADK define agent logic as a graph of execution nodes @@ -13,26 +13,53 @@ logic, this approach allows you to define a specific, step-wise process workflow in code, providing improved precision and reliability over purely prompt-based agents. -![Graph-based flight upgrade agent](/assets/graph-workflow-router.svg) - -```python -root_agent = Workflow( - name="routing_workflow", - edges=[ - ("START", process_message, router), - (router, - { - "output-1": response_1, - "output-2": response_2, - "output-3": response_3, - }, - ), - ], -) -``` - -**Figure 1.** Visualization of a task graph and the ***Workflow*** code to -implement it. +![Task graph with conditional routing between nodes](/assets/graph-workflow-router.svg) + +**Figure 1.** Visualization of a task graph and the routing code to implement it. + +=== "Python" + + ```python + root_agent = Workflow( + name="routing_workflow", + edges=[ + ("START", process_message, router), + (router, + { + "output-1": response_1, + "output-2": response_2, + "output-3": response_3, + }, + ), + ], + ) + ``` + +=== "Go" + + ADK Go v2.0.0 provides the following approach to graph-based + workflows: + + **Graph engine** (`workflowagent` + `workflow.Edge`): A node-and-edges + graph API that maps directly to Python's `Workflow(edges=[...])`. + Nodes are defined with `workflow.NewFunctionNode`, `workflow.NewAgentNode`, + or `workflow.NewDynamicNode`, edges are declared as `[]workflow.Edge`, and + the whole graph is wrapped in a `workflowagent.New` call: + + ```go + edges := workflow.Concat( + workflow.Chain(workflow.Start, classifyNode), + []workflow.Edge{ + {From: classifyNode, To: responseA, Route: workflow.StringRoute("output-1")}, + {From: classifyNode, To: responseB, Route: workflow.StringRoute("output-2")}, + {From: classifyNode, To: responseC, Route: workflow.StringRoute("output-3")}, + }, + ) + rootAgent, _ := workflowagent.New(workflowagent.Config{ + Name: "routing_workflow", + Edges: edges, + }) + ``` The advantage of using a graph-based agent workflow is the significant increase in control, predictability, and reliability over prompt-based agents. By @@ -49,189 +76,364 @@ Get started with graph-based workflows in ADK by checking out A graph is composed of execution nodes. These *nodes* can be ***Agents***, ADK ***Tools***, human input tasks, or code functions you write. Nodes can take inputs from previously executed nodes, and emit data through ***Event*** -objects. The following shows a simple ***FunctionNode*** that handles text -inputs and sends a text output: +objects. -```python -from google.adk import Event +=== "Python" -def my_function_node(node_input: str): - input_text_modified = node_input.upper() - return Event(output=input_text_modified) -``` + The following shows a simple ***FunctionNode*** that handles text inputs + and sends a text output: -For more information about transferring data between nodes, see . -[Data handling for agent workflows](/graphs/data-handling/). + ```python + from google.adk import Event -## Workflow graphs syntax + def my_function_node(node_input: str): + input_text_modified = node_input.upper() + return Event(output=input_text_modified) + ``` + +=== "Go" -You define a graph by creating an ***edges*** array, which defines a logical -execution path of *nodes* and conditions to be followed. This section -provides an overview of graph syntax in an ***edges*** array. The following code -example shows a basic workflow with two nodes to be executed in order: + In ADK Go v2.0.0, the primary node type is `workflow.NewFunctionNode`. + A `FunctionNode` wraps a plain Go function: the function returns a typed + value, and the framework automatically wraps it in a `session.Event`, + setting `event.Output`. The successor node receives this value as its + typed `input` parameter — no manual state writes or event construction + needed: -```python -from google.adk import Workflow + ```go + --8<-- "examples/go/snippets/graphs/routes/main.go:function-node" + ``` -root_agent = Workflow( - name="sequential_workflow", - edges=[("START", task_A_node, task_B_node)], -) -``` +For more information about transferring data between nodes, see +[Data handling for agent workflows](/graphs/data-handling/). -!!! caution "Caution: Workflows and agent limitations" +## Workflow graphs syntax - You can add ***Agents***, or ***LlmAgents***, to graph-based workflows, - however they must be set to a task or single-turn mode. For more - information about agent modes, see +You define a graph by composing workflow agents. This section provides an +overview of the common routing patterns. + +!!! caution "Caution: Workflow agent limitations" + + You can add ***LlmAgents*** to graph-based workflows. However, they must + be configured for single-turn or task mode. For more information about + agent modes, see [Build collaborative agent teams](/workflows/collaboration/#mode-configuration-and-behaviors). ### Route sequences -The ***edges*** array executes nodes based on the order or nodes presented in -the array, starting with the first row and proceeding through the subsequent -rows until execution is complete. The first row of the ***edges*** array uses -the ***START*** keyword to indicate the beginning of a graph execution, with -each listed node executed in sequence, as shown in the following code -snippets: - -```python -edges=[("START", task_A_node)] # single node run -edges=[("START", - task_A_node, - task_B_node, - task_C_node)] # 3 nodes run in order -``` - -You can also use ***START*** more than once to initiate parallel tasks at the -beginning of a workflow graph, as shown in the following code snippet: - -```python -edges=[ - ("START", parallel_task_A), - ("START", parallel_task_B), - ("START", parallel_task_C), -] -``` - -!!! warning "Caution: Limitations on parallel nodes" - - Not all workflow nodes or subagents can be run in parallel. In particular, - you cannot run multiple interactive chat sessions within the same agent - session. +A sequential route runs each node once, in the listed order. -### Route branches and conditional execution +=== "Python" -The subsequent rows of the ***edges*** arrays after the START keyword define -additional execution logic for nodes. For branching paths, which is how you create a conditional node, you define a node, -usually a ***FunctionNode***, that outputs an Event with a specific ***route*** value. In the edges graph, you then define the conditional execution logic by mapping these route values to target nodes, as shown in the following code example: + The `edges` array uses the `START` keyword to indicate the beginning of a + graph execution, with each listed node executed in sequence: -```python -def router(node_input: str): - """Route to task B or C based on node_input.""" - if condition(node_input): - return Event(route="RUN_TASK_C") - return Event(route="RUN_TASK_B") + ```python + edges=[("START", task_A_node)] # single node run + edges=[("START", + task_A_node, + task_B_node, + task_C_node)] # 3 nodes run in order + ``` -task_B_node = Agent(name="task_B_agent") # An agent to execute node B +=== "Go" -def task_C_node(node_input: str): - """A FunctionNode to execute node C.""" - return Event(output="Task C completed") + `workflow.Chain(workflow.Start, nodeA, nodeB, nodeC)` wires nodes into a + sequential edge slice. Each node's typed return value is forwarded to the + next node via `event.Output` — no session state writes needed: -root_agent = Workflow( - name="routing_workflow", - edges=[ - ("START", task_A_node, router), - (router, - { - # "route value": node_to_run - "RUN_TASK_B": task_B_node, - "RUN_TASK_C": task_C_node, - }, - ), - ], -) -``` + ```go + --8<-- "examples/go/snippets/graphs/routes/main.go:sequential-nodes" + ``` + +### Route branches and conditional execution + +=== "Python" + + In Python, branching is handled by a `FunctionNode` that returns an + `Event(route=...)` value, which the `edges` dict dispatches to different nodes. + + ```python + def router(node_input: str): + """Route to task B or C based on node_input.""" + if condition(node_input): + return Event(route="RUN_TASK_C") + return Event(route="RUN_TASK_B") + + task_B_node = Agent(name="task_B_agent") # An agent to execute node B + + def task_C_node(node_input: str): + """A FunctionNode to execute node C.""" + return Event(output="Task C completed") + + root_agent = Workflow( + name="routing_workflow", + edges=[ + ("START", task_A_node, router), + (router, + { + # "route value": node_to_run + "RUN_TASK_B": task_B_node, + "RUN_TASK_C": task_C_node, + }, + ), + ], + ) + ``` + +=== "Go" + + In ADK Go v2.0.0, conditional dispatch uses the `workflow` graph engine. + A node sets `Event.Routes` to one or more string route keys, and each + `workflow.Edge` selects its successor using a `workflow.Route` matcher: + + - `workflow.StringRoute("category")` — matches a single string value + - `workflow.IntRoute(n)` or `workflow.MultiRoute[int]{1, 2, 3}` — matches + integer values + - `workflow.BoolRoute(true)` — matches a boolean value + - `workflow.Default` — matches when no other route on the same source + node matches + + The following pattern is the Go equivalent of the Python router: + + ```go + // classifyNode emits an Event with Routes=[]string{"BUG"}, + // ["CUSTOMER_SUPPORT"], or ["LOGISTICS"] based on the message. + edges := workflow.Concat( + workflow.Chain(workflow.Start, processMessage, classifyNode), + []workflow.Edge{ + {From: classifyNode, To: bugHandler, Route: workflow.StringRoute("BUG")}, + {From: classifyNode, To: supportHandler, Route: workflow.StringRoute("CUSTOMER_SUPPORT")}, + {From: classifyNode, To: logisticsHandler, Route: workflow.StringRoute("LOGISTICS")}, + }, + ) + rootAgent, _ := workflowagent.New(workflowagent.Config{ + Name: "routing_workflow", + Edges: edges, + }) + ``` + + `workflow.EdgeBuilder` provides a fluent alternative to assembling the + `[]workflow.Edge` slice by hand. The builder's `Add`, `AddFanOut`, and + `AddFanIn` methods express the same topology with less repetition: + + ```go + eb := workflow.NewEdgeBuilder() + eb.Add(workflow.Start, processMessage) + eb.Add(processMessage, classifyNode) + eb.AddRoute(classifyNode, bugHandler, workflow.StringRoute("BUG")) + eb.AddRoute(classifyNode, supportHandler, workflow.StringRoute("CUSTOMER_SUPPORT")) + eb.AddRoute(classifyNode, logisticsHandler, workflow.StringRoute("LOGISTICS")) + + rootAgent, _ := workflowagent.New(workflowagent.Config{ + Name: "routing_workflow", + Edges: eb.Build(), + }) + ``` + + For complete, runnable routing examples see: + [string routing](https://github.com/google/adk-go/tree/v2/examples/workflow/routing/string), + [int / multi-value routing](https://github.com/google/adk-go/tree/v2/examples/workflow/routing/int), + and [LLM-driven routing](https://github.com/google/adk-go/tree/v2/examples/workflow/routing/llm). + + !!! note "Prebuilt agents: encoding routing in state" + + When using `sequentialagent` / `parallelagent` / `loopagent` instead + of the graph engine, there is no `Event.Routes` dispatch. Encode the + routing decision in session state via `OutputKey` and let downstream + agents inspect it in their `Instruction` template, or use a `loopagent` + with an `Escalate`-based exit — see the + [loop and escalation example](#loop-and-escalation-exit) below. ## Parallel tasks: fan out and join paths You can create graphs that split execution across multiple, parallel nodes, and typically you need to assemble the output of each node for further processing. -This task execution pattern has two stages. The workflow first fans out when it -starts multiple parallel tasks, and then it re-joins those paths when those -those tasks are completed before proceeding to the next step. - -You accomplish the join step by using a ***JoinNode*** object, which waits for -each parallel task to complete and then passes the collection of outputs from -these nodes to the next node. +This task execution pattern has two stages. The workflow first fans out when it +starts multiple parallel tasks, and then it re-joins those paths when those +tasks are completed before proceeding to the next step. ![Tasks connecting to a JoinNode](/assets/graph-joinnode.svg) -**Figure 2.** The output of parallel task nodes can be assembled using a -JoinNode object. - -The following code snippet shows how to start three parallel tasks from -***START*** and use a basic ***JoinNode*** object to join their outputs before -running the final task: +**Figure 2.** The output of parallel task nodes can be assembled and joined +before passing results to the next step. -```python -​​from google.adk.workflow import JoinNode +=== "Python" -my_join_node = JoinNode(name="my_join_node") + You accomplish the join step by using a ***JoinNode*** object, which waits + for each parallel task to complete and then passes the collection of outputs + from these nodes to the next node. -edges=[ - ("START", parallel_task_A, my_join_node), - ("START", parallel_task_B, my_join_node), - ("START", parallel_task_C, my_join_node), - (my_join_node, final_task_D), -] -``` + ```python + from google.adk.workflow import JoinNode -!!! warning "Caution: Stuck JoinNode from incomplete nodes" + my_join_node = JoinNode(name="my_join_node") - The ***JoinNode*** object proceeds only after all its upstream nodes have - provided an Event output. If one of the upstream nodes fails to provide output, - the JoinNode is stuck and workflow execution stops. Make sure to include - failsafe output from any node that outputs to a ***JoinNode***. + edges=[ + ("START", parallel_task_A, my_join_node), + ("START", parallel_task_B, my_join_node), + ("START", parallel_task_C, my_join_node), + (my_join_node, final_task_D), + ] + ``` + + !!! warning "Caution: Stuck JoinNode from incomplete nodes" + + The ***JoinNode*** object proceeds only after all its upstream nodes + have provided an Event output. If one of the upstream nodes fails to + provide output, the JoinNode is stuck and workflow execution stops. + Make sure to include failsafe output from any node that outputs to a + ***JoinNode***. + +=== "Go" + + ADK Go v2.0.0 provides `workflow.NewJoinNode` for true fan-in in the + graph engine: fan-out edges from `workflow.Start` (or any shared source + node) feed in parallel to the join node, which waits for all of them to + complete before emitting a `map[string]any` keyed by predecessor node name + to the next node. + + `workflow.EdgeBuilder` makes the fan-out / fan-in wiring concise with its + dedicated `AddFanOut` and `AddFanIn` helpers (as shown in the + [complex workflow example](https://github.com/google/adk-go/tree/v2/examples/workflow/complex)): + + ```go + gatherNode := workflow.NewJoinNode("gather") + + eb := workflow.NewEdgeBuilder() + eb.AddFanOut(workflow.Start, researchNodeA, researchNodeB, researchNodeC) + eb.AddFanIn(gatherNode, researchNodeA, researchNodeB, researchNodeC) + eb.Add(gatherNode, formatNode) + eb.Add(formatNode, synthesisNode) + + rootAgent, _ := workflowagent.New(workflowagent.Config{ + Name: "research_pipeline", + Edges: eb.Build(), + }) + ``` + + The following snippet shows the complete fan-out / join pattern using + `workflow.NewJoinNode` and `EdgeBuilder.AddFanOut` / `AddFanIn`: + + ```go + --8<-- "examples/go/snippets/graphs/routes/main.go:parallel-fan-out" + ``` + + !!! warning "Caution: Stuck JoinNode from incomplete nodes" + + `workflow.NewJoinNode` proceeds only after every predecessor node has + emitted an `event.Output`. If a predecessor fails without emitting + output, the JoinNode is stuck and workflow execution stops. Attach a + `RetryConfig` to flaky predecessor nodes to guard against transient + failures. ## Nested workflows When building more complex workflows, you may want to encapsulate the functionality for specific tasks into reusable workflows. One or more -***Workflow*** objects can be used as a node within the graph of another -workflow agent to accomplish this goal. +workflow agents can be used as a sub-agent within another workflow agent to +accomplish this goal. ![Nested Workflows inside a parent Workflow](/assets/graph-workflow-nodes.svg) -**Figure 3.** Nested ***Workflows*** as nodes inside a parent ***Workflow***. - -The following code snippet shows how to implement a workflow agent with two -nested more ***Workflow*** objects (workflow_B, workflow_C) as nodes in the -graph: - -```python -from google.adk import Workflow - -root_agent = Workflow( - name="parent_workflow", - edges=[ - ("START", task_A1, router), - (router, { - "RUN_WORKFLOW_B": workflow_B, - "RUN_WORKFLOW_C": workflow_C, - }, - ), - ], -) -``` - -### Nested workflow data output - -Output for nested Workflow objects works slightly differently from individual -nodes. When the nested workflow completes one of its nodes, it transmits data -to the next node in the nested workflow's graph *and* the system bubbles up the -Event for that node to the parent workflow for process traceability. When the -nested workflow completes the last node in its process, the parent node extracts -data from the final leaf nodes and emits it as the output of the nested -workflow. +**Figure 3.** Nested workflow agents as sub-agents inside a parent workflow. + +=== "Python" + + ```python + from google.adk import Workflow + + root_agent = Workflow( + name="parent_workflow", + edges=[ + ("START", task_A1, router), + (router, { + "RUN_WORKFLOW_B": workflow_B, + "RUN_WORKFLOW_C": workflow_C, + }, + ), + ], + ) + ``` + + #### Nested workflow data output + + Output for nested Workflow objects works slightly differently from + individual nodes. When the nested workflow completes one of its nodes, it + transmits data to the next node in the nested workflow's graph *and* the + system bubbles up the Event for that node to the parent workflow for + process traceability. When the nested workflow completes the last node in + its process, the parent node extracts data from the final leaf nodes and + emits it as the output of the nested workflow. + +=== "Go" + + ADK Go v2.0.0 supports nested workflows in two complementary ways: + + **Graph engine** (`workflowagent` + `workflow.Edge`): A `workflowagent` + created with `workflowagent.New` is itself an `agent.Agent`, so it can + be wrapped with `workflow.NewAgentNode` and used as a node inside another + workflow's `edges` slice. The inner workflow runs to completion as a single + node from the outer graph's perspective, and its terminal output is emitted + as the node output on the outer graph's edge: + + ```go + innerNode, _ := workflow.NewAgentNode(innerWorkflowAgent, workflow.NodeConfig{}) + + outerEdges := workflow.Chain(workflow.Start, outerStepNode, innerNode, finalNode) + rootAgent, _ := workflowagent.New(workflowagent.Config{ + Name: "parent_workflow", + Edges: outerEdges, + }) + ``` + + The following snippet shows both the inner and outer graph construction. + `workflow.NewAgentNode` wraps the inner `workflowagent` so it can be + placed in the outer graph's `workflow.Chain`: + + ```go + --8<-- "examples/go/snippets/graphs/routes/main.go:nested-workflows" + ``` + +## Loop and escalation exit + +A loop repeats a set of steps until a termination condition is met. In Python +this is expressed as a back-edge in the `edges` graph that routes back to an +earlier node. In ADK Go v2.0.0, the graph engine supports the same pattern +directly: add an edge from a downstream node back to an earlier node with a +route condition, and the engine re-activates the target node with a fresh +lifecycle on each iteration. + +=== "Python" + + ```python + def router(node_input: str): + """Route to task B or C based on node_input.""" + if condition(node_input): + return Event(route="RUN_TASK_C") + return Event(route="RUN_TASK_B") + + root_agent = Workflow( + name="routing_workflow", + edges=[ + ("START", task_A_node, router), + (router, + { + "RUN_TASK_B": task_B_node, + "RUN_TASK_C": task_C_node, + }, + ), + ], + ) + ``` + +=== "Go" + + The following example uses the graph engine with `workflow.EdgeBuilder`. + The critic node returns a verdict, a router node sets `Event.Routes`, and + a back-edge from the refiner to the critic creates the loop. When the + critic is satisfied it routes to the terminal `done` node instead: + + ```go + --8<-- "examples/go/snippets/graphs/routes/main.go:loop-escalate" + ``` diff --git a/docs/release-notes.md b/docs/release-notes.md index 000025244e..9966bb69e8 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,3 +8,10 @@ language. For detailed information on ADK releases, see these locations: * [ADK Go release notes](https://github.com/google/adk-go/releases) * [ADK Java release notes](https://github.com/google/adk-java/releases) * [ADK Kotlin release notes](https://github.com/google/adk-kotlin/releases) + +!!! tip "ADK Go v2.0.0" + + ADK Go v2.0.0 introduces graph-based and dynamic workflow support, a new + `workflow` package, agent execution modes, and Human-in-the-Loop tool + confirmation. See the [ADK 2.0 release page](/2.0/) for the full feature + list and Go 1.x migration guidance. diff --git a/docs/workflows/collaboration.md b/docs/workflows/collaboration.md index b0d690b2ca..bffae32a49 100644 --- a/docs/workflows/collaboration.md +++ b/docs/workflows/collaboration.md @@ -1,7 +1,7 @@ # Build collaborative agent teams
- Supported in ADKPython v2.0.0 + Supported in ADKPython v2.0.0Go v2.0.0
Some complex tasks may require multiple agents with specific responsibilities @@ -39,27 +39,40 @@ agent behavior. The following code example shows how to set operating modes for a small team of subagents and assign them to a coordinator agent: -```python -from google.adk import Agent - -weather_agent = Agent( - name="weather_checker", - mode="single_turn", # no user interaction - tools=[get_weather, user_info, geocode_address], -) -flight_agent = Agent( - name="flight_booker", - mode="task", # can ask user questions - input_schema=FlightInput, - output_schema=FlightResult, - tools=[search_flights, book_flight], -) -root = Agent( - name="travel_planner", # coordinator agent - sub_agents=[weather_agent, flight_agent], - # Auto-injects: request_task_weather_checker, request_task_flight_booker -) -``` +=== "Python" + + ```python + from google.adk import Agent + + weather_agent = Agent( + name="weather_checker", + mode="single_turn", # no user interaction + tools=[get_weather, user_info, geocode_address], + ) + flight_agent = Agent( + name="flight_booker", + mode="task", # can ask user questions + input_schema=FlightInput, + output_schema=FlightResult, + tools=[search_flights, book_flight], + ) + root = Agent( + name="travel_planner", # coordinator agent + sub_agents=[weather_agent, flight_agent], + # Auto-injects: request_task_weather_checker, request_task_flight_booker + ) + ``` + +=== "Go" + + In ADK Go v2.0.0, the `Mode` field on `llmagent.Config` accepts the same + mode strings as Python: `"chat"`, `"task"`, and `"single_turn"`. Declaring + `SubAgents` on the coordinator agent causes ADK to automatically generate + `request_task_` delegation tools, exactly as in Python. + + ```go + --8<-- "examples/go/snippets/workflows/collaboration/main.go:get-started" + ``` When you run this workflow, the `travel_planner` coordinator agent automatically identifies and assigns tasks to the subagents. When a subagent completes @@ -139,10 +152,12 @@ Workflow Agent graph nodes, and with ***LlmAgent*** instances. However the execution transfer behavior is different depending on the calling, or parent, agent: -**As a workflow graph node:** When a task agent is placed within a workflow -graph, such as ***SequentialAgent***, ***ParallelAgent***, the agent executes -its task. Upon completion, control automatically advances to the next node based -on the logic of the workflow agent's graph. +**As a workflow graph node:** When a task or single-turn agent is placed within +a workflow graph — such as a ***SequentialAgent*** or ***ParallelAgent*** (Python +and Go prebuilt agents), or wrapped with `workflow.NewAgentNode` in the ADK Go +v2.0.0 graph engine — the agent executes its task. Upon completion, control +automatically advances to the next node based on the logic of the workflow +agent's graph. **As a transferee from an LlmAgent:** When a parent ***LlmAgent*** transfers control to a task agent via `request_task`, the task agent executes until it diff --git a/examples/go/go.mod b/examples/go/go.mod index 9da1ff2dce..ea5a452c23 100644 --- a/examples/go/go.mod +++ b/examples/go/go.mod @@ -1,9 +1,10 @@ module snippets -go 1.25.1 +go 1.25.0 require ( - google.golang.org/adk v1.3.0 + google.golang.org/adk v1.4.0 + google.golang.org/adk/v2 v2.0.0-20260630132642-893e4a403a8d google.golang.org/genai v1.57.0 ) diff --git a/examples/go/go.sum b/examples/go/go.sum index 3b64a84a66..83bd390ee6 100644 --- a/examples/go/go.sum +++ b/examples/go/go.sum @@ -105,8 +105,10 @@ golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/adk v1.3.0 h1:paUr9uM2qANnMUAQ4ydMXMCnM1HtymhDYl8y7gnKvqs= -google.golang.org/adk v1.3.0/go.mod h1:R8tNFnI/eiBXHn7zJPJtqdiK/WXC+tVkyuZsXyNZXN4= +google.golang.org/adk v1.4.0 h1:Qi4KB9YKD00/I5K9v3QsZ9ng5YiZQ7MfMgM8BZjNcsM= +google.golang.org/adk v1.4.0/go.mod h1:R8tNFnI/eiBXHn7zJPJtqdiK/WXC+tVkyuZsXyNZXN4= +google.golang.org/adk/v2 v2.0.0-20260630132642-893e4a403a8d h1:1Y/1n1U7PmtnceaDu2SWf3+bX172UkphaEaXGsGsPSs= +google.golang.org/adk/v2 v2.0.0-20260630132642-893e4a403a8d/go.mod h1:fPuMPT5s3LsWu97mdeFjTPZu/02tIALWRWeqHL2FWKE= google.golang.org/api v0.279.0 h1:hsx2M2OaRcaKtVYK6vXEUnQvdjnend7ZYES+lYaot74= google.golang.org/api v0.279.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ= google.golang.org/genai v1.57.0 h1:qTyG2ynz5dQy2jF4CvZdLHHVslhR0heMue+zM1a4GNM= diff --git a/examples/go/snippets/graphs/data-handling/main.go b/examples/go/snippets/graphs/data-handling/main.go new file mode 100644 index 0000000000..28f61ca1f6 --- /dev/null +++ b/examples/go/snippets/graphs/data-handling/main.go @@ -0,0 +1,412 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package main demonstrates data-handling patterns for ADK Go v2 workflow agents. +// +// NOTE: This file requires google.golang.org/adk (the workflow package), +// available in ADK Go v2.0.0 and later. +// +// # Data flow in ADK Go v2 +// +// ADK Go v2 provides two complementary data-passing mechanisms depending on +// which agent style you use: +// +// ## workflow package (graph engine: FunctionNode / AgentNode / DynamicNode) +// +// Nodes communicate by setting fields on session.Event: +// +// - Event.Output (any): the node's typed return value, set automatically by +// the framework when a FunctionNode returns a non-*genai.Content value. +// Successor nodes receive this as their typed `input` parameter via +// workflow.RunNode. +// - Event.Routes ([]string): routing keys a node emits to select which edge +// to follow. Set explicitly by an emitting function node using +// session.NewEvent + ev.Routes = []string{"category"}. +// - Event.NodeInfo (*session.NodeInfo): scheduler metadata (path, +// MessageAsOutput, OutputFor). Set by the workflow engine; nodes do not +// set this directly. +// - Event.Content (*genai.Content): when a FunctionNode returns a string or +// *genai.Content, the framework stores it here for the LLM / user stream. +// +// ## Prebuilt workflow agents (sequentialagent / parallelagent / loopagent) +// +// These agents communicate through session state: +// +// - llmagent.Config.OutputKey: the framework writes the agent's final text +// response to state[OutputKey] after each turn. +// - ctx.Session().State().Set / .Get: write/read arbitrary values from state +// inside custom code. +// - {key} in Instruction: the framework substitutes state["key"] into the +// prompt before calling the model. +package main + +import ( + "context" + "fmt" + "log" + "strings" + + "google.golang.org/genai" + + "google.golang.org/adk/v2/agent" + "google.golang.org/adk/v2/agent/llmagent" + "google.golang.org/adk/v2/agent/workflowagent" + "google.golang.org/adk/v2/agent/workflowagents/sequentialagent" + "google.golang.org/adk/v2/model" + "google.golang.org/adk/v2/model/gemini" + "google.golang.org/adk/v2/session" + "google.golang.org/adk/v2/workflow" +) + +// --8<-- [start:event-output] +// newEventOutputPipeline demonstrates the primary data-passing mechanism for +// workflow package nodes: a FunctionNode returns a typed Go value, and the +// framework automatically sets event.Output to that value. The successor node +// receives it as its typed `input` parameter. +// +// This mirrors the Python pattern exactly: +// +// def my_function_node(node_input: str): +// return Event(output=node_input.upper()) +// +// In Go, the function simply returns the value — no Event construction needed. +func newEventOutputPipeline() (agent.Agent, error) { + upperFn := func(_ agent.Context, input string) (string, error) { + return strings.ToUpper(input), nil + } + + suffixFn := func(_ agent.Context, input string) (string, error) { + return input + " IS AWESOME!", nil + } + + nodeA := workflow.NewFunctionNode("upper", upperFn, workflow.NodeConfig{}) + nodeB := workflow.NewFunctionNode("suffix", suffixFn, workflow.NodeConfig{}) + + // workflow.Chain wires START → nodeA → nodeB. The output of nodeA is + // delivered as the typed input of nodeB via event.Output. + return workflowagent.New(workflowagent.Config{ + Name: "event_output_pipeline", + Description: "Demonstrates Event.Output data flow between FunctionNodes.", + Edges: workflow.Chain(workflow.Start, nodeA, nodeB), + }) +} + +// --8<-- [end:event-output] + +// --8<-- [start:routing-output] +// classifyAndRoute shows how to set event.Routes alongside event.Output from +// an emitting FunctionNode. The function constructs a session.Event directly, +// sets Routes to select the conditional edge, and sets Output to forward the +// payload to the successor node. +// +// This mirrors the Python pattern: +// +// def router(node_input: str): +// return Event(route="BUG") +func classifyAndRoute(ctx agent.Context, msg string, emit func(*session.Event) error) (any, error) { + category := classifyMessage(msg) + + ev := session.NewEvent(ctx, ctx.InvocationID()) + ev.Routes = []string{category} // drives edge dispatch + ev.Output = msg // forwarded as typed input to the successor + if err := emit(ev); err != nil { + return nil, err + } + return nil, nil // nil suppresses the automatic terminal event +} + +func classifyMessage(msg string) string { + switch { + case strings.Contains(strings.ToLower(msg), "bug"): + return "BUG" + case strings.Contains(strings.ToLower(msg), "help"): + return "CUSTOMER_SUPPORT" + default: + return "LOGISTICS" + } +} + +func newRoutingPipeline() (agent.Agent, error) { + classifyNode := workflow.NewEmittingFunctionNode("classify", classifyAndRoute, workflow.NodeConfig{}) + + bugHandler := workflow.NewFunctionNode("bug_handler", + func(_ agent.Context, msg string) (string, error) { + return "Handling bug: " + msg, nil + }, workflow.NodeConfig{}) + + supportHandler := workflow.NewFunctionNode("support_handler", + func(_ agent.Context, msg string) (string, error) { + return "Handling support: " + msg, nil + }, workflow.NodeConfig{}) + + logisticsHandler := workflow.NewFunctionNode("logistics_handler", + func(_ agent.Context, msg string) (string, error) { + return "Handling logistics: " + msg, nil + }, workflow.NodeConfig{}) + + edges := workflow.Concat( + workflow.Chain(workflow.Start, classifyNode), + []workflow.Edge{ + {From: classifyNode, To: bugHandler, Route: workflow.StringRoute("BUG")}, + {From: classifyNode, To: supportHandler, Route: workflow.StringRoute("CUSTOMER_SUPPORT")}, + {From: classifyNode, To: logisticsHandler, Route: workflow.StringRoute("LOGISTICS")}, + }, + ) + return workflowagent.New(workflowagent.Config{ + Name: "routing_pipeline", + Description: "Classifies and routes a message using Event.Routes.", + Edges: edges, + }) +} + +// --8<-- [end:routing-output] + +// --8<-- [start:structured-output] +// newStructuredOutputPipeline shows how to pass a struct from one FunctionNode +// to another. The framework serialises the return value into event.Output and +// deserialises it back into the successor's typed input parameter. +// +// This is the Go equivalent of: +// +// class CityTime(BaseModel): +// time_info: str +// city: str +// +// def lookup_time_function(city: str): +// return Event(output=CityTime(time_info="10:10 AM", city=city)) +// +// def city_report(node_input: CityTime): +// return Event(output=f"It is {node_input.time_info} in {node_input.city}.") +type CityTime struct { + TimeInfo string `json:"time_info"` + City string `json:"city"` +} + +func newStructuredOutputPipeline(ctx context.Context, geminiModel model.LLM) (agent.Agent, error) { + lookupTimeFn := func(_ agent.Context, city string) (CityTime, error) { + // Simulate looking up the current time in the city. + return CityTime{TimeInfo: "10:10 AM", City: city}, nil + } + + cityReportAgent, err := llmagent.New(llmagent.Config{ + Name: "city_report_agent", + Model: geminiModel, + Description: "Reports the city and current time from the previous node's output.", + // When wrapped as an AgentNode, the predecessor's event.Output + // is delivered as the agent's user content. The {key} template + // syntax is not required — the struct fields are provided inline. + Instruction: "Report the city time information you received in a friendly sentence.", + }) + if err != nil { + return nil, fmt.Errorf("cityReportAgent: %w", err) + } + + lookupTimeNode := workflow.NewFunctionNode("lookup_time", lookupTimeFn, workflow.NodeConfig{}) + cityReportNode, err := workflow.NewAgentNode(cityReportAgent, workflow.NodeConfig{}) + if err != nil { + return nil, fmt.Errorf("NewAgentNode: %w", err) + } + + return workflowagent.New(workflowagent.Config{ + Name: "city_time_pipeline", + Edges: workflow.Chain(workflow.Start, lookupTimeNode, cityReportNode), + SubAgents: []agent.Agent{cityReportAgent}, + }) +} + +// --8<-- [end:structured-output] + +// --8<-- [start:output-key] +// newOutputKeyPipeline demonstrates the OutputKey mechanism for the prebuilt +// sequentialagent. When OutputKey is set on an llmagent.Config, the framework +// automatically writes the agent's final text response to session state under +// that key. Downstream agents read it by referencing {key} in their Instruction. +// +// This pattern applies to sequentialagent / parallelagent / loopagent. +// For the workflow package (FunctionNode / AgentNode), use Event.Output instead. +func newOutputKeyPipeline(ctx context.Context, geminiModel model.LLM) (agent.Agent, error) { + step1, err := llmagent.New(llmagent.Config{ + Name: "step_1", + Model: geminiModel, + Description: "Transforms the user's text.", + Instruction: "Convert the user's message to uppercase. Output only the transformed text.", + OutputKey: "upper_result", + }) + if err != nil { + return nil, fmt.Errorf("step1: %w", err) + } + + step2, err := llmagent.New(llmagent.Config{ + Name: "step_2", + Model: geminiModel, + Description: "Reports the transformed text.", + Instruction: "The transformed text is: {upper_result}. Report it to the user.", + }) + if err != nil { + return nil, fmt.Errorf("step2: %w", err) + } + + return sequentialagent.New(sequentialagent.Config{ + AgentConfig: agent.Config{ + Name: "output_key_pipeline", + SubAgents: []agent.Agent{step1, step2}, + }, + }) +} + +// --8<-- [end:output-key] + +// --8<-- [start:state-scopes] +// stateScopes shows how session-state key prefixes control the lifetime and +// visibility of stored values. This pattern applies to the prebuilt workflow +// agents (sequentialagent / parallelagent / loopagent) and to tools and +// callbacks. For the workflow package (FunctionNode / AgentNode), prefer +// returning values directly via Event.Output. +// +// Available prefixes: +// +// session.KeyPrefixApp ("app:") – shared across all users and sessions +// session.KeyPrefixUser ("user:") – tied to the user, shared across sessions +// session.KeyPrefixTemp ("temp:") – discarded after the current invocation +// +// Keys with no prefix persist for the lifetime of the session. +func stateScopes(ctx agent.Context) error { + st := ctx.Session().State() + + // Session-scoped (no prefix) — persists for the life of this session. + if err := st.Set("attempts", 0); err != nil { + return fmt.Errorf("state.Set attempts: %w", err) + } + + // App-scoped — shared across all users and sessions for this app. + if err := st.Set(session.KeyPrefixApp+"global_counter", 42); err != nil { + return fmt.Errorf("state.Set app:global_counter: %w", err) + } + + // User-scoped — shared across all sessions belonging to this user. + if err := st.Set(session.KeyPrefixUser+"login_count", 1); err != nil { + return fmt.Errorf("state.Set user:login_count: %w", err) + } + + // Temp-scoped — discarded after this invocation ends. + if err := st.Set(session.KeyPrefixTemp+"scratch", "ephemeral"); err != nil { + return fmt.Errorf("state.Set temp:scratch: %w", err) + } + + return nil +} + +// --8<-- [end:state-scopes] + +// --8<-- [start:input-output-schema] +// FlightSearchInput is the typed input schema for the flight-search agent node. +// workflow.NewAgentNodeTyped[FlightSearchInput, FlightSearchOutput] reflects +// these structs into *jsonschema.Schema automatically — no hand-built schema +// construction needed. +type FlightSearchInput struct { + Origin string `json:"origin" jsonschema:"description=Departure airport code e.g. SFO"` + Destination string `json:"destination" jsonschema:"description=Arrival airport code e.g. CDG"` + DepartureDate string `json:"departure_date" jsonschema:"description=Travel date in YYYY-MM-DD format"` +} + +// FlightSearchOutput is the typed output schema for the flight-search agent node. +type FlightSearchOutput struct { + CheapestPrice string `json:"cheapest_price" jsonschema:"description=Cheapest available fare e.g. $450"` + FlightCount string `json:"flight_count" jsonschema:"description=Number of matching flights found"` +} + +// newSchemaAgentPipeline demonstrates workflow.NewAgentNodeTyped, which infers +// *jsonschema.Schema from the generic type parameters. This is the Go equivalent +// of Python's: +// +// flight_searcher = Agent( +// input_schema=FlightSearchInput, +// output_schema=FlightSearchOutput, +// ... +// ) +// +// The node's event.Output carries the structured result to the successor — +// no OutputKey or state write is needed. +func newSchemaAgentPipeline(ctx context.Context, geminiModel model.LLM) (agent.Agent, error) { + flightSearchAgent, err := llmagent.New(llmagent.Config{ + Name: "flight_searcher", + Model: geminiModel, + Description: "Searches for available flights and returns structured results.", + Instruction: `You are a flight-search assistant. Respond ONLY with a JSON object.`, + }) + if err != nil { + return nil, fmt.Errorf("flightSearchAgent: %w", err) + } + + synthAgent, err := llmagent.New(llmagent.Config{ + Name: "trip_assistant", + Model: geminiModel, + Description: "Summarises flight search results for the user.", + Instruction: `You help users plan trips. Summarise the flight result you received.`, + }) + if err != nil { + return nil, fmt.Errorf("synthAgent: %w", err) + } + + // NewAgentNodeTyped[In, Out] reflects FlightSearchInput and FlightSearchOutput + // into *jsonschema.Schema automatically. The node enforces the input schema + // and constrains the model reply to the output schema's shape. + flightNode, err := workflow.NewAgentNodeTyped[FlightSearchInput, FlightSearchOutput](flightSearchAgent, workflow.NodeConfig{}) + if err != nil { + return nil, fmt.Errorf("flightNode: %w", err) + } + + synthNode, err := workflow.NewAgentNode(synthAgent, workflow.NodeConfig{}) + if err != nil { + return nil, fmt.Errorf("synthNode: %w", err) + } + + return workflowagent.New(workflowagent.Config{ + Name: "flight_booking_pipeline", + Edges: workflow.Chain(workflow.Start, flightNode, synthNode), + SubAgents: []agent.Agent{flightSearchAgent, synthAgent}, + }) +} + +// --8<-- [end:input-output-schema] + +func main() { + ctx := context.Background() + + if _, err := newEventOutputPipeline(); err != nil { + log.Printf("newEventOutputPipeline: %v", err) + } + + if _, err := newRoutingPipeline(); err != nil { + log.Printf("newRoutingPipeline: %v", err) + } + + model, err := gemini.NewModel(ctx, "gemini-flash-latest", &genai.ClientConfig{}) + if err != nil { + log.Printf("gemini.NewModel: %v", err) + return + } + + if _, err := newStructuredOutputPipeline(ctx, model); err != nil { + log.Printf("newStructuredOutputPipeline: %v", err) + } + + if _, err := newOutputKeyPipeline(ctx, model); err != nil { + log.Printf("newOutputKeyPipeline: %v", err) + } + + if _, err := newSchemaAgentPipeline(ctx, model); err != nil { + log.Printf("newSchemaAgentPipeline: %v", err) + } +} diff --git a/examples/go/snippets/graphs/dynamic/main.go b/examples/go/snippets/graphs/dynamic/main.go new file mode 100644 index 0000000000..3d3201c272 --- /dev/null +++ b/examples/go/snippets/graphs/dynamic/main.go @@ -0,0 +1,501 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// Package main demonstrates dynamic workflow patterns in ADK Go v2. +// +// NOTE: This file requires the google.golang.org/adk/workflow package, +// which is available in ADK Go v2.0.0 and later. The workflow package is +// not present in v1.x releases. The snippets in this file are based on the +// examples found in https://github.com/google/adk-go/releases/tag/v2.0.0/examples/workflow/. +// +// Key types and functions used in this file: +// +// - workflow.NewFunctionNode[IN, OUT] – wraps a plain Go function as a workflow node. +// Equivalent to Python's @node decorator on a regular function. +// +// - workflow.NewDynamicNode[IN, OUT] – wraps an orchestrator function that calls +// workflow.RunNode to schedule child nodes at runtime. Equivalent to +// Python's @node(rerun_on_resume=True) on an async orchestrator. +// +// - workflow.RunNode[OUT] – executes a child node from inside a dynamic +// node body and returns its typed output. Equivalent to ctx.run_node(). +// +// - workflow.NewAgentNode – wraps an agent.Agent as a workflow Node so it +// can be invoked via workflow.RunNode inside a dynamic orchestrator. +// +// - workflow.NewParallelWorker – runs a wrapped node concurrently for each +// item in a list input. Equivalent to asyncio.gather() in Python. +// +// - workflow.ResumeOrRequestInput – collapses the re-entry HITL pattern: +// pauses for input on the first pass and returns the human's reply on +// resume. Equivalent to yielding RequestInput then checking ctx for reply. +// +// - workflow.WithRunID – option for workflow.RunNode that supplies a +// stable custom identifier, equivalent to ctx.run_node(..., run_id=...). +// +// - workflowagent.New – creates an agent.Agent backed by a Workflow +// engine. Use workflow.Chain to build the edges slice. +package main + +import ( + "context" + "fmt" + "log" + "os" + + "google.golang.org/genai" + + "google.golang.org/adk/v2/agent" + "google.golang.org/adk/v2/agent/llmagent" + "google.golang.org/adk/v2/agent/workflowagent" + "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/session" + "google.golang.org/adk/v2/workflow" +) + +// --8<-- [start:get-started] +// helloNode is a simple FunctionNode that returns "Hello World". +// In Python this would be written as: +// +// @node(name="hello_node") +// def my_node(node_input: Any): +// return "Hello World" +// +// In Go, workflow.NewFunctionNode wraps the same logic with the +// required node interface, inferring input and output types from +// the generic parameters. +var helloNode = workflow.NewFunctionNode("hello_node", + func(_ agent.Context, _ string) (string, error) { + return "Hello World", nil + }, + workflow.NodeConfig{}, +) + +// myWorkflow is a dynamic orchestrator node. It calls workflow.RunNode +// to schedule helloNode as a child and returns its output. +// In Python this would be: +// +// @node(rerun_on_resume=True) +// async def my_workflow(ctx: Context, node_input: str) -> str: +// result = await ctx.run_node(my_node, node_input="hello") +// return result +// +// workflow.NewDynamicNode defaults RerunOnResume to &true, matching the +// Python @node(rerun_on_resume=True) behaviour. +var myWorkflow = workflow.NewDynamicNode[string, string]("my_workflow", + func(ctx agent.Context, _ string, _ func(*session.Event) error) (string, error) { + return workflow.RunNode[string](ctx, helloNode, "hello") + }, + workflow.NodeConfig{}, +) + +func runGetStarted() error { + ctx := context.Background() + + // workflowagent.New creates an agent.Agent backed by the workflow engine. + // workflow.Chain(workflow.Start, myWorkflow) produces the edges slice + // equivalent to Python's edges=[("START", my_workflow)]. + wa, err := workflowagent.New(workflowagent.Config{ + Name: "root_agent", + Description: "A minimal dynamic workflow.", + Edges: workflow.Chain(workflow.Start, myWorkflow), + }) + if err != nil { + return fmt.Errorf("workflowagent.New: %w", err) + } + + l := full.NewLauncher() + return l.Execute(ctx, &launcher.Config{ + AgentLoader: agent.NewSingleLoader(wa), + }, os.Args[1:]) +} + +// --8<-- [end:get-started] + +// --8<-- [start:building-blocks-nodes] +// myFunctionNode demonstrates the explicit NewFunctionNode constructor — +// equivalent to wrapping a function in a FunctionNode manually in Python: +// +// success_node = FunctionNode(my_function_node, name="hello", rerun_on_resume=True) +// +// Creating the node directly (rather than via @node) is useful when you +// need multiple nodes from the same function with different configurations, +// or when wrapping functions from an external library. +var myFunctionNode = workflow.NewFunctionNode("hello", + func(_ agent.Context, _ any) (string, error) { + return "Hello World", nil + }, + workflow.NodeConfig{}, +) + +// myFormattingNode is a second function node that the dynamic orchestrator +// calls in sequence, mirroring: +// +// result_formatted = await ctx.run_node(my_formatting_node, node_input=result) +var myFormattingNode = workflow.NewFunctionNode("format", + func(_ agent.Context, in string) (string, error) { + return fmt.Sprintf("[formatted] %s", in), nil + }, + workflow.NodeConfig{}, +) + +// --8<-- [end:building-blocks-nodes] + +// --8<-- [start:building-blocks-workflow] +// orchestratorWorkflow is a dynamic node that schedules two children in +// sequence via workflow.RunNode, equivalent to: +// +// @node(rerun_on_resume=True) +// async def my_workflow(ctx): +// result = await ctx.run_node(my_function_node, node_input="Hello") +// result_formatted = await ctx.run_node(my_formatting_node, node_input=result) +// return result_formatted +var orchestratorWorkflow = workflow.NewDynamicNode[string, string]("my_workflow", + func(ctx agent.Context, _ string, _ func(*session.Event) error) (string, error) { + result, err := workflow.RunNode[string](ctx, myFunctionNode, "Hello") + if err != nil { + return "", err + } + return workflow.RunNode[string](ctx, myFormattingNode, result) + }, + workflow.NodeConfig{}, +) + +// --8<-- [end:building-blocks-workflow] + +// --8<-- [start:data-handling] +// newDataHandlingWorkflow demonstrates how to pass data between a dynamic +// orchestrator and an LlmAgent-backed node. workflow.NewAgentNode wraps an +// agent.Agent so it can be invoked via workflow.RunNode. +// +// In Python this mirrors: +// +// city_report_agent = Agent(name="city_report_agent", ...) +// @node +// async def city_workflow(ctx: Context): +// city_time = await ctx.run_node(city_time_function, "Paris") +// report_text = await ctx.run_node(city_report_agent, city_time) +// return report_text +func newDataHandlingWorkflow(ctx context.Context) (agent.Agent, error) { + model, err := gemini.NewModel(ctx, "gemini-flash-latest", &genai.ClientConfig{}) + if err != nil { + return nil, fmt.Errorf("gemini.NewModel: %w", err) + } + + // cityTimeNode is a FunctionNode that returns a formatted city-time string. + cityTimeNode := workflow.NewFunctionNode("city_time_function", + func(_ agent.Context, city string) (string, error) { + return fmt.Sprintf("10:10 AM in %s", city), nil + }, + workflow.NodeConfig{}, + ) + + // cityReportAgent is an LlmAgent that receives the city-time string and + // produces a human-friendly report. + cityReportAgent, err := llmagent.New(llmagent.Config{ + Name: "city_report_agent", + Model: model, + Description: "Reports city time information.", + Instruction: "Output the data provided by the previous node in a friendly sentence.", + }) + if err != nil { + return nil, fmt.Errorf("llmagent.New (cityReport): %w", err) + } + + // workflow.NewAgentNode wraps cityReportAgent so it can be called from + // inside a dynamic node via workflow.RunNode. + cityReportNode, err := workflow.NewAgentNode(cityReportAgent, workflow.NodeConfig{}) + if err != nil { + return nil, fmt.Errorf("workflow.NewAgentNode: %w", err) + } + + cityWorkflow := workflow.NewDynamicNode[string, string]("city_workflow", + func(ctx agent.Context, _ string, _ func(*session.Event) error) (string, error) { + cityTime, err := workflow.RunNode[string](ctx, cityTimeNode, "Paris") + if err != nil { + return "", err + } + return workflow.RunNode[string](ctx, cityReportNode, cityTime) + }, + workflow.NodeConfig{}, + ) + + return workflowagent.New(workflowagent.Config{ + Name: "data_handling_workflow", + SubAgents: []agent.Agent{cityReportAgent}, + Edges: workflow.Chain(workflow.Start, cityWorkflow), + }) +} + +// --8<-- [end:data-handling] + +// --8<-- [start:loop-route] +// newLoopWorkflow demonstrates an iterative loop inside a dynamic node. +// The orchestrator body uses a plain Go for loop to keep calling the +// lintCheckNode until there are no findings — equivalent to Python's: +// +// @node +// async def code_workflow(ctx: Context, user_request: str): +// code = await ctx.run_node(coder_agent, user_request) +// check_resp = await ctx.run_node(compile_lint_check, code) +// while check_resp.findings: +// code = await ctx.run_node(fixer_agent, ...) +// check_resp = await ctx.run_node(compile_lint_check, code) +// return code +func newLoopWorkflow(ctx context.Context) (agent.Agent, error) { + model, err := gemini.NewModel(ctx, "gemini-flash-latest", &genai.ClientConfig{}) + if err != nil { + return nil, fmt.Errorf("gemini.NewModel: %w", err) + } + + coderAgent, err := llmagent.New(llmagent.Config{ + Name: "generator_agent", + Model: model, + Description: "Writes Go code for the user request.", + Instruction: "Write Go code for the user request. Output only the code.", + OutputKey: "generated_code", + }) + if err != nil { + return nil, fmt.Errorf("llmagent.New (coder): %w", err) + } + + coderNode, err := workflow.NewAgentNode(coderAgent, workflow.NodeConfig{}) + if err != nil { + return nil, fmt.Errorf("workflow.NewAgentNode (coder): %w", err) + } + + // lintCheckNode simulates a lint/compile check. It returns an empty + // string when there are no findings, signalling the loop to exit. + lintCheckNode := workflow.NewFunctionNode("lint_reviewer", + func(_ agent.Context, code string) (string, error) { + // Simulate a lint check: return findings or empty string when clean. + if len(code) < 50 { + return "Code is too short; add error handling.", nil + } + return "", nil // no findings — loop exits + }, + workflow.NodeConfig{}, + ) + + fixerAgent, err := llmagent.New(llmagent.Config{ + Name: "fixer_agent", + Model: model, + Description: "Refactors code based on lint findings.", + Instruction: "Refactor the provided code to address the review findings. Output only the improved code.", + }) + if err != nil { + return nil, fmt.Errorf("llmagent.New (fixer): %w", err) + } + + fixerNode, err := workflow.NewAgentNode(fixerAgent, workflow.NodeConfig{}) + if err != nil { + return nil, fmt.Errorf("workflow.NewAgentNode (fixer): %w", err) + } + + codeWorkflow := workflow.NewDynamicNode[string, string]("code_workflow", + func(ctx agent.Context, userRequest string, _ func(*session.Event) error) (string, error) { + code, err := workflow.RunNode[string](ctx, coderNode, userRequest) + if err != nil { + return "", err + } + + findings, err := workflow.RunNode[string](ctx, lintCheckNode, code) + if err != nil { + return "", err + } + + // Loop until the lint check reports no findings. + for findings != "" { + code, err = workflow.RunNode[string](ctx, fixerNode, code) + if err != nil { + return "", err + } + findings, err = workflow.RunNode[string](ctx, lintCheckNode, code) + if err != nil { + return "", err + } + } + return code, nil + }, + workflow.NodeConfig{}, + ) + + return workflowagent.New(workflowagent.Config{ + Name: "code_pipeline", + SubAgents: []agent.Agent{coderAgent, fixerAgent}, + Edges: workflow.Chain(workflow.Start, codeWorkflow), + }) +} + +// --8<-- [end:loop-route] + +// --8<-- [start:parallel-route] +// newParallelWorkflow demonstrates parallel execution using +// workflow.NewParallelWorker. The worker node runs a wrapped child node +// concurrently for each element in a list input, collecting results. +// +// This is the Go equivalent of using asyncio.gather in Python: +// +// @node(rerun_on_resume=True) +// async def parallel_supervisor(ctx, node_input, real_node): +// tasks = [ctx.run_node(real_node, item) for item in node_input] +// results = await asyncio.gather(*tasks) +// return results +func newParallelWorkflow() (agent.Agent, error) { + // workerNode processes a single item. NewParallelWorker will call it + // once per element of the list input, concurrently. + workerNode := workflow.NewFunctionNode("worker", + func(_ agent.Context, item string) (string, error) { + return fmt.Sprintf("processed: %s", item), nil + }, + workflow.NodeConfig{}, + ) + + // NewParallelWorker wraps workerNode so it runs concurrently for each + // element of a []string input. maxConcurrency=0 means unlimited. + parallelWorker, err := workflow.NewParallelWorker( + "parallel_supervisor", + workerNode, + 0, // maxConcurrency: 0 = unlimited + workflow.NodeConfig{}, + ) + if err != nil { + return nil, fmt.Errorf("workflow.NewParallelWorker: %w", err) + } + + return workflowagent.New(workflowagent.Config{ + Name: "parallel_workflow", + Description: "Runs a worker node in parallel for each item in the input list.", + Edges: workflow.Chain(workflow.Start, parallelWorker), + }) +} + +// --8<-- [end:parallel-route] + +// --8<-- [start:human-input] +// newHITLWorkflow demonstrates the re-entry HITL pattern using +// workflow.ResumeOrRequestInput. On the first pass the node emits a +// RequestInput event and returns ErrNodeInterrupted (pausing the workflow). +// After the human replies, the same node is re-run from the top +// (RerunOnResume=&true) and ResumeOrRequestInput returns the human's reply. +// +// In Python this is equivalent to: +// +// @node(rerun_on_resume=True) +// async def get_user_approval(ctx, node_input): +// yield RequestInput(message="Please approve this request (Yes/No)") +// +// @node(rerun_on_resume=True) +// async def handle_process(ctx, node_input): +// user_response = await ctx.run_node(get_user_approval) +// if user_response.lower() == "yes": +// return "Approved" +// return "Denied" +func newHITLWorkflow() (agent.Agent, error) { + rerun := true + + // approvalNode pauses on the first pass to ask the user for a Yes/No + // approval, then resolves their decision on resume. + // workflow.ResumeOrRequestInput handles both phases. + approvalNode := workflow.NewEmittingFunctionNode[any, any]("get_user_approval", + func(nc agent.Context, _ any, emit func(*session.Event) error) (any, error) { + // ResumeOrRequestInput: on first pass, emits the prompt and + // returns ErrNodeInterrupted. On re-run after the human replies, + // it returns the reply payload directly. + reply, err := workflow.ResumeOrRequestInput(nc, emit, session.RequestInput{ + InterruptID: "user_approval", + Message: "Please approve this request (Yes/No)", + }) + if err != nil { + return nil, err + } + + response, _ := reply.(string) + if response == "" { + response = "No" + } + if response == "yes" || response == "Yes" { + return "Approved", nil + } + return "Denied", nil + }, + workflow.NodeConfig{RerunOnResume: &rerun}, + ) + + return workflowagent.New(workflowagent.Config{ + Name: "hitl_workflow", + Description: "Pauses for user approval before completing a task.", + Edges: workflow.Chain(workflow.Start, approvalNode), + }) +} + +// --8<-- [end:human-input] + +// --8<-- [start:custom-execution-ids] +// newCustomIDWorkflow demonstrates supplying stable custom run IDs via +// workflow.WithRunID — equivalent to Python's: +// +// task = ctx.run_node(process_order, order, run_id=f"order-{order.order_id}") +// +// Custom run IDs must contain at least one non-numeric character to avoid +// collision with auto-generated sequential integer IDs. +func newCustomIDWorkflow() (agent.Agent, error) { + processOrderNode := workflow.NewFunctionNode("process_order", + func(_ agent.Context, orderID string) (string, error) { + return fmt.Sprintf("processed order %s", orderID), nil + }, + workflow.NodeConfig{}, + ) + + orders := []string{"ord-001", "ord-002", "ord-003"} + + processAllOrders := workflow.NewDynamicNode[any, []string]("process_all_orders", + func(ctx agent.Context, _ any, _ func(*session.Event) error) ([]string, error) { + results := make([]string, 0, len(orders)) + for _, orderID := range orders { + // WithRunID supplies a stable, deterministic identifier for + // each child invocation. IDs must contain at least one + // non-numeric character to avoid collision with the + // auto-generated sequential counter IDs. + result, err := workflow.RunNode[string]( + ctx, + processOrderNode, + orderID, + workflow.WithRunID(fmt.Sprintf("order-%s", orderID)), + ) + if err != nil { + return nil, fmt.Errorf("process order %s: %w", orderID, err) + } + results = append(results, result) + } + return results, nil + }, + workflow.NodeConfig{}, + ) + + return workflowagent.New(workflowagent.Config{ + Name: "custom_id_workflow", + Description: "Processes orders with stable per-order execution IDs.", + Edges: workflow.Chain(workflow.Start, processAllOrders), + }) +} + +// --8<-- [end:custom-execution-ids] + +func main() { + if err := runGetStarted(); err != nil { + log.Fatalf("runGetStarted: %v", err) + } +} diff --git a/examples/go/snippets/graphs/human-input/main.go b/examples/go/snippets/graphs/human-input/main.go new file mode 100644 index 0000000000..9bde3cff0b --- /dev/null +++ b/examples/go/snippets/graphs/human-input/main.go @@ -0,0 +1,336 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package main demonstrates Human-in-the-Loop (HITL) patterns in ADK Go v2. +// +// NOTE: This file requires google.golang.org/adk/v2, available in ADK Go +// v2.0.0 and later. +// +// # Graph HITL (primary pattern for /graphs/ pages) +// +// In ADK Go v2, the primary way to add a human input node to a graph-based +// workflow is workflow.NewEmittingFunctionNode with workflow.ResumeOrRequestInput. +// This is the direct Go equivalent of the Python RequestInput node: +// +// - On the first pass the node emits a session.RequestInput event +// (surfaced via Event.RequestedInput) and returns ErrNodeInterrupted, +// pausing the workflow. +// - The workflow resumes after the client sends a reply. The node is +// re-invoked from the top (RerunOnResume defaults to &true on dynamic +// nodes; set it explicitly on EmittingFunctionNode), and +// workflow.ResumeOrRequestInput returns the human's reply payload. +// +// # Tool-confirmation (secondary pattern, LLM-agent feature) +// +// Tool-confirmation (RequireConfirmation / ctx.RequestConfirmation) is a +// separate LLM-agent mechanism for yes/no approval prompts before a tool +// executes. It is not graph-node based. +package main + +import ( + "context" + "fmt" + "log" + + "google.golang.org/genai" + + "google.golang.org/adk/v2/agent" + "google.golang.org/adk/v2/agent/llmagent" + "google.golang.org/adk/v2/agent/workflowagent" + "google.golang.org/adk/v2/model/gemini" + "google.golang.org/adk/v2/session" + "google.golang.org/adk/v2/tool" + "google.golang.org/adk/v2/tool/functiontool" + "google.golang.org/adk/v2/workflow" +) + +const ( + appName = "hitl_demo" + userID = "demo_user" + modelName = "gemini-flash-latest" +) + +// --8<-- [start:graph-hitl-get-started] +// newGraphHITLWorkflow demonstrates a graph HITL node using +// workflow.NewEmittingFunctionNode and workflow.ResumeOrRequestInput. +// +// This is the Go equivalent of the Python RequestInput node: +// +// def step1(): # Human input step +// yield RequestInput(message="Enter a number:") +// +// def step2(node_input): +// return node_input * 2 +// +// root_agent = Workflow( +// name="root_agent", +// edges=[('START', step1, step2)], +// ) +// +// On the first pass, step1Node emits a RequestInput event and pauses the +// workflow (ErrNodeInterrupted). After the human replies, the node is re-run +// and ResumeOrRequestInput returns the reply, which flows as typed input to +// step2Node via event.Output. +func newGraphHITLWorkflow() (agent.Agent, error) { + rerun := true + + // step1Node: pauses for human input on the first pass, returns the + // human's reply on resume. workflow.ResumeOrRequestInput handles both + // phases — no manual re-entry bookkeeping needed. + step1Node := workflow.NewEmittingFunctionNode[any, string]("step1", + func(ctx agent.Context, _ any, emit func(*session.Event) error) (string, error) { + reply, err := workflow.ResumeOrRequestInput(ctx, emit, session.RequestInput{ + InterruptID: "enter_number", + Message: "Enter a number:", + }) + if err != nil { + // ErrNodeInterrupted on first pass — workflow pauses here. + return "", err + } + // On resume, reply is the human's text response. + number, _ := reply.(string) + return number, nil + }, + workflow.NodeConfig{RerunOnResume: &rerun}, + ) + + // step2Node: receives the human's input as its typed string input via + // event.Output and doubles the number. + step2Node := workflow.NewFunctionNode("step2", + func(_ agent.Context, input string) (string, error) { + return fmt.Sprintf("You entered: %s (doubled: %s%s)", input, input, input), nil + }, + workflow.NodeConfig{}, + ) + + return workflowagent.New(workflowagent.Config{ + Name: "root_agent", + Description: "Pauses for a number from the user, then doubles it.", + Edges: workflow.Chain(workflow.Start, step1Node, step2Node), + }) +} + +// --8<-- [end:graph-hitl-get-started] + +// --8<-- [start:graph-hitl-with-payload] +// ItineraryItem represents a single activity in a travel plan. +type ItineraryItem struct { + Name string `json:"name"` + Description string `json:"description"` +} + +// newItineraryReviewWorkflow demonstrates a graph HITL node that sends a +// structured payload alongside the input prompt so the client can render +// additional context for the user. This mirrors Python's: +// +// async def get_user_feedback(node_input: ActivitiesList): +// yield RequestInput( +// message="Which items appeal to you?", +// payload=node_input, +// response_schema=UserFeedback, +// ) +func newItineraryReviewWorkflow() (agent.Agent, error) { + rerun := true + + // buildItineraryNode: generates an itinerary and passes it to the HITL + // node as its typed output via event.Output. + buildItineraryNode := workflow.NewFunctionNode("build_itinerary", + func(_ agent.Context, _ any) ([]ItineraryItem, error) { + return []ItineraryItem{ + {Name: "Eiffel Tower", Description: "Iconic iron lattice tower."}, + {Name: "Louvre Museum", Description: "World's largest art museum."}, + {Name: "Seine River Cruise", Description: "Scenic boat tour of Paris."}, + }, nil + }, + workflow.NodeConfig{}, + ) + + // reviewNode: sends the itinerary as payload alongside the prompt so the + // client can display it. On resume, the human's selection is returned. + reviewNode := workflow.NewEmittingFunctionNode[[]ItineraryItem, string]("get_user_feedback", + func(ctx agent.Context, itinerary []ItineraryItem, emit func(*session.Event) error) (string, error) { + reply, err := workflow.ResumeOrRequestInput(ctx, emit, session.RequestInput{ + InterruptID: "itinerary_review", + Message: fmt.Sprintf("Here is your recommended itinerary (%d activities). Which items appeal to you?", len(itinerary)), + Payload: itinerary, // structured payload rendered by the client + }) + if err != nil { + // ErrNodeInterrupted on first pass — workflow pauses here. + return "", err + } + feedback, _ := reply.(string) + return feedback, nil + }, + workflow.NodeConfig{RerunOnResume: &rerun}, + ) + + // finalNode: receives the user's feedback and produces a confirmation. + finalNode := workflow.NewFunctionNode("finalize", + func(_ agent.Context, feedback string) (string, error) { + return fmt.Sprintf("Itinerary finalised with your feedback: %q", feedback), nil + }, + workflow.NodeConfig{}, + ) + + return workflowagent.New(workflowagent.Config{ + Name: "concierge_workflow", + Description: "Builds an itinerary, asks the user for feedback, then finalises.", + Edges: workflow.Chain(workflow.Start, buildItineraryNode, reviewNode, finalNode), + }) +} + +// --8<-- [end:graph-hitl-with-payload] + +// --8<-- [start:simple-hitl] +// DoubleNumberArgs holds the input for the doubleNumber tool. +type DoubleNumberArgs struct { + Number int `json:"number" jsonschema:"description=The number to double."` +} + +// DoubleNumberResults holds the output of the doubleNumber tool. +type DoubleNumberResults struct { + Result int `json:"result"` +} + +// doubleNumber is a tool that doubles the given number. +// Because RequireConfirmation is true, the framework automatically pauses +// execution and emits an "adk_request_confirmation" event to the client before +// running the tool. The client must reply with a FunctionResponse confirming +// or denying the action. +func doubleNumber(_ agent.Context, args DoubleNumberArgs) (DoubleNumberResults, error) { + return DoubleNumberResults{Result: args.Number * 2}, nil +} + +// newSimpleHITLAgent creates an LLM agent with a tool that always requires +// user confirmation before it executes (tool-confirmation pattern). +func newSimpleHITLAgent(ctx context.Context) (agent.Agent, error) { + model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + return nil, fmt.Errorf("failed to create model: %w", err) + } + + doubleNumberTool, err := functiontool.New( + functiontool.Config{ + Name: "double_number", + Description: "Doubles the given number. Requires user approval before running.", + RequireConfirmation: true, + }, + doubleNumber, + ) + if err != nil { + return nil, fmt.Errorf("failed to create tool: %w", err) + } + + return llmagent.New(llmagent.Config{ + Name: "double_number_agent", + Model: model, + Instruction: "You are a helpful assistant. When asked to double a number, use the double_number tool.", + Tools: []tool.Tool{doubleNumberTool}, + }) +} + +// --8<-- [end:simple-hitl] + +// --8<-- [start:hitl-with-hint] +// BookFlightArgs holds the input for the bookFlight tool. +type BookFlightArgs struct { + Origin string `json:"origin" jsonschema:"description=Departure airport code."` + Destination string `json:"destination" jsonschema:"description=Arrival airport code."` + Date string `json:"date" jsonschema:"description=Travel date in YYYY-MM-DD format."` +} + +// BookFlightResults holds the outcome of the bookFlight tool. +type BookFlightResults struct { + Status string `json:"status"` + ConfirmNumber string `json:"confirm_number,omitempty"` +} + +// bookFlight is a tool that pauses for human approval before completing a +// booking (tool-confirmation pattern with a custom hint message). +func bookFlight(ctx agent.Context, args BookFlightArgs) (BookFlightResults, error) { + if confirmation := ctx.ToolConfirmation(); confirmation != nil { + if !confirmation.Confirmed { + return BookFlightResults{Status: "Booking cancelled by user."}, nil + } + return BookFlightResults{ + Status: "Booking confirmed.", + ConfirmNumber: "FLT-20251031", + }, nil + } + + hint := fmt.Sprintf( + "The agent wants to book a flight from %s to %s on %s. Do you approve?", + args.Origin, args.Destination, args.Date, + ) + if err := ctx.RequestConfirmation(hint, nil); err != nil { + return BookFlightResults{}, fmt.Errorf("failed to request confirmation: %w", err) + } + return BookFlightResults{Status: "Awaiting user approval."}, nil +} + +// newHITLWithHintAgent creates an LLM agent whose bookFlight tool manually +// requests confirmation with a descriptive hint (tool-confirmation pattern). +func newHITLWithHintAgent(ctx context.Context) (agent.Agent, error) { + model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + return nil, fmt.Errorf("failed to create model: %w", err) + } + + bookFlightTool, err := functiontool.New( + functiontool.Config{ + Name: "book_flight", + Description: "Books a flight between two airports on a given date.", + }, + bookFlight, + ) + if err != nil { + return nil, fmt.Errorf("failed to create tool: %w", err) + } + + return llmagent.New(llmagent.Config{ + Name: "flight_booking_agent", + Model: model, + Instruction: "You are a flight booking assistant. Help the user book flights.", + Tools: []tool.Tool{bookFlightTool}, + }) +} + +// --8<-- [end:hitl-with-hint] + +func main() { + graphAgent, err := newGraphHITLWorkflow() + if err != nil { + log.Fatalf("Failed to create graph HITL workflow: %v", err) + } + log.Printf("Created graph HITL workflow: %s", graphAgent.Name()) + + itineraryAgent, err := newItineraryReviewWorkflow() + if err != nil { + log.Fatalf("Failed to create itinerary review workflow: %v", err) + } + log.Printf("Created itinerary review workflow: %s", itineraryAgent.Name()) + + ctx := context.Background() + simpleAgent, err := newSimpleHITLAgent(ctx) + if err != nil { + log.Fatalf("Failed to create simple HITL agent: %v", err) + } + log.Printf("Created simple HITL agent: %s", simpleAgent.Name()) + + hintAgent, err := newHITLWithHintAgent(ctx) + if err != nil { + log.Fatalf("Failed to create hint HITL agent: %v", err) + } + log.Printf("Created hint HITL agent: %s", hintAgent.Name()) +} diff --git a/examples/go/snippets/graphs/index/main.go b/examples/go/snippets/graphs/index/main.go new file mode 100644 index 0000000000..06f03ee5d7 --- /dev/null +++ b/examples/go/snippets/graphs/index/main.go @@ -0,0 +1,200 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package main provides snippet examples for graph-based workflow agents in ADK Go v2. +// +// NOTE: This file requires google.golang.org/adk (the workflow package), +// available in ADK Go v2.0.0 and later. +// +// Both snippets use the v2 graph engine (workflow.NewFunctionNode + +// workflowagent.New) rather than the prebuilt workflow agents from v1.x. +// This mirrors the Python Workflow(edges=[...]) API directly: +// +// - workflow.Chain(workflow.Start, nodeA, nodeB) — sequential edges +// - workflow.NewEmittingFunctionNode + ev.Routes + []workflow.Edge — routing +// - workflow.StringRoute("category") — conditional edge matcher +package main + +import ( + "fmt" + "log" + "strings" + + "google.golang.org/adk/v2/agent" + "google.golang.org/adk/v2/agent/workflowagent" + "google.golang.org/adk/v2/session" + "google.golang.org/adk/v2/workflow" +) + +// --8<-- [start:sequential-get-started] +// cityTime holds the data passed from the lookup step to the report step. +type cityTime struct { + City string + TimeInfo string +} + +// newSequentialGetStarted builds a three-node sequential workflow using the +// v2 graph engine. Each node is a workflow.NewFunctionNode whose return value +// is automatically wrapped in session.Event.Output and forwarded to the next +// node as its typed input. +// +// This is the Go equivalent of the Python Workflow example: +// +// root_agent = Workflow( +// name="root_agent", +// edges=[("START", city_generator_agent, lookup_time_function, +// city_report_agent, completed_message_function)], +// ) +func newSequentialGetStarted() (agent.Agent, error) { + // Step 1: return a city name. The string is set as event.Output and + // becomes the typed input of the next node. + cityGeneratorNode := workflow.NewFunctionNode("city_generator_agent", + func(_ agent.Context, _ any) (string, error) { + return "Tokyo", nil + }, + workflow.NodeConfig{}, + ) + + // Step 2: receive the city name and return structured time data. + lookupTimeNode := workflow.NewFunctionNode("lookup_time_function", + func(_ agent.Context, city string) (cityTime, error) { + return cityTime{City: city, TimeInfo: "10:10 AM"}, nil + }, + workflow.NodeConfig{}, + ) + + // Step 3: receive the cityTime struct and produce the final report string. + cityReportNode := workflow.NewFunctionNode("city_report_agent", + func(_ agent.Context, ct cityTime) (string, error) { + return fmt.Sprintf("It is %s in %s right now.\nWORKFLOW COMPLETED.", + ct.TimeInfo, ct.City), nil + }, + workflow.NodeConfig{}, + ) + + // workflow.Chain wires START → cityGeneratorNode → lookupTimeNode → cityReportNode. + // Data flows through event.Output: no session state writes needed. + return workflowagent.New(workflowagent.Config{ + Name: "root_agent", + Description: "Sequential workflow: generate city → look up time → report.", + Edges: workflow.Chain(workflow.Start, cityGeneratorNode, lookupTimeNode, cityReportNode), + }) +} + +// --8<-- [end:sequential-get-started] + +// --8<-- [start:process-pipeline] +// classifyMessage is the router node. It emits ev.Routes to select which +// branch to follow — the Go equivalent of Python's: +// +// def router(node_input: str): +// return Event(route=["BUG"]) +func classifyMessage(ctx agent.Context, msg string, emit func(*session.Event) error) (any, error) { + // In a real workflow this step calls an LLM; here we classify by keyword. + category := "LOGISTICS" + lower := strings.ToLower(msg) + switch { + case strings.Contains(lower, "bug") || strings.Contains(lower, "error"): + category = "BUG" + case strings.Contains(lower, "help") || strings.Contains(lower, "support"): + category = "CUSTOMER_SUPPORT" + } + + ev := session.NewEvent(ctx, ctx.InvocationID()) + ev.Routes = []string{category} // drives edge dispatch + ev.Output = msg // forward original message to the chosen handler + if err := emit(ev); err != nil { + return nil, err + } + return nil, nil // nil suppresses the automatic terminal event +} + +// newProcessPipeline builds a classification + conditional-routing workflow +// using the v2 graph engine. The classifyMessage emitting node sets +// ev.Routes, and the graph engine dispatches to the matching handler via +// workflow.StringRoute. +// +// This is the Go equivalent of the Python Workflow example: +// +// root_agent = Workflow( +// name="routing_workflow", +// edges=[ +// ("START", process_message, router), +// (router, { +// "BUG": response_1_bug, +// "CUSTOMER_SUPPORT": response_2_support, +// "LOGISTICS": response_3_logistics, +// }), +// ], +// ) +func newProcessPipeline() (agent.Agent, error) { + classifyNode := workflow.NewEmittingFunctionNode( + "process_message", classifyMessage, workflow.NodeConfig{}, + ) + + bugNode := workflow.NewFunctionNode("response_1_bug", + func(_ agent.Context, _ any) (string, error) { + return "Handling bug...", nil + }, + workflow.NodeConfig{}, + ) + + supportNode := workflow.NewFunctionNode("response_2_support", + func(_ agent.Context, _ any) (string, error) { + return "Handling customer support...", nil + }, + workflow.NodeConfig{}, + ) + + logisticsNode := workflow.NewFunctionNode("response_3_logistics", + func(_ agent.Context, _ any) (string, error) { + return "Handling logistics...", nil + }, + workflow.NodeConfig{}, + ) + + // workflow.Concat merges the sequential chain with the conditional edges. + // Each workflow.Edge carries a workflow.StringRoute matcher that the engine + // checks against ev.Routes emitted by classifyNode. + edges := workflow.Concat( + workflow.Chain(workflow.Start, classifyNode), + []workflow.Edge{ + {From: classifyNode, To: bugNode, Route: workflow.StringRoute("BUG")}, + {From: classifyNode, To: supportNode, Route: workflow.StringRoute("CUSTOMER_SUPPORT")}, + {From: classifyNode, To: logisticsNode, Route: workflow.StringRoute("LOGISTICS")}, + }, + ) + + return workflowagent.New(workflowagent.Config{ + Name: "routing_workflow", + Description: "Classifies a message and routes it to the appropriate handler.", + Edges: edges, + }) +} + +// --8<-- [end:process-pipeline] + +func main() { + seqAgent, err := newSequentialGetStarted() + if err != nil { + log.Fatalf("Failed to create sequential agent: %v", err) + } + log.Printf("Created sequential workflow agent: %s", seqAgent.Name()) + + pipelineAgent, err := newProcessPipeline() + if err != nil { + log.Fatalf("Failed to create process pipeline: %v", err) + } + log.Printf("Created process pipeline agent: %s", pipelineAgent.Name()) +} diff --git a/examples/go/snippets/graphs/routes/main.go b/examples/go/snippets/graphs/routes/main.go new file mode 100644 index 0000000000..96e239ec9b --- /dev/null +++ b/examples/go/snippets/graphs/routes/main.go @@ -0,0 +1,399 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package main demonstrates graph routing patterns in ADK Go v2 using the +// graph engine: workflow.NewFunctionNode, workflow.NewAgentNode, workflow.Chain, +// workflow.Concat, workflow.NewEdgeBuilder, workflow.NewJoinNode, and +// workflowagent.New. +// +// NOTE: This file requires google.golang.org/adk (the workflow package), +// available in ADK Go v2.0.0 and later. +// +// This file contains five snippet regions used in docs/graphs/routes.md: +// +// function-node – workflow.NewFunctionNode as a graph node +// sequential-nodes – sequential route using workflow.Chain +// parallel-fan-out – fan-out/join using workflow.NewJoinNode + EdgeBuilder +// nested-workflows – inner workflowagent wrapped as workflow.NewAgentNode +// loop-escalate – back-edge loop using workflow.EdgeBuilder.AddRoute +package main + +import ( + "fmt" + "log" + "strings" + + "google.golang.org/adk/v2/agent" + "google.golang.org/adk/v2/agent/workflowagent" + "google.golang.org/adk/v2/session" + "google.golang.org/adk/v2/workflow" +) + +// --8<-- [start:function-node] +// newFunctionNodePipeline demonstrates workflow.NewFunctionNode as the primary +// v2 node type. A FunctionNode wraps a plain Go function: the function returns +// a typed value, and the framework automatically wraps it in a session.Event, +// setting event.Output. The successor node receives this value as its typed +// input parameter. +// +// This is the direct Go equivalent of the Python FunctionNode: +// +// def my_function_node(node_input: str): +// return Event(output=node_input.upper()) +func newFunctionNodePipeline() (agent.Agent, error) { + upperFn := func(_ agent.Context, input string) (string, error) { + return strings.ToUpper(input), nil + } + + suffixFn := func(_ agent.Context, input string) (string, error) { + return input + " IS AWESOME!", nil + } + + // workflow.NewFunctionNode wraps each function as a graph node. + // workflow.Chain wires them in order: START → upper → suffix. + // The output of upperFn is delivered as the typed input of suffixFn + // via event.Output — no session state writes are needed. + nodeA := workflow.NewFunctionNode("upper", upperFn, workflow.NodeConfig{}) + nodeB := workflow.NewFunctionNode("suffix", suffixFn, workflow.NodeConfig{}) + + return workflowagent.New(workflowagent.Config{ + Name: "function_node_pipeline", + Description: "Demonstrates workflow.NewFunctionNode data flow via Event.Output.", + Edges: workflow.Chain(workflow.Start, nodeA, nodeB), + }) +} + +// --8<-- [end:function-node] + +// --8<-- [start:sequential-nodes] +// newSequentialNodes builds a two-step sequential workflow using the v2 graph +// engine. workflow.Chain wires the nodes in order; each node's typed return +// value is forwarded to the next node via event.Output. +// +// This is the Go equivalent of: +// +// edges=[("START", task_A_node, task_B_node)] +func newSequentialNodes() (agent.Agent, error) { + // task_A_node: transforms the user's input. + taskANode := workflow.NewFunctionNode("task_A_node", + func(_ agent.Context, input string) (string, error) { + return "Summary: " + strings.TrimSpace(input), nil + }, + workflow.NodeConfig{}, + ) + + // task_B_node: receives task A's output as its typed input and produces + // the final result. No session state reads needed. + taskBNode := workflow.NewFunctionNode("task_B_node", + func(_ agent.Context, summary string) (string, error) { + return strings.ToUpper(summary), nil + }, + workflow.NodeConfig{}, + ) + + return workflowagent.New(workflowagent.Config{ + Name: "sequential_workflow", + Description: "Runs task A then task B in order via workflow.Chain.", + Edges: workflow.Chain(workflow.Start, taskANode, taskBNode), + }) +} + +// --8<-- [end:sequential-nodes] + +// --8<-- [start:parallel-fan-out] +// newParallelFanOut builds a fan-out / join workflow using the v2 graph engine. +// Three research nodes run in parallel from Start; workflow.NewJoinNode waits +// for all of them to complete and emits a map[nodeName]output to the format +// node, which assembles the results for a synthesis node. +// +// Graph topology: +// +// START ─┬─> research_A ──┐ +// ├─> research_B ──┼─> gather (JoinNode) ─> format ─> synthesis +// └─> research_C ──┘ +// +// Python equivalent: +// +// edges=[ +// ("START", research_A, my_join_node), +// ("START", research_B, my_join_node), +// ("START", research_C, my_join_node), +// (my_join_node, format_node), +// (format_node, synthesis_node), +// ] +func newParallelFanOut() (agent.Agent, error) { + researchA := workflow.NewFunctionNode("research_A", + func(_ agent.Context, _ any) (string, error) { + return "Fact about renewable energy.", nil + }, + workflow.NodeConfig{}, + ) + researchB := workflow.NewFunctionNode("research_B", + func(_ agent.Context, _ any) (string, error) { + return "Fact about electric vehicles.", nil + }, + workflow.NodeConfig{}, + ) + researchC := workflow.NewFunctionNode("research_C", + func(_ agent.Context, _ any) (string, error) { + return "Fact about carbon capture.", nil + }, + workflow.NodeConfig{}, + ) + + // workflow.NewJoinNode waits for all predecessors (research_A, research_B, + // research_C) to complete and emits a map[nodeName]output to its successor. + gatherNode := workflow.NewJoinNode("gather") + + // formatNode receives map[string]any from gatherNode and assembles a + // combined prompt string. + formatNode := workflow.NewFunctionNode("format", + func(_ agent.Context, results map[string]any) (string, error) { + return fmt.Sprintf("A: %v\nB: %v\nC: %v", + results["research_A"], + results["research_B"], + results["research_C"], + ), nil + }, + workflow.NodeConfig{}, + ) + + synthesisNode := workflow.NewFunctionNode("synthesis", + func(_ agent.Context, prompt string) (string, error) { + return "Combined report: " + prompt, nil + }, + workflow.NodeConfig{}, + ) + + // EdgeBuilder.AddFanOut fans workflow.Start out to all three research nodes. + // EdgeBuilder.AddFanIn routes all three research nodes into gatherNode. + eb := workflow.NewEdgeBuilder() + eb.AddFanOut(workflow.Start, researchA, researchB, researchC) + eb.AddFanIn(gatherNode, researchA, researchB, researchC) + eb.Add(gatherNode, formatNode) + eb.Add(formatNode, synthesisNode) + + return workflowagent.New(workflowagent.Config{ + Name: "fan_out_workflow", + Description: "Parallel research fan-out with JoinNode barrier and synthesis.", + Edges: eb.Build(), + }) +} + +// --8<-- [end:parallel-fan-out] + +// --8<-- [start:nested-workflows] +// newNestedWorkflows shows how to nest one workflowagent inside another using +// the v2 graph engine. The inner workflowagent is wrapped with +// workflow.NewAgentNode and placed as a node in the outer graph's edge slice. +// From the outer graph's perspective the inner workflow is a single node that +// runs to completion before the edge to finalNode is followed. +// +// Python equivalent: +// +// root_agent = Workflow( +// name="parent_workflow", +// edges=[("START", task_A1, workflow_B, final_node)], +// ) +func newNestedWorkflows() (agent.Agent, error) { + // --- Inner workflow B --- + innerStep1 := workflow.NewFunctionNode("inner_step_1", + func(_ agent.Context, input string) (string, error) { + return "[ES] " + input, nil // simulate translation to Spanish + }, + workflow.NodeConfig{}, + ) + innerStep2 := workflow.NewFunctionNode("inner_step_2", + func(_ agent.Context, spanish string) (string, error) { + return "[EN] " + spanish, nil // simulate translation back to English + }, + workflow.NodeConfig{}, + ) + + // workflowB is a self-contained inner graph. + workflowB, err := workflowagent.New(workflowagent.Config{ + Name: "workflow_B", + Description: "Translates input to Spanish then back to English.", + Edges: workflow.Chain(workflow.Start, innerStep1, innerStep2), + }) + if err != nil { + return nil, fmt.Errorf("workflowB: %w", err) + } + + // --- Outer graph --- + taskA1 := workflow.NewFunctionNode("task_A1", + func(_ agent.Context, input string) (string, error) { + return "Summary: " + strings.TrimSpace(input), nil + }, + workflow.NodeConfig{}, + ) + + finalNode := workflow.NewFunctionNode("final_node", + func(_ agent.Context, result string) (string, error) { + return "Final: " + result, nil + }, + workflow.NodeConfig{}, + ) + + // workflow.NewAgentNode wraps workflowB so it can be placed as a node + // in the outer graph's edges slice. + innerNode, err := workflow.NewAgentNode(workflowB, workflow.NodeConfig{}) + if err != nil { + return nil, fmt.Errorf("NewAgentNode(workflowB): %w", err) + } + + return workflowagent.New(workflowagent.Config{ + Name: "parent_workflow", + Description: "Runs task_A1 then the nested workflow_B then final_node.", + Edges: workflow.Chain(workflow.Start, taskA1, innerNode, finalNode), + SubAgents: []agent.Agent{workflowB}, + }) +} + +// --8<-- [end:nested-workflows] + +// --8<-- [start:loop-escalate] +// draft carries the working document through the refinement loop. +type draft struct { + Text string `json:"text"` +} + +// criticResult is emitted by the critic node with the review verdict and +// optional suggestions. The router reads Verdict to set Event.Routes. +type criticResult struct { + Verdict string `json:"verdict"` // "REFINE" or "DONE" + Suggestions string `json:"suggestions"` // non-empty when Verdict == "REFINE" +} + +// writeDraft is the initial writer node: produces the first draft from the +// user's topic. Its typed return value becomes the input to the critic node +// via Event.Output — no session state writes needed. +func writeDraft(_ agent.Context, topic string) (draft, error) { + // In a real workflow this would call an LLM; here we return a stub. + return draft{Text: "Draft about " + topic + ": placeholder content."}, nil +} + +// reviewDraft is the critic node: inspects the draft and returns a verdict. +// "DONE" exits the loop; "REFINE" triggers a back-edge to the refiner. +func reviewDraft(_ agent.Context, d draft) (criticResult, error) { + // Simulate a critic: approve once the draft contains "improved". + if strings.Contains(d.Text, "improved") { + return criticResult{Verdict: "DONE"}, nil + } + return criticResult{ + Verdict: "REFINE", + Suggestions: "Add more detail and mark the text as improved.", + }, nil +} + +// routeVerdict reads the critic's verdict and sets Event.Routes so the +// graph engine dispatches to either the refiner or the done node. +// Returning nil suppresses the automatic terminal event. +func routeVerdict(ctx agent.Context, r criticResult, emit func(*session.Event) error) (any, error) { + ev := session.NewEvent(ctx, ctx.InvocationID()) + ev.Routes = []string{r.Verdict} + ev.Output = r // forward the full result to the chosen successor + if err := emit(ev); err != nil { + return nil, err + } + return nil, nil +} + +// refineDraft applies the critic's suggestions and returns the improved draft. +// Its output feeds back to the critic node via the back-edge. +func refineDraft(_ agent.Context, r criticResult) (draft, error) { + return draft{Text: "improved draft incorporating: " + r.Suggestions}, nil +} + +// reportDone is the terminal node, reached only when the critic is satisfied. +func reportDone(_ agent.Context, r criticResult) (string, error) { + return "Refinement complete. Final verdict: " + r.Verdict, nil +} + +// newLoopEscalate builds an iterative document-refinement workflow using the +// graph engine. The critic node emits a route ("REFINE" or "DONE") and the +// engine dispatches to either the refiner (which loops back to the critic via +// a back-edge) or the terminal done node. +// +// Graph topology: +// +// START → writer → critic → router ─┬─ "REFINE" → refiner ──┐ +// └─ "DONE" → done │ +// ▲_______________________________┘ (back-edge) +// +// Python equivalent: +// +// edges=[ +// ("START", writer_node, critic_node, router), +// (router, {"REFINE": refiner_node, "DONE": done_node}), +// (refiner_node, critic_node), # back-edge creates the loop +// ] +func newLoopEscalate() (agent.Agent, error) { + writerNode := workflow.NewFunctionNode("writer", writeDraft, workflow.NodeConfig{}) + criticNode := workflow.NewFunctionNode("critic", reviewDraft, workflow.NodeConfig{}) + routerNode := workflow.NewEmittingFunctionNode("router", routeVerdict, workflow.NodeConfig{}) + refinerNode := workflow.NewFunctionNode("refiner", refineDraft, workflow.NodeConfig{}) + doneNode := workflow.NewFunctionNode("done", reportDone, workflow.NodeConfig{}) + + // Build the edges. The back-edge from refinerNode to criticNode creates + // the loop; the graph engine re-activates criticNode with a fresh + // lifecycle on each iteration. + eb := workflow.NewEdgeBuilder() + eb.Add(workflow.Start, writerNode) + eb.Add(writerNode, criticNode) + eb.Add(criticNode, routerNode) + eb.AddRoute(routerNode, refinerNode, workflow.StringRoute("REFINE")) + eb.AddRoute(routerNode, doneNode, workflow.StringRoute("DONE")) + eb.Add(refinerNode, criticNode) // back-edge: loop back for another review + + return workflowagent.New(workflowagent.Config{ + Name: "iterative_writer", + Description: "Writes then iteratively refines a document using a critic/refiner loop.", + Edges: eb.Build(), + }) +} + +// --8<-- [end:loop-escalate] + +func main() { + fnPipeline, err := newFunctionNodePipeline() + if err != nil { + log.Fatalf("newFunctionNodePipeline: %v", err) + } + log.Printf("created %s", fnPipeline.Name()) + + seqAgent, err := newSequentialNodes() + if err != nil { + log.Fatalf("newSequentialNodes: %v", err) + } + log.Printf("created %s", seqAgent.Name()) + + parallelAgent, err := newParallelFanOut() + if err != nil { + log.Fatalf("newParallelFanOut: %v", err) + } + log.Printf("created %s", parallelAgent.Name()) + + nestedAgent, err := newNestedWorkflows() + if err != nil { + log.Fatalf("newNestedWorkflows: %v", err) + } + log.Printf("created %s", nestedAgent.Name()) + + loopAgent, err := newLoopEscalate() + if err != nil { + log.Fatalf("newLoopEscalate: %v", err) + } + log.Printf("created %s", loopAgent.Name()) +} diff --git a/examples/go/snippets/workflows/collaboration/main.go b/examples/go/snippets/workflows/collaboration/main.go new file mode 100644 index 0000000000..6bb7c9a663 --- /dev/null +++ b/examples/go/snippets/workflows/collaboration/main.go @@ -0,0 +1,156 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package main demonstrates collaborative agent team patterns in ADK Go v2. +// +// NOTE: This file requires google.golang.org/adk/v2, available in ADK Go +// v2.0.0 and later. It carries //go:build ignore so it is excluded from the +// current examples/go module (which is still on the v1 path) until examples/go +// is migrated to google.golang.org/adk at the v2.0.0 release. +// +// # Agent collaboration modes in ADK Go v2 +// +// The Mode field on llmagent.Config controls how a subagent behaves when +// invoked by a coordinator agent. Three modes are available: +// +// - "chat" (ModeChat, default): full user interaction; agent controls +// flow until it explicitly calls transfer_to_agent. +// - "task" (ModeTask): agent may ask the user clarifying questions and +// automatically returns control to the parent when it calls complete_task. +// - "single_turn" (ModeSingleTurn): no user interaction; executes one turn +// and returns automatically; can run in parallel with peer agents. +// +// When a coordinator llmagent declares SubAgents, ADK automatically generates +// request_task_ tools for each subagent, wiring the delegation pattern. +// +// When an llmagent is used as a node in the v2 workflow graph engine +// (workflow.NewAgentNode), the engine automatically applies ModeSingleTurn +// if no mode is configured on the agent. +package main + +import ( + "context" + "log" + + "google.golang.org/genai" + + "google.golang.org/adk/v2/agent" + "google.golang.org/adk/v2/agent/llmagent" + "google.golang.org/adk/v2/model/gemini" + "google.golang.org/adk/v2/tool" + "google.golang.org/adk/v2/tool/functiontool" +) + +// --8<-- [start:get-started] +// Stub tool functions — in a real agent these call external services. +func getWeather(_ agent.Context, _ struct{ City string }) (string, error) { + return "Sunny, 22°C", nil +} + +func searchFlights(_ agent.Context, _ struct{ Origin, Destination string }) (string, error) { + return "3 flights found", nil +} + +func bookFlight(_ agent.Context, _ struct{ FlightID string }) (string, error) { + return "Flight booked", nil +} + +// newCollaborativeTeam builds a coordinator agent with two subagents, each +// configured with a different collaboration mode. This is the Go equivalent of: +// +// weather_agent = Agent(name="weather_checker", mode="single_turn", ...) +// flight_agent = Agent(name="flight_booker", mode="task", ...) +// root = Agent(name="travel_planner", sub_agents=[weather_agent, flight_agent]) +func newCollaborativeTeam(ctx context.Context) (agent.Agent, error) { + model, err := gemini.NewModel(ctx, "gemini-flash-latest", &genai.ClientConfig{}) + if err != nil { + return nil, err + } + + getWeatherTool, err := functiontool.New(functiontool.Config{ + Name: "get_weather", + Description: "Returns the current weather for a city.", + }, getWeather) + if err != nil { + return nil, err + } + + searchFlightsTool, err := functiontool.New(functiontool.Config{ + Name: "search_flights", + Description: "Searches for available flights between two airports.", + }, searchFlights) + if err != nil { + return nil, err + } + + bookFlightTool, err := functiontool.New(functiontool.Config{ + Name: "book_flight", + Description: "Books a specific flight by ID.", + }, bookFlight) + if err != nil { + return nil, err + } + + // weatherAgent runs in ModeSingleTurn: no user interaction, executes one + // turn and returns automatically. Equivalent to mode="single_turn" in Python. + weatherAgent, err := llmagent.New(llmagent.Config{ + Name: "weather_checker", + Model: model, + Mode: llmagent.ModeSingleTurn, + Description: "Checks the current weather for a given city.", + Instruction: "Use the get_weather tool to look up the current weather.", + Tools: []tool.Tool{getWeatherTool}, + }) + if err != nil { + return nil, err + } + + // flightAgent runs in ModeTask: may ask the user clarifying questions and + // automatically returns control to the coordinator when done. Equivalent to + // mode="task" in Python. + flightAgent, err := llmagent.New(llmagent.Config{ + Name: "flight_booker", + Model: model, + Mode: llmagent.ModeTask, + Description: "Searches for and books flights.", + Instruction: "Help the user find and book a flight using the available tools.", + Tools: []tool.Tool{searchFlightsTool, bookFlightTool}, + }) + if err != nil { + return nil, err + } + + // The coordinator agent declares SubAgents. ADK automatically generates + // request_task_weather_checker and request_task_flight_booker tools so the + // coordinator can delegate work to each subagent. + return llmagent.New(llmagent.Config{ + Name: "travel_planner", + Model: model, + Description: "Coordinator agent that delegates to weather and flight subagents.", + Instruction: "Help the user plan their trip. Use the weather checker and flight booker as needed.", + SubAgents: []agent.Agent{weatherAgent, flightAgent}, + }) +} + +// --8<-- [end:get-started] + +func main() { + ctx := context.Background() + + rootAgent, err := newCollaborativeTeam(ctx) + if err != nil { + log.Fatalf("newCollaborativeTeam: %v", err) + } + log.Printf("created coordinator agent: %s", rootAgent.Name()) +} diff --git a/overrides/main.html b/overrides/main.html index 0fa0b27727..2d78c6fc13 100644 --- a/overrides/main.html +++ b/overrides/main.html @@ -49,10 +49,8 @@ {% block announce %} - ADK Python 2.0 GA + ADK Go 2.0 GA - is LIVE with graph workflows and collaborative agents, and check out - ADK Kotlin! + is LIVE with graph workflows and collaborative agents! Get started. {% endblock %} diff --git a/tools/go-snippets/files_to_test.txt b/tools/go-snippets/files_to_test.txt index 65c901179c..1509350390 100644 --- a/tools/go-snippets/files_to_test.txt +++ b/tools/go-snippets/files_to_test.txt @@ -46,3 +46,9 @@ snippets/sessions/memory_example/memory_example.go snippets/tools-custom/customer_support_agent/main.go snippets/get-started/multi_tool_agent/main.go snippets/runtime/triggers/event_processing_agent.go +snippets/graphs/human-input/main.go +snippets/graphs/index/main.go +snippets/graphs/routes/main.go +snippets/graphs/data-handling/main.go +snippets/graphs/dynamic/main.go +snippets/workflows/collaboration/main.go diff --git a/tools/go-snippets/runner.sh b/tools/go-snippets/runner.sh index ee3acca0b6..6850dfe76b 100755 --- a/tools/go-snippets/runner.sh +++ b/tools/go-snippets/runner.sh @@ -84,10 +84,15 @@ get_command_for_action() { local command="" if [ "${action}" == "build" ]; then - # For 'build', extract only the file paths, ignoring any arguments. - # 'go build' does not accept application arguments, so they must be stripped. - local files_to_build=$(echo "${line}" | awk '{for(i=1;i<=NF;i++) if($i ~ /\.go$/) printf "%s ", $i}') - command="go build ${files_to_build}" + # For 'build', build by package directory rather than by individual file. + # File-mode `go build pkg/main.go` ignores Go build constraints (e.g. + # //go:build tags), so a constrained file would be compiled regardless. + # Building the package directory honors those constraints. Each line lists + # files from a single package directory, and `go build ./dir/` is + # non-recursive, so this matches the intended target. Arguments are dropped + # because `go build` does not accept application arguments. + local dirs_to_build=$(echo "${line}" | awk '{for(i=1;i<=NF;i++) if($i ~ /\.go$/){d=$i; if (sub(/\/[^\/]*$/,"",d)) print "./"d"/"; else print "./"}}' | sort -u | tr '\n' ' ') + command="go build ${dirs_to_build}" elif [ "${action}" == "run" ]; then # For 'run', use the line as is, as 'go run' will pass arguments to the application. command="go run ${line}" @@ -137,7 +142,7 @@ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then # Update to the latest version of the ADK. # This ensures that we are always testing against the most recent release. - execute_and_check "(cd examples/go && go get google.golang.org/adk@latest)" "Updating google.golang.org/adk to latest" + execute_and_check "(cd examples/go && go get google.golang.org/adk/v2@latest)" "Updating google.golang.org/adk/v2 to latest" if [ ${EXIT_CODE} -ne 0 ]; then exit ${EXIT_CODE} # Exit immediately if go get failed. fi From d802044831ab68bc99aaa34908b199817758dac4 Mon Sep 17 00:00:00 2001 From: Haran Rajkumar Date: Tue, 30 Jun 2026 08:51:51 -0700 Subject: [PATCH 2/6] docs: update GenAI auth env var and Client parameter (#1832) * docs: update GOOGLE_GENAI_USE_VERTEXAI to GOOGLE_GENAI_USE_ENTERPRISE in configs and models * docs: update GOOGLE_GENAI_USE_VERTEXAI in deployment and setup guides * docs: update GOOGLE_GENAI_USE_VERTEXAI and Client parameter in integrations and streaming docs * docs: update GOOGLE_GENAI_USE_VERTEXAI in tutorials and examples --------- Co-authored-by: Joe Fernandez <931947+joefernandez@users.noreply.github.com> --- .github/workflows/update-python-docs.yaml | 2 +- docs/agents/config.md | 4 ++-- docs/agents/models/agent-platform.md | 4 ++-- docs/deploy/cloud-run.md | 6 +++--- docs/deploy/gke.md | 10 +++++----- .../get-started/streaming/quickstart-streaming-java.md | 2 +- docs/get-started/streaming/quickstart-streaming.md | 4 ++-- docs/grounding/grounding_with_search.md | 2 +- docs/integrations/bigquery-agent-analytics.md | 6 +++--- docs/integrations/cloud-trace.md | 2 +- docs/integrations/express-mode.md | 6 +++--- docs/integrations/galileo.md | 2 +- docs/streaming/dev-guide/part1.md | 10 +++++----- docs/streaming/streaming-tools.md | 2 +- docs/tutorials/agent-team.md | 2 +- examples/java/demos/patent-search-agent/README.md | 2 +- .../python/notebooks/express-mode-weather-agent.ipynb | 2 +- examples/python/notebooks/shop_agent.ipynb | 4 ++-- .../adk-streaming-ws/tests/test_log_20251029_151045.md | 2 +- .../python/tutorial/agent_team/adk-tutorial/readme.md | 2 +- examples/python/tutorial/agent_team/adk_tutorial.ipynb | 2 +- 21 files changed, 39 insertions(+), 39 deletions(-) diff --git a/.github/workflows/update-python-docs.yaml b/.github/workflows/update-python-docs.yaml index c404bbd0bf..bd31fa24eb 100644 --- a/.github/workflows/update-python-docs.yaml +++ b/.github/workflows/update-python-docs.yaml @@ -61,7 +61,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.ADK_BOT_GITHUB_TOKEN }} GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }} GOOGLE_CLOUD_LOCATION: ${{ secrets.GOOGLE_CLOUD_LOCATION }} - GOOGLE_GENAI_USE_VERTEXAI: 1 + GOOGLE_GENAI_USE_ENTERPRISE: 1 DOC_OWNER: 'google' DOC_REPO: 'adk-docs' CODE_OWNER: 'google' diff --git a/docs/agents/config.md b/docs/agents/config.md index f602cef189..6249931601 100644 --- a/docs/agents/config.md +++ b/docs/agents/config.md @@ -88,7 +88,7 @@ To create an ADK project for use with Agent Config: 1. For Gemini model access through Google API, add a line to the file with your API key: - GOOGLE_GENAI_USE_VERTEXAI=0 + GOOGLE_GENAI_USE_ENTERPRISE=0 GOOGLE_API_KEY= You can get an API key from the Google AI Studio @@ -96,7 +96,7 @@ To create an ADK project for use with Agent Config: 1. For Gemini model access through Google Cloud, add these lines to the file: - GOOGLE_GENAI_USE_VERTEXAI=1 + GOOGLE_GENAI_USE_ENTERPRISE=1 GOOGLE_CLOUD_PROJECT= GOOGLE_CLOUD_LOCATION=us-central1 diff --git a/docs/agents/models/agent-platform.md b/docs/agents/models/agent-platform.md index 29bfd7d0de..d8b6c74619 100644 --- a/docs/agents/models/agent-platform.md +++ b/docs/agents/models/agent-platform.md @@ -147,7 +147,7 @@ Agent Platform. **Setup:** 1. **Agent Platform Environment:** Ensure the consolidated Agent Platform setup (ADC, Env - Vars, `GOOGLE_GENAI_USE_VERTEXAI=TRUE`) is complete. + Vars, `GOOGLE_GENAI_USE_ENTERPRISE=TRUE`) is complete. 2. **Install Provider Library:** Install the necessary client library configured for Agent Platform. @@ -278,7 +278,7 @@ Agent Platform offers a curated selection of open-source models, such as Meta Ll **Setup:** 1. **Agent Platform Environment:** Ensure the consolidated Agent Platform setup (ADC, Env - Vars, `GOOGLE_GENAI_USE_VERTEXAI=TRUE`) is complete. + Vars, `GOOGLE_GENAI_USE_ENTERPRISE=TRUE`) is complete. 2. **Install LiteLLM:** ```shell diff --git a/docs/deploy/cloud-run.md b/docs/deploy/cloud-run.md index 9515d7a411..ee483e82d0 100644 --- a/docs/deploy/cloud-run.md +++ b/docs/deploy/cloud-run.md @@ -56,7 +56,7 @@ Set your environment variables as described in the [Setup and Installation](../g ```bash export GOOGLE_CLOUD_PROJECT=your-project-id export GOOGLE_CLOUD_LOCATION=us-central1 # Or your preferred location -export GOOGLE_GENAI_USE_VERTEXAI=True +export GOOGLE_GENAI_USE_ENTERPRISE=True ``` For more information on connecting to Google Cloud from ADK agents, see @@ -327,7 +327,7 @@ unless you specify it as deployment setting, such as the `--with_ui` option for --region $GOOGLE_CLOUD_LOCATION \ --project $GOOGLE_CLOUD_PROJECT \ --allow-unauthenticated \ - --set-env-vars="GOOGLE_CLOUD_PROJECT=$GOOGLE_CLOUD_PROJECT,GOOGLE_CLOUD_LOCATION=$GOOGLE_CLOUD_LOCATION,GOOGLE_GENAI_USE_VERTEXAI=$GOOGLE_GENAI_USE_VERTEXAI" + --set-env-vars="GOOGLE_CLOUD_PROJECT=$GOOGLE_CLOUD_PROJECT,GOOGLE_CLOUD_LOCATION=$GOOGLE_CLOUD_LOCATION,GOOGLE_GENAI_USE_ENTERPRISE=$GOOGLE_GENAI_USE_ENTERPRISE" # Add any other necessary environment variables your agent might need ``` @@ -570,7 +570,7 @@ unless you specify it as deployment setting, such as the `--with_ui` option for --region $GOOGLE_CLOUD_LOCATION \ --project $GOOGLE_CLOUD_PROJECT \ --allow-unauthenticated \ - --set-env-vars="GOOGLE_CLOUD_PROJECT=$GOOGLE_CLOUD_PROJECT,GOOGLE_CLOUD_LOCATION=$GOOGLE_CLOUD_LOCATION,GOOGLE_GENAI_USE_VERTEXAI=$GOOGLE_GENAI_USE_VERTEXAI" + --set-env-vars="GOOGLE_CLOUD_PROJECT=$GOOGLE_CLOUD_PROJECT,GOOGLE_CLOUD_LOCATION=$GOOGLE_CLOUD_LOCATION,GOOGLE_GENAI_USE_ENTERPRISE=$GOOGLE_GENAI_USE_ENTERPRISE" # Add any other necessary environment variables your agent might need ``` diff --git a/docs/deploy/gke.md b/docs/deploy/gke.md index 42dde6ca94..32089d68af 100644 --- a/docs/deploy/gke.md +++ b/docs/deploy/gke.md @@ -8,7 +8,7 @@ To deploy your agent you will need to have a Kubernetes cluster running on GKE. You can create a cluster using the Google Cloud Console or the `gcloud` command line tool. -In this example we will deploy a simple agent to GKE. The Python agent is a FastAPI application that uses `Gemini Flash` as the LLM. The Go agent uses the ADK launcher and a statically-linked binary in a minimal container. We can use Vertex AI or AI Studio as the LLM provider. +In this example deploys a simple agent to GKE. The Python agent is a FastAPI application that uses `Gemini Flash` as the LLM. The Go agent uses the ADK launcher and a statically-linked binary in a minimal container. We can use Vertex AI or AI Studio as the LLM provider. You can use Agent Platform or AI Studio as the LLM provider with the environment variable `GOOGLE_GENAI_USE_ENTERPRISE`. ## Environment variables @@ -17,7 +17,7 @@ Set your environment variables as described in the [Setup and Installation](../g ```bash export GOOGLE_CLOUD_PROJECT=your-project-id # Your GCP project ID export GOOGLE_CLOUD_LOCATION=us-central1 # Or your preferred location -export GOOGLE_GENAI_USE_VERTEXAI=true # Set to true if using Agent Platform +export GOOGLE_GENAI_USE_ENTERPRISE=true # Set to true if using Agent Platform export GOOGLE_CLOUD_PROJECT_NUMBER=$(gcloud projects describe --format json $GOOGLE_CLOUD_PROJECT | jq -r ".projectNumber") ``` @@ -479,9 +479,9 @@ spec: value: $GOOGLE_CLOUD_PROJECT - name: GOOGLE_CLOUD_LOCATION value: $GOOGLE_CLOUD_LOCATION - - name: GOOGLE_GENAI_USE_VERTEXAI - value: "$GOOGLE_GENAI_USE_VERTEXAI" - # If using AI Studio, set GOOGLE_GENAI_USE_VERTEXAI to false and set the following: + - name: GOOGLE_GENAI_USE_ENTERPRISE + value: "$GOOGLE_GENAI_USE_ENTERPRISE" + # If using AI Studio, set GOOGLE_GENAI_USE_ENTERPRISE to false and set the following: # - name: GOOGLE_API_KEY # value: $GOOGLE_API_KEY # Add any other necessary environment variables your agent might need diff --git a/docs/get-started/streaming/quickstart-streaming-java.md b/docs/get-started/streaming/quickstart-streaming-java.md index bf415c3389..1029996d4d 100644 --- a/docs/get-started/streaming/quickstart-streaming-java.md +++ b/docs/get-started/streaming/quickstart-streaming-java.md @@ -109,7 +109,7 @@ To run the server, you’ll need to export two environment variables: * a variable to specify we’re not using Agent Platform this time. ```shell -export GOOGLE_GENAI_USE_VERTEXAI=FALSE +export GOOGLE_GENAI_USE_ENTERPRISE=FALSE export GOOGLE_API_KEY=YOUR_API_KEY ``` diff --git a/docs/get-started/streaming/quickstart-streaming.md b/docs/get-started/streaming/quickstart-streaming.md index e4855c9bc1..21fc3c04eb 100644 --- a/docs/get-started/streaming/quickstart-streaming.md +++ b/docs/get-started/streaming/quickstart-streaming.md @@ -90,7 +90,7 @@ To run the agent, choose a platform from either Google AI Studio or Google Cloud 2. Open the **`.env`** file located inside (`app/`) and copy-paste the following code. ```env title=".env" - GOOGLE_GENAI_USE_VERTEXAI=FALSE + GOOGLE_GENAI_USE_ENTERPRISE=FALSE GOOGLE_API_KEY=PASTE_YOUR_ACTUAL_API_KEY_HERE ``` @@ -111,7 +111,7 @@ To run the agent, choose a platform from either Google AI Studio or Google Cloud the following code and update the project ID and location. ```env title=".env" - GOOGLE_GENAI_USE_VERTEXAI=TRUE + GOOGLE_GENAI_USE_ENTERPRISE=TRUE GOOGLE_CLOUD_PROJECT=PASTE_YOUR_ACTUAL_PROJECT_ID GOOGLE_CLOUD_LOCATION=us-central1 ``` diff --git a/docs/grounding/grounding_with_search.md b/docs/grounding/grounding_with_search.md index 1b7a86f0f1..2df534670f 100644 --- a/docs/grounding/grounding_with_search.md +++ b/docs/grounding/grounding_with_search.md @@ -22,7 +22,7 @@ Before creating a grounded agent, you must have an existing Agent Search Data St * For Java, ensure your application environment has Google Cloud default credentials configured (`GOOGLE_APPLICATION_CREDENTIALS`). ```env title=".env" -GOOGLE_GENAI_USE_VERTEXAI=TRUE +GOOGLE_GENAI_USE_ENTERPRISE=TRUE GOOGLE_CLOUD_PROJECT=YOUR_PROJECT_ID GOOGLE_CLOUD_LOCATION=LOCATION ``` diff --git a/docs/integrations/bigquery-agent-analytics.md b/docs/integrations/bigquery-agent-analytics.md index 4bb6d35279..568fb4b619 100644 --- a/docs/integrations/bigquery-agent-analytics.md +++ b/docs/integrations/bigquery-agent-analytics.md @@ -146,7 +146,7 @@ shows the BigQuery view optionally created when os.environ['GOOGLE_CLOUD_PROJECT'] = 'your-gcp-project-id' os.environ['GOOGLE_CLOUD_LOCATION'] = 'us-central1' - os.environ['GOOGLE_GENAI_USE_VERTEXAI'] = 'True' + os.environ['GOOGLE_GENAI_USE_ENTERPRISE'] = 'True' plugin = BigQueryAgentAnalyticsPlugin( project_id="your-gcp-project-id", @@ -274,7 +274,7 @@ LIMIT 20; # --- CRITICAL: Set environment variables BEFORE Gemini instantiation --- os.environ['GOOGLE_CLOUD_PROJECT'] = PROJECT_ID os.environ['GOOGLE_CLOUD_LOCATION'] = VERTEX_LOCATION - os.environ['GOOGLE_GENAI_USE_VERTEXAI'] = 'True' + os.environ['GOOGLE_GENAI_USE_ENTERPRISE'] = 'True' # --- Initialize the Plugin with Config --- bq_config = BigQueryLoggerConfig( @@ -1674,7 +1674,7 @@ DATASET_ID = os.environ.get("BQ_DATASET", "agent_analytics") # region used by GOOGLE_CLOUD_LOCATION. BQ_LOCATION = os.environ.get("BQ_LOCATION", "US") -os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "True" +os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "True" # --- Plugin --- bq_analytics_plugin = BigQueryAgentAnalyticsPlugin( diff --git a/docs/integrations/cloud-trace.md b/docs/integrations/cloud-trace.md index e8d7ad2de0..2e08bfae08 100644 --- a/docs/integrations/cloud-trace.md +++ b/docs/integrations/cloud-trace.md @@ -44,7 +44,7 @@ working_dir/ os.environ.setdefault("GOOGLE_CLOUD_PROJECT", "{your-project-id}") os.environ.setdefault("GOOGLE_CLOUD_LOCATION", "global") - os.environ.setdefault("GOOGLE_GENAI_USE_VERTEXAI", "True") + os.environ.setdefault("GOOGLE_GENAI_USE_ENTERPRISE", "True") # Define a tool function diff --git a/docs/integrations/express-mode.md b/docs/integrations/express-mode.md index 23df6db1ca..958cf6735d 100644 --- a/docs/integrations/express-mode.md +++ b/docs/integrations/express-mode.md @@ -47,7 +47,7 @@ With this approach, `Session` objects are handled as children of the variables are set correctly, as shown below: ```env title="agent/.env" -GOOGLE_GENAI_USE_VERTEXAI=TRUE +GOOGLE_GENAI_USE_ENTERPRISE=TRUE GOOGLE_API_KEY=PASTE_YOUR_ACTUAL_EXPRESS_MODE_API_KEY_HERE ``` @@ -90,7 +90,7 @@ the session object without any project or location. ```py # Requires: pip install google-adk[vertexai] # Plus environment variable setup: -# GOOGLE_GENAI_USE_VERTEXAI=TRUE +# GOOGLE_GENAI_USE_ENTERPRISE=TRUE # GOOGLE_API_KEY=PASTE_YOUR_ACTUAL_EXPRESS_MODE_API_KEY_HERE from google.adk.sessions import VertexAiSessionService @@ -119,7 +119,7 @@ the memory object without any project or location. ```py # Requires: pip install google-adk[vertexai] # Plus environment variable setup: -# GOOGLE_GENAI_USE_VERTEXAI=TRUE +# GOOGLE_GENAI_USE_ENTERPRISE=TRUE # GOOGLE_API_KEY=PASTE_YOUR_ACTUAL_EXPRESS_MODE_API_KEY_HERE from google.adk.memory import VertexAiMemoryBankService diff --git a/docs/integrations/galileo.md b/docs/integrations/galileo.md index 53aa233239..3e4490f35d 100644 --- a/docs/integrations/galileo.md +++ b/docs/integrations/galileo.md @@ -38,7 +38,7 @@ Configure environment variables: ```env title="my_agent/.env" # Gemini environment variables -GOOGLE_GENAI_USE_VERTEXAI=0 +GOOGLE_GENAI_USE_ENTERPRISE=0 GOOGLE_API_KEY="YOUR_API_KEY" # Galileo environment variables diff --git a/docs/streaming/dev-guide/part1.md b/docs/streaming/dev-guide/part1.md index 7c788f08f7..3ef6fafe9b 100644 --- a/docs/streaming/dev-guide/part1.md +++ b/docs/streaming/dev-guide/part1.md @@ -206,10 +206,10 @@ One of ADK's most powerful features is its transparent support for both [Gemini #### How Platform Selection Works -ADK uses the `GOOGLE_GENAI_USE_VERTEXAI` environment variable to determine which Live API platform to use: +ADK uses the `GOOGLE_GENAI_USE_ENTERPRISE` environment variable to determine which Live API platform to use: -- `GOOGLE_GENAI_USE_VERTEXAI=FALSE` (or not set): Uses Gemini Live API via Google AI Studio -- `GOOGLE_GENAI_USE_VERTEXAI=TRUE`: Uses Gemini Live API (Agent Platform) via Google Cloud +- `GOOGLE_GENAI_USE_ENTERPRISE=FALSE` (or not set): Uses Gemini Live API via Google AI Studio +- `GOOGLE_GENAI_USE_ENTERPRISE=TRUE`: Uses Gemini Live API (Agent Platform) via Google Cloud This environment variable is read by the underlying `google-genai` SDK when ADK creates the LLM connection. No code changes are needed when switching platforms—only environment configuration changes. @@ -217,7 +217,7 @@ This environment variable is read by the underlying `google-genai` SDK when ADK ```bash # .env.development -GOOGLE_GENAI_USE_VERTEXAI=FALSE +GOOGLE_GENAI_USE_ENTERPRISE=FALSE GOOGLE_API_KEY=your_api_key_here ``` @@ -232,7 +232,7 @@ GOOGLE_API_KEY=your_api_key_here ```bash # .env.production -GOOGLE_GENAI_USE_VERTEXAI=TRUE +GOOGLE_GENAI_USE_ENTERPRISE=TRUE GOOGLE_CLOUD_PROJECT=your_project_id GOOGLE_CLOUD_LOCATION=us-central1 ``` diff --git a/docs/streaming/streaming-tools.md b/docs/streaming/streaming-tools.md index 35d7cd4d12..9eeaafcdef 100644 --- a/docs/streaming/streaming-tools.md +++ b/docs/streaming/streaming-tools.md @@ -68,7 +68,7 @@ Now let's define an agent that can monitor stock price changes and monitor the v ) -> AsyncGenerator[str, None]: """Monitor how many people are in the video streams.""" print("start monitor_video_stream!") - client = Client(vertexai=False) + client = Client(enterprise=False) prompt_text = ( "Count the number of people in this image. Just respond with a numeric" " number." diff --git a/docs/tutorials/agent-team.md b/docs/tutorials/agent-team.md index b63c0c67b6..a79743a1f2 100644 --- a/docs/tutorials/agent-team.md +++ b/docs/tutorials/agent-team.md @@ -117,7 +117,7 @@ print(f"OpenAI API Key set: {'Yes' if os.environ.get('OPENAI_API_KEY') and os.en print(f"Anthropic API Key set: {'Yes' if os.environ.get('ANTHROPIC_API_KEY') and os.environ['ANTHROPIC_API_KEY'] != 'YOUR_ANTHROPIC_API_KEY' else 'No (REPLACE PLACEHOLDER!)'}") # Configure ADK to use API keys directly (not Agent Platform for this multi-model setup) -os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "False" +os.environ["GOOGLE_GENAI_USE_ENTERPRISE"] = "False" # @markdown **Security Note:** It's best practice to manage API keys securely (e.g., using Colab Secrets or environment variables) rather than hardcoding them directly in the notebook. Replace the placeholder strings above. diff --git a/examples/java/demos/patent-search-agent/README.md b/examples/java/demos/patent-search-agent/README.md index b24713be46..cb34965f2d 100644 --- a/examples/java/demos/patent-search-agent/README.md +++ b/examples/java/demos/patent-search-agent/README.md @@ -6,7 +6,7 @@ Using ADK Java SDK to perform the popular Patent Search (Contextual Search) use 2. Set env variables: ``` -export GOOGLE_GENAI_USE_VERTEXAI=FALSE +export GOOGLE_GENAI_USE_ENTERPRISE=FALSE export GOOGLE_API_KEY="" ``` 3. Update the placeholders in the code with values from your project (like PROJECT_ID etc.) diff --git a/examples/python/notebooks/express-mode-weather-agent.ipynb b/examples/python/notebooks/express-mode-weather-agent.ipynb index 8655e292a0..e699dcf726 100644 --- a/examples/python/notebooks/express-mode-weather-agent.ipynb +++ b/examples/python/notebooks/express-mode-weather-agent.ipynb @@ -102,7 +102,7 @@ "express_mode_api_key = \"YOUR-EXPRESS-MODE-API-KEY\" # @param {type:\"string\"}\n", "os.environ[\"GOOGLE_API_KEY\"] = express_mode_api_key\n", "# Set vertex to true\n", - "os.environ[\"GOOGLE_GENAI_USE_VERTEXAI\"] = \"True\"\n", + "os.environ[\"GOOGLE_GENAI_USE_ENTERPRISE\"] = \"True\"\n", "\n", "# --- Verify Keys (Optional Check) ---\n", "print(\"API Keys Set:\")\n", diff --git a/examples/python/notebooks/shop_agent.ipynb b/examples/python/notebooks/shop_agent.ipynb index 2a635a136d..da48cb8c54 100644 --- a/examples/python/notebooks/shop_agent.ipynb +++ b/examples/python/notebooks/shop_agent.ipynb @@ -147,14 +147,14 @@ "from getpass import getpass\n", "\n", "# Set environment variables required for running ADK (with Gemini API Key)\n", - "os.environ[\"GOOGLE_GENAI_USE_VERTEXAI\"] = \"False\"\n", + "os.environ[\"GOOGLE_GENAI_USE_ENTERPRISE\"] = \"False\"\n", "os.environ[\"GOOGLE_API_KEY\"] = getpass(\"Enter your Gemini API Key: \")\n", "\n", "# To use Agent Platform instead of Gemini API Key in Colab Enterprise or Cloud Workbench, use the following:\n", "# [PROJECT_ID] = !gcloud config list --format \"value(core.project)\"\n", "# os.environ[\"GOOGLE_CLOUD_PROJECT\"] = PROJECT_ID\n", "# os.environ[\"GOOGLE_CLOUD_LOCATION\"] = \"us-central1\"\n", - "# os.environ[\"GOOGLE_GENAI_USE_VERTEXAI\"] = \"True\"" + "# os.environ[\"GOOGLE_GENAI_USE_ENTERPRISE\"] = \"True\"" ] }, { diff --git a/examples/python/snippets/streaming/adk-streaming-ws/tests/test_log_20251029_151045.md b/examples/python/snippets/streaming/adk-streaming-ws/tests/test_log_20251029_151045.md index 9ab2fd4737..1fda526d86 100644 --- a/examples/python/snippets/streaming/adk-streaming-ws/tests/test_log_20251029_151045.md +++ b/examples/python/snippets/streaming/adk-streaming-ws/tests/test_log_20251029_151045.md @@ -13,7 +13,7 @@ ## Configuration ```env -GOOGLE_GENAI_USE_VERTEXAI=TRUE +GOOGLE_GENAI_USE_ENTERPRISE=TRUE GOOGLE_CLOUD_PROJECT=gcp-samples-ic0 GOOGLE_CLOUD_LOCATION=us-central1 DEMO_AGENT_MODEL=gemini-live-2.5-flash-preview-native-audio-09-2025 diff --git a/examples/python/tutorial/agent_team/adk-tutorial/readme.md b/examples/python/tutorial/agent_team/adk-tutorial/readme.md index fccd597b70..f527c11630 100644 --- a/examples/python/tutorial/agent_team/adk-tutorial/readme.md +++ b/examples/python/tutorial/agent_team/adk-tutorial/readme.md @@ -63,7 +63,7 @@ Before running any agent step, you **must** configure your API keys. **Example `.env` content:** ```dotenv # Set to False to use API keys directly (required for multi-model) - GOOGLE_GENAI_USE_VERTEXAI=FALSE + GOOGLE_GENAI_USE_ENTERPRISE=FALSE # --- Replace with your actual keys --- GOOGLE_API_KEY=PASTE_YOUR_ACTUAL_GOOGLE_API_KEY_HERE diff --git a/examples/python/tutorial/agent_team/adk_tutorial.ipynb b/examples/python/tutorial/agent_team/adk_tutorial.ipynb index e17a5678fc..4eec2a6391 100644 --- a/examples/python/tutorial/agent_team/adk_tutorial.ipynb +++ b/examples/python/tutorial/agent_team/adk_tutorial.ipynb @@ -148,7 +148,7 @@ ")\n", "\n", "# Configure ADK to use API keys directly (not Agent Platform for this multi-model setup)\n", - "os.environ[\"GOOGLE_GENAI_USE_VERTEXAI\"] = \"False\"\n", + "os.environ[\"GOOGLE_GENAI_USE_ENTERPRISE\"] = \"False\"\n", "\n", "\n", "# @markdown **Security Note:** It's best practice to manage API keys securely (e.g., using Colab Secrets or environment variables) rather than hardcoding them directly in the notebook. Replace the placeholder strings above." From 4922c8ea571a4be2b194952eaaa70c5405a6e2fc Mon Sep 17 00:00:00 2001 From: Kristopher Overholt Date: Tue, 30 Jun 2026 10:55:08 -0500 Subject: [PATCH 3/6] Pin adk/v2 to v2.0.0 release tag (#1908) Co-authored-by: Toni Klopfenstein <2359976+ToniCorinne@users.noreply.github.com> --- examples/go/go.mod | 2 +- examples/go/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/go/go.mod b/examples/go/go.mod index ea5a452c23..62535953cf 100644 --- a/examples/go/go.mod +++ b/examples/go/go.mod @@ -4,7 +4,7 @@ go 1.25.0 require ( google.golang.org/adk v1.4.0 - google.golang.org/adk/v2 v2.0.0-20260630132642-893e4a403a8d + google.golang.org/adk/v2 v2.0.0 google.golang.org/genai v1.57.0 ) diff --git a/examples/go/go.sum b/examples/go/go.sum index 83bd390ee6..62c956ceb5 100644 --- a/examples/go/go.sum +++ b/examples/go/go.sum @@ -107,8 +107,8 @@ gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/adk v1.4.0 h1:Qi4KB9YKD00/I5K9v3QsZ9ng5YiZQ7MfMgM8BZjNcsM= google.golang.org/adk v1.4.0/go.mod h1:R8tNFnI/eiBXHn7zJPJtqdiK/WXC+tVkyuZsXyNZXN4= -google.golang.org/adk/v2 v2.0.0-20260630132642-893e4a403a8d h1:1Y/1n1U7PmtnceaDu2SWf3+bX172UkphaEaXGsGsPSs= -google.golang.org/adk/v2 v2.0.0-20260630132642-893e4a403a8d/go.mod h1:fPuMPT5s3LsWu97mdeFjTPZu/02tIALWRWeqHL2FWKE= +google.golang.org/adk/v2 v2.0.0 h1:7eRbsnv0XkQPVctf8qtQ+KuO8XjkBrMNxznY6OA/sTs= +google.golang.org/adk/v2 v2.0.0/go.mod h1:fPuMPT5s3LsWu97mdeFjTPZu/02tIALWRWeqHL2FWKE= google.golang.org/api v0.279.0 h1:hsx2M2OaRcaKtVYK6vXEUnQvdjnend7ZYES+lYaot74= google.golang.org/api v0.279.0/go.mod h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ= google.golang.org/genai v1.57.0 h1:qTyG2ynz5dQy2jF4CvZdLHHVslhR0heMue+zM1a4GNM= From dbac4943471d39bdfe2432c48268546f746d9fb6 Mon Sep 17 00:00:00 2001 From: Kristopher Overholt Date: Tue, 30 Jun 2026 11:45:37 -0500 Subject: [PATCH 4/6] Fix jsonschema tags in ADK Go 2.0.0 graph snippets (#1909) --- examples/go/snippets/graphs/data-handling/main.go | 10 +++++----- examples/go/snippets/graphs/human-input/main.go | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/examples/go/snippets/graphs/data-handling/main.go b/examples/go/snippets/graphs/data-handling/main.go index 28f61ca1f6..b771ecdbfb 100644 --- a/examples/go/snippets/graphs/data-handling/main.go +++ b/examples/go/snippets/graphs/data-handling/main.go @@ -315,15 +315,15 @@ func stateScopes(ctx agent.Context) error { // these structs into *jsonschema.Schema automatically — no hand-built schema // construction needed. type FlightSearchInput struct { - Origin string `json:"origin" jsonschema:"description=Departure airport code e.g. SFO"` - Destination string `json:"destination" jsonschema:"description=Arrival airport code e.g. CDG"` - DepartureDate string `json:"departure_date" jsonschema:"description=Travel date in YYYY-MM-DD format"` + Origin string `json:"origin" jsonschema:"Departure airport code e.g. SFO"` + Destination string `json:"destination" jsonschema:"Arrival airport code e.g. CDG"` + DepartureDate string `json:"departure_date" jsonschema:"Travel date in YYYY-MM-DD format"` } // FlightSearchOutput is the typed output schema for the flight-search agent node. type FlightSearchOutput struct { - CheapestPrice string `json:"cheapest_price" jsonschema:"description=Cheapest available fare e.g. $450"` - FlightCount string `json:"flight_count" jsonschema:"description=Number of matching flights found"` + CheapestPrice string `json:"cheapest_price" jsonschema:"Cheapest available fare e.g. $450"` + FlightCount string `json:"flight_count" jsonschema:"Number of matching flights found"` } // newSchemaAgentPipeline demonstrates workflow.NewAgentNodeTyped, which infers diff --git a/examples/go/snippets/graphs/human-input/main.go b/examples/go/snippets/graphs/human-input/main.go index 9bde3cff0b..90713795d1 100644 --- a/examples/go/snippets/graphs/human-input/main.go +++ b/examples/go/snippets/graphs/human-input/main.go @@ -195,7 +195,7 @@ func newItineraryReviewWorkflow() (agent.Agent, error) { // --8<-- [start:simple-hitl] // DoubleNumberArgs holds the input for the doubleNumber tool. type DoubleNumberArgs struct { - Number int `json:"number" jsonschema:"description=The number to double."` + Number int `json:"number" jsonschema:"The number to double."` } // DoubleNumberResults holds the output of the doubleNumber tool. @@ -245,9 +245,9 @@ func newSimpleHITLAgent(ctx context.Context) (agent.Agent, error) { // --8<-- [start:hitl-with-hint] // BookFlightArgs holds the input for the bookFlight tool. type BookFlightArgs struct { - Origin string `json:"origin" jsonschema:"description=Departure airport code."` - Destination string `json:"destination" jsonschema:"description=Arrival airport code."` - Date string `json:"date" jsonschema:"description=Travel date in YYYY-MM-DD format."` + Origin string `json:"origin" jsonschema:"Departure airport code."` + Destination string `json:"destination" jsonschema:"Arrival airport code."` + Date string `json:"date" jsonschema:"Travel date in YYYY-MM-DD format."` } // BookFlightResults holds the outcome of the bookFlight tool. From 10176bf96e03bb903db3056fc49f5120ccdffa34 Mon Sep 17 00:00:00 2001 From: Kristopher Overholt Date: Tue, 30 Jun 2026 11:48:49 -0500 Subject: [PATCH 5/6] Clean up ADK Go 2.0.0 quickstart and snippet docs (#1910) * Format localhost URLs as inline code in quickstarts * Correct module paths and stale notes in snippet comments * Reword "and later" to "and higher" in snippet notes --- docs/get-started/go.md | 2 +- docs/get-started/java.md | 2 +- docs/get-started/kotlin.md | 2 +- docs/get-started/python.md | 2 +- docs/get-started/typescript.md | 2 +- examples/go/snippets/graphs/data-handling/main.go | 4 ++-- examples/go/snippets/graphs/dynamic/main.go | 6 +++--- examples/go/snippets/graphs/human-input/main.go | 2 +- examples/go/snippets/graphs/index/main.go | 4 ++-- examples/go/snippets/graphs/routes/main.go | 4 ++-- examples/go/snippets/workflows/collaboration/main.go | 4 +--- 11 files changed, 16 insertions(+), 18 deletions(-) diff --git a/docs/get-started/go.md b/docs/get-started/go.md index e1ba724d55..3ba3e03986 100644 --- a/docs/get-started/go.md +++ b/docs/get-started/go.md @@ -172,7 +172,7 @@ go run agent.go web api webui ``` This command starts a web server with a chat interface for your agent. You can -access the web interface at (http://localhost:8080). Select your agent at the +access the web interface at `http://localhost:8080`. Select your agent at the upper left corner and type a request. ![adk-web-dev-ui-chat.png](/assets/adk-web-dev-ui-chat.png) diff --git a/docs/get-started/java.md b/docs/get-started/java.md index 03749122e1..abd1886e91 100644 --- a/docs/get-started/java.md +++ b/docs/get-started/java.md @@ -269,7 +269,7 @@ mvn compile exec:java \ ``` This command starts a web server with a chat interface for your agent. You can -access the web interface at (http://localhost:8000). Select your agent at the +access the web interface at `http://localhost:8000`. Select your agent at the upper left corner and type a request. ![adk-web-dev-ui-chat.png](/assets/adk-web-dev-ui-chat.png) diff --git a/docs/get-started/kotlin.md b/docs/get-started/kotlin.md index 25d21ce8f4..18d3f1d813 100644 --- a/docs/get-started/kotlin.md +++ b/docs/get-started/kotlin.md @@ -279,7 +279,7 @@ gradle run -PmainClass=com.example.agent.WebMainKt ``` This command starts a web server with a chat interface for your agent. You can -access the web interface at (http://localhost:8080). Select your agent at the +access the web interface at `http://localhost:8080`. Select your agent at the upper left corner and type a request. ![adk-web-dev-ui-chat.png](/assets/adk-web-dev-ui-chat.png) diff --git a/docs/get-started/python.md b/docs/get-started/python.md index 40d8de02a4..cacd20b70a 100644 --- a/docs/get-started/python.md +++ b/docs/get-started/python.md @@ -150,7 +150,7 @@ adk web --port 8000 run `adk web` from the `agents/` directory. This command starts a web server with a chat interface for your agent. You can -access the web interface at (http://localhost:8000). Select the agent at the +access the web interface at `http://localhost:8000`. Select the agent at the upper left corner and type a request. ![adk-web-dev-ui-chat.png](/assets/adk-web-dev-ui-chat.png) diff --git a/docs/get-started/typescript.md b/docs/get-started/typescript.md index b241b67c2c..c928de220e 100644 --- a/docs/get-started/typescript.md +++ b/docs/get-started/typescript.md @@ -139,7 +139,7 @@ npx adk web ``` This command starts a web server with a chat interface for your agent. You can -access the web interface at (http://localhost:8000). Select your agent at the +access the web interface at `http://localhost:8000`. Select your agent at the upper right corner and type a request. ![adk-web-dev-ui-chat.png](/assets/adk-web-dev-ui-chat.png) diff --git a/examples/go/snippets/graphs/data-handling/main.go b/examples/go/snippets/graphs/data-handling/main.go index b771ecdbfb..f021642246 100644 --- a/examples/go/snippets/graphs/data-handling/main.go +++ b/examples/go/snippets/graphs/data-handling/main.go @@ -14,8 +14,8 @@ // Package main demonstrates data-handling patterns for ADK Go v2 workflow agents. // -// NOTE: This file requires google.golang.org/adk (the workflow package), -// available in ADK Go v2.0.0 and later. +// NOTE: This file requires google.golang.org/adk/v2 (the workflow package), +// available in ADK Go v2.0.0 and higher. // // # Data flow in ADK Go v2 // diff --git a/examples/go/snippets/graphs/dynamic/main.go b/examples/go/snippets/graphs/dynamic/main.go index 3d3201c272..a87a8e6dcb 100644 --- a/examples/go/snippets/graphs/dynamic/main.go +++ b/examples/go/snippets/graphs/dynamic/main.go @@ -13,10 +13,10 @@ // limitations under the License. // Package main demonstrates dynamic workflow patterns in ADK Go v2. // -// NOTE: This file requires the google.golang.org/adk/workflow package, -// which is available in ADK Go v2.0.0 and later. The workflow package is +// NOTE: This file requires the google.golang.org/adk/v2/workflow package, +// which is available in ADK Go v2.0.0 and higher. The workflow package is // not present in v1.x releases. The snippets in this file are based on the -// examples found in https://github.com/google/adk-go/releases/tag/v2.0.0/examples/workflow/. +// examples found in https://github.com/google/adk-go/tree/main/examples/workflow. // // Key types and functions used in this file: // diff --git a/examples/go/snippets/graphs/human-input/main.go b/examples/go/snippets/graphs/human-input/main.go index 90713795d1..488a5c0d39 100644 --- a/examples/go/snippets/graphs/human-input/main.go +++ b/examples/go/snippets/graphs/human-input/main.go @@ -15,7 +15,7 @@ // Package main demonstrates Human-in-the-Loop (HITL) patterns in ADK Go v2. // // NOTE: This file requires google.golang.org/adk/v2, available in ADK Go -// v2.0.0 and later. +// v2.0.0 and higher. // // # Graph HITL (primary pattern for /graphs/ pages) // diff --git a/examples/go/snippets/graphs/index/main.go b/examples/go/snippets/graphs/index/main.go index 06f03ee5d7..72c1632b64 100644 --- a/examples/go/snippets/graphs/index/main.go +++ b/examples/go/snippets/graphs/index/main.go @@ -14,8 +14,8 @@ // Package main provides snippet examples for graph-based workflow agents in ADK Go v2. // -// NOTE: This file requires google.golang.org/adk (the workflow package), -// available in ADK Go v2.0.0 and later. +// NOTE: This file requires google.golang.org/adk/v2 (the workflow package), +// available in ADK Go v2.0.0 and higher. // // Both snippets use the v2 graph engine (workflow.NewFunctionNode + // workflowagent.New) rather than the prebuilt workflow agents from v1.x. diff --git a/examples/go/snippets/graphs/routes/main.go b/examples/go/snippets/graphs/routes/main.go index 96e239ec9b..7da692b138 100644 --- a/examples/go/snippets/graphs/routes/main.go +++ b/examples/go/snippets/graphs/routes/main.go @@ -17,8 +17,8 @@ // workflow.Concat, workflow.NewEdgeBuilder, workflow.NewJoinNode, and // workflowagent.New. // -// NOTE: This file requires google.golang.org/adk (the workflow package), -// available in ADK Go v2.0.0 and later. +// NOTE: This file requires google.golang.org/adk/v2 (the workflow package), +// available in ADK Go v2.0.0 and higher. // // This file contains five snippet regions used in docs/graphs/routes.md: // diff --git a/examples/go/snippets/workflows/collaboration/main.go b/examples/go/snippets/workflows/collaboration/main.go index 6bb7c9a663..6ea9d75c5f 100644 --- a/examples/go/snippets/workflows/collaboration/main.go +++ b/examples/go/snippets/workflows/collaboration/main.go @@ -15,9 +15,7 @@ // Package main demonstrates collaborative agent team patterns in ADK Go v2. // // NOTE: This file requires google.golang.org/adk/v2, available in ADK Go -// v2.0.0 and later. It carries //go:build ignore so it is excluded from the -// current examples/go module (which is still on the v1 path) until examples/go -// is migrated to google.golang.org/adk at the v2.0.0 release. +// v2.0.0 and higher. // // # Agent collaboration modes in ADK Go v2 // From d002e9752a84ce8e595d6bccfe7593cc17d558cc Mon Sep 17 00:00:00 2001 From: Keyur Date: Tue, 30 Jun 2026 09:51:32 -0700 Subject: [PATCH 6/6] Add documentation for the GEPARootAgentOptimizer (#1847) Co-authored-by: Joe Fernandez <931947+joefernandez@users.noreply.github.com> --- docs/optimize/index.md | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/docs/optimize/index.md b/docs/optimize/index.md index 6c6501dfdc..ea9e7cd10c 100644 --- a/docs/optimize/index.md +++ b/docs/optimize/index.md @@ -255,6 +255,43 @@ Defaults to 3. optimization results if desired. Facilitates warm starts. +### `GEPARootAgentOptimizer` {#geparootagentoptimizer} + +The +[`GEPARootAgentOptimizer`](https://github.com/google/adk-python/blob/main/src/google/adk/optimization/gepa_root_agent_optimizer.py) +improves both the instructions of the root agent and the instructions of skills +provided to it via a +[`SkillToolset`](https://github.com/google/adk-python/blob/main/src/google/adk/tools/skill_toolset.py) +using the [GEPA](https://gepa-ai.github.io/gepa/) optimizer. +In many ways it can be considered to be an extension of the +[`GEPARootAgentPromptOptimizer`](#geparootagentpromptoptimizer). +It expects the sampler to provide eval results as an +[`UnstructuredSamplingResult`](#sampler-results). +Its output is a subclass of [`OptimizerResult`](#agent-optimizer-results) which +specifies a list of [optimized agents with scores](#agent-optimizer-results) and +additional metrics collected during optimization. + +Note: The `GEPARootAgentOptimizer` does not improve any sub-agents or agent +tools. + +You can configure the `GEPARootAgentOptimizer` with a +`GEPARootAgentOptimizerConfig` that contains the following fields: + +* `optimizer_model` (optional): The model used to analyze evaluation results and +optimize the agent. +Defaults to `"gemini-3.5-flash"`. +* `model_configuration` (optional): The configuration for the optimizer model. +Defaults to a config with a `ThinkingLevel` of `HIGH`. +* `max_metric_calls` (optional): The maximum number of evaluations to run during +optimization. +Defaults to 100. +* `reflection_minibatch_size` (optional): The number of examples to use at a +time to update the instructions. +Defaults to 3. +* `run_dir` (optional): The directory to save intermediate and final +optimization results if desired. +Facilitates warm starts. + ### `SimplePromptOptimizer` {#simplepromptoptimizer} The `SimplePromptOptimizer` is an automated, iterative prompt-tuning component designed @@ -272,7 +309,7 @@ The optimizer automatically executes an asynchronous, four-stage feedback loop: **Note:** The optimization loop does not mutate your initial agent instance in place. Upon completion, it returns an `OptimizerResult` containing the highest-scoring agent variation extracted during the process. -### Configuration +#### Configuration Configure the behavior of the loop by passing a `SimplePromptOptimizerConfig` instance to the optimizer. @@ -281,7 +318,7 @@ Configure the behavior of the loop by passing a `SimplePromptOptimizerConfig` in | `num_iterations` | int | *Required* | The total number of optimization rounds to execute. | | `batch_size` | int | *Required* | The number of evaluation sample cases processed by the sampler during each individual iteration. | -### Implementation Example +#### Implementation Example Once your configuration is defined, run the optimization with: