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/.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/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/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/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/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/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/go.md b/docs/get-started/go.md index 31a1bffe80..3ba3e03986 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 ``` @@ -163,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) @@ -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/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/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/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/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/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/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: 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/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/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..62535953cf 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 google.golang.org/genai v1.57.0 ) diff --git a/examples/go/go.sum b/examples/go/go.sum index 3b64a84a66..62c956ceb5 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 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= 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..f021642246 --- /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/v2 (the workflow package), +// available in ADK Go v2.0.0 and higher. +// +// # 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:"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:"Cheapest available fare e.g. $450"` + FlightCount string `json:"flight_count" jsonschema:"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..a87a8e6dcb --- /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/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/tree/main/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..488a5c0d39 --- /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 higher. +// +// # 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:"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:"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. +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..72c1632b64 --- /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/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. +// 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..7da692b138 --- /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/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: +// +// 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..6ea9d75c5f --- /dev/null +++ b/examples/go/snippets/workflows/collaboration/main.go @@ -0,0 +1,154 @@ +// 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 higher. +// +// # 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/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." 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