Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions contributing/samples/a2a_state_forwarding/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# A2A State Forwarding Sample Agent

This sample demonstrates how to forward **client-side session state** to a
remote ADK agent over the **Agent-to-Agent (A2A)** protocol using A2A request
metadata.

## Overview

ADK's A2A transport is stateless: when a `RemoteA2aAgent` sends a message to
a remote agent, the caller's `session.state` is **not** automatically copied
to the remote side. If the remote agent expects state values such as
`user_name` (e.g. to resolve an `instruction` template placeholder like
`{user_name}`), those values will be missing and the agent will not behave
as intended.

This sample shows a working client-and-server configuration for bridging
that gap:

- The **client** attaches a `RequestInterceptor` that copies a whitelisted
subset of `session.state` into the outgoing A2A request metadata.
- The **server** reads the incoming metadata (which ADK exposes as
`run_config.custom_metadata['a2a_metadata']`) from a `before_agent_callback`
and writes the values back into its own session state so the remote agent's
`instruction` template can resolve them.

This pattern was confirmed as the recommended approach by an ADK maintainer
in [google/adk-python#3098](https://github.com/google/adk-python/issues/3098),
following the metadata-propagation work in commit
[`ba631764`](https://github.com/google/adk-python/commit/ba631764a5be0c045de0d0be40330c7be8292a71).
This sample complements that discussion by showing both sides wired together
in a working minimal example.

## Architecture

```
┌─────────────── Client (adk web) ───────────────┐
│ │
│ root_agent (Agent) │
│ └─ before_agent_callback: seed state │
│ state["user_name"] = "Alice" │
│ └─ sub_agent: greet_agent (RemoteA2aAgent) │
│ └─ RequestInterceptor.before_request │
│ whitelisted session.state keys │
│ └─→ parameters.request_metadata │
│ │
└──────────────────────┬─────────────────────────┘
│ A2A JSON-RPC
│ (MessageSendParams.metadata)
┌─────────── Remote agent (adk api_server --a2a) ┐
│ │
│ ADK converts incoming metadata to: │
│ run_config.custom_metadata['a2a_metadata'] │
│ │
│ greet_agent (Agent) │
│ └─ before_agent_callback │
│ run_config.custom_metadata[...] │
│ └─→ callback_context.state["user_name"] │
│ │
│ instruction "Hello {user_name}! ..." │
│ resolves to "Hello Alice! ..." │
│ │
└────────────────────────────────────────────────┘
```

## Key Features

### 1. Bridging Session State Across A2A

The remote `greet_agent` depends on `user_name` in its session state (used
here as an `instruction` template placeholder `{user_name}`). No
A2A-specific code is required beyond the one `before_agent_callback` that
copies incoming metadata into session state.

### 2. Uses ADK Public APIs Only

Both sides of the sample rely on public ADK APIs:

- Client: `RemoteA2aAgent`, `A2aRemoteAgentConfig`, `RequestInterceptor`,
`ParametersConfig` (from `google.adk.a2a.agent.config`).
- Server: `callback_context.run_config.custom_metadata['a2a_metadata']`
(populated automatically by ADK's A2A request converter).

No monkey-patching, no private attribute access.

## Setup and Usage

### Prerequisites

1. **Start the remote greet_agent A2A server**:

```bash
adk api_server --a2a --port 8001 contributing/samples/a2a_state_forwarding/remote_a2a
```

2. **Run the main agent** (in a separate terminal):

```bash
adk web contributing/samples/
```

### Example Interaction

```
User: Please greet me.
Bot: Hello Alice! How are you doing today?
```

The root agent seeds `user_name = "Alice"` into its own session state, the
`RequestInterceptor` forwards it through the A2A request metadata, and the
remote `greet_agent` resolves its `{user_name}` template from that value.

## Limitations

- **One-way only.** This sample covers client → server state forwarding. The
reverse direction (server → client) is not supported by the plain
`to_a2a()` / `adk api_server --a2a` path because it does not expose an
`after_agent` execute-interceptor hook. Applications that need bidirectional
state sync must build a custom `A2aAgentExecutor` with an
`ExecuteInterceptor`.
- **Keep the whitelist tight.** The whitelist is a deliberate design choice,
not a nicety. Do not replace `ALLOWED_FORWARD_KEYS` with `dict(ctx.session.state)`
in production — doing so will leak whatever the caller happens to have in
their session to every remote agent they call.
15 changes: 15 additions & 0 deletions contributing/samples/a2a_state_forwarding/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2026 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.

from . import agent
81 changes: 81 additions & 0 deletions contributing/samples/a2a_state_forwarding/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Copyright 2026 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.

from typing import Any

from a2a.types import Message as A2AMessage
from google.adk.a2a.agent.config import A2aRemoteAgentConfig
from google.adk.a2a.agent.config import ParametersConfig
from google.adk.a2a.agent.config import RequestInterceptor
from google.adk.agents.callback_context import CallbackContext
from google.adk.agents.invocation_context import InvocationContext
from google.adk.agents.llm_agent import Agent
from google.adk.agents.remote_a2a_agent import AGENT_CARD_WELL_KNOWN_PATH
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent

# Only these session state keys are forwarded to the remote agent as A2A
# request metadata. Keeping the list explicit prevents accidentally leaking
# unrelated state (credentials, internal flags, large blobs, etc.) across the
# service boundary.
ALLOWED_FORWARD_KEYS: frozenset[str] = frozenset({"user_name"})


async def _forward_state_as_a2a_metadata(
ctx: InvocationContext,
a2a_request: A2AMessage,
parameters: ParametersConfig,
) -> tuple[A2AMessage, ParametersConfig]:
"""Forward whitelisted session state keys through A2A request metadata."""
payload: dict[str, Any] = {
key: value
for key, value in ctx.session.state.items()
if key in ALLOWED_FORWARD_KEYS
}
if payload:
parameters.request_metadata = {
**(parameters.request_metadata or {}),
**payload,
}
return a2a_request, parameters


greet_agent = RemoteA2aAgent(
name="greet_agent",
description="Greets the user using a name taken from session state.",
agent_card=(
f"http://localhost:8001/a2a/greet_agent{AGENT_CARD_WELL_KNOWN_PATH}"
),
config=A2aRemoteAgentConfig(
request_interceptors=[
RequestInterceptor(before_request=_forward_state_as_a2a_metadata),
]
),
)


def _seed_state(callback_context: CallbackContext) -> None:
"""Seed demo session state so the remote agent has something to greet."""
callback_context.state.setdefault("user_name", "Alice")


root_agent = Agent(
model="gemini-2.5-flash",
name="root_agent",
instruction=(
"You are a helpful assistant. When the user asks to be greeted,"
" delegate to the greet_agent sub-agent."
),
sub_agents=[greet_agent],
before_agent_callback=_seed_state,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2026 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.

from . import agent
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"capabilities": {},
"defaultInputModes": ["text/plain"],
"defaultOutputModes": ["application/json"],
"description": "An agent that greets the user using a name taken from session state. Demonstrates how A2A request metadata can be forwarded into the remote agent's session state so that instruction templates (e.g. {user_name}) resolve correctly across the A2A boundary.",
"name": "greet_agent",
"skills": [
{
"id": "greet_user",
"name": "Greet User",
"description": "Greets the user by name using a value provided via session state / A2A request metadata.",
"tags": ["greeting", "state", "a2a", "metadata"]
}
],
"url": "http://localhost:8001/a2a/greet_agent",
"version": "1.0.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Copyright 2026 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.

from google.adk.agents.callback_context import CallbackContext
from google.adk.agents.llm_agent import Agent


def _inject_metadata_into_state(callback_context: CallbackContext) -> None:
"""Expand incoming A2A metadata into the session state.

ADK's request_converter places A2A request metadata under
`run_config.custom_metadata['a2a_metadata']`. We copy those entries into
session state so that the agent's `instruction` template can resolve
placeholders like `{user_name}` from values the caller provided.
"""
run_config = callback_context.run_config
if run_config is None or not run_config.custom_metadata:
return
a2a_metadata = run_config.custom_metadata.get("a2a_metadata") or {}
for key, value in a2a_metadata.items():
callback_context.state[key] = value


root_agent = Agent(
model="gemini-2.5-flash",
name="greet_agent",
description="Greets the user using a name provided in session state.",
instruction=(
"Greet the user exactly in the following format, without any extra"
" text:\n"
"Hello {user_name}! How are you doing today?"
),
before_agent_callback=_inject_metadata_into_state,
)
Loading