Skip to content

Commit 971de79

Browse files
committed
feat: add a2a_state_forwarding sample
Demonstrate how to forward client-side session state to a remote ADK agent over the A2A protocol via request metadata. The client uses a RequestInterceptor to copy whitelisted state keys into A2A metadata, and the server injects them back into session state through a before_agent_callback so that instruction template placeholders like {user_name} resolve correctly.
1 parent 4b677e7 commit 971de79

6 files changed

Lines changed: 297 additions & 0 deletions

File tree

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# A2A State Forwarding Sample Agent
2+
3+
This sample demonstrates how to forward **client-side session state** to a
4+
remote ADK agent over the **Agent-to-Agent (A2A)** protocol using A2A request
5+
metadata.
6+
7+
## Overview
8+
9+
ADK's A2A transport is stateless: when a `RemoteA2aAgent` sends a message to
10+
a remote agent, the caller's `session.state` is **not** automatically copied
11+
to the remote side. If the remote agent expects state values such as
12+
`user_name` (e.g. to resolve an `instruction` template placeholder like
13+
`{user_name}`), those values will be missing and the agent will not behave
14+
as intended.
15+
16+
This sample shows a working client-and-server configuration for bridging
17+
that gap:
18+
19+
- The **client** attaches a `RequestInterceptor` that copies a whitelisted
20+
subset of `session.state` into the outgoing A2A request metadata.
21+
- The **server** reads the incoming metadata (which ADK exposes as
22+
`run_config.custom_metadata['a2a_metadata']`) from a `before_agent_callback`
23+
and writes the values back into its own session state so the remote agent's
24+
`instruction` template can resolve them.
25+
26+
This pattern was confirmed as the recommended approach by an ADK maintainer
27+
in [google/adk-python#3098](https://github.com/google/adk-python/issues/3098),
28+
following the metadata-propagation work in commit
29+
[`ba631764`](https://github.com/google/adk-python/commit/ba631764a5be0c045de0d0be40330c7be8292a71).
30+
This sample complements that discussion by showing both sides wired together
31+
in a working minimal example.
32+
33+
## Architecture
34+
35+
```
36+
┌─────────────── Client (adk web) ───────────────┐
37+
│ │
38+
│ root_agent (Agent) │
39+
│ └─ before_agent_callback: seed state │
40+
│ state["user_name"] = "Alice" │
41+
│ └─ sub_agent: greet_agent (RemoteA2aAgent) │
42+
│ └─ RequestInterceptor.before_request │
43+
│ whitelisted session.state keys │
44+
│ └─→ parameters.request_metadata │
45+
│ │
46+
└──────────────────────┬─────────────────────────┘
47+
│ A2A JSON-RPC
48+
│ (MessageSendParams.metadata)
49+
50+
┌─────────── Remote agent (adk api_server --a2a) ┐
51+
│ │
52+
│ ADK converts incoming metadata to: │
53+
│ run_config.custom_metadata['a2a_metadata'] │
54+
│ │
55+
│ greet_agent (Agent) │
56+
│ └─ before_agent_callback │
57+
│ run_config.custom_metadata[...] │
58+
│ └─→ callback_context.state["user_name"] │
59+
│ │
60+
│ instruction "Hello {user_name}! ..." │
61+
│ resolves to "Hello Alice! ..." │
62+
│ │
63+
└────────────────────────────────────────────────┘
64+
```
65+
66+
## Key Features
67+
68+
### 1. Bridging Session State Across A2A
69+
70+
The remote `greet_agent` depends on `user_name` in its session state (used
71+
here as an `instruction` template placeholder `{user_name}`). No
72+
A2A-specific code is required beyond the one `before_agent_callback` that
73+
copies incoming metadata into session state.
74+
75+
### 2. Uses ADK Public APIs Only
76+
77+
Both sides of the sample rely on public ADK APIs:
78+
79+
- Client: `RemoteA2aAgent`, `A2aRemoteAgentConfig`, `RequestInterceptor`,
80+
`ParametersConfig` (from `google.adk.a2a.agent.config`).
81+
- Server: `callback_context.run_config.custom_metadata['a2a_metadata']`
82+
(populated automatically by ADK's A2A request converter).
83+
84+
No monkey-patching, no private attribute access.
85+
86+
## Setup and Usage
87+
88+
### Prerequisites
89+
90+
1. **Start the remote greet_agent A2A server**:
91+
92+
```bash
93+
adk api_server --a2a --port 8001 contributing/samples/a2a_state_forwarding/remote_a2a
94+
```
95+
96+
2. **Run the main agent** (in a separate terminal):
97+
98+
```bash
99+
adk web contributing/samples/
100+
```
101+
102+
### Example Interaction
103+
104+
```
105+
User: Please greet me.
106+
Bot: Hello Alice! How are you doing today?
107+
```
108+
109+
The root agent seeds `user_name = "Alice"` into its own session state, the
110+
`RequestInterceptor` forwards it through the A2A request metadata, and the
111+
remote `greet_agent` resolves its `{user_name}` template from that value.
112+
113+
## Limitations
114+
115+
- **One-way only.** This sample covers client → server state forwarding. The
116+
reverse direction (server → client) is not supported by the plain
117+
`to_a2a()` / `adk api_server --a2a` path because it does not expose an
118+
`after_agent` execute-interceptor hook. Applications that need bidirectional
119+
state sync must build a custom `A2aAgentExecutor` with an
120+
`ExecuteInterceptor`.
121+
- **Keep the whitelist tight.** The whitelist is a deliberate design choice,
122+
not a nicety. Do not replace `ALLOWED_FORWARD_KEYS` with `dict(ctx.session.state)`
123+
in production — doing so will leak whatever the caller happens to have in
124+
their session to every remote agent they call.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from . import agent
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from typing import Any
16+
17+
from a2a.types import Message as A2AMessage
18+
from google.adk.a2a.agent.config import A2aRemoteAgentConfig
19+
from google.adk.a2a.agent.config import ParametersConfig
20+
from google.adk.a2a.agent.config import RequestInterceptor
21+
from google.adk.agents.callback_context import CallbackContext
22+
from google.adk.agents.invocation_context import InvocationContext
23+
from google.adk.agents.llm_agent import Agent
24+
from google.adk.agents.remote_a2a_agent import AGENT_CARD_WELL_KNOWN_PATH
25+
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
26+
27+
# Only these session state keys are forwarded to the remote agent as A2A
28+
# request metadata. Keeping the list explicit prevents accidentally leaking
29+
# unrelated state (credentials, internal flags, large blobs, etc.) across the
30+
# service boundary.
31+
ALLOWED_FORWARD_KEYS: frozenset[str] = frozenset({"user_name"})
32+
33+
34+
async def _forward_state_as_a2a_metadata(
35+
ctx: InvocationContext,
36+
a2a_request: A2AMessage,
37+
parameters: ParametersConfig,
38+
) -> tuple[A2AMessage, ParametersConfig]:
39+
"""Forward whitelisted session state keys through A2A request metadata."""
40+
payload: dict[str, Any] = {
41+
key: value
42+
for key, value in ctx.session.state.items()
43+
if key in ALLOWED_FORWARD_KEYS
44+
}
45+
if payload:
46+
parameters.request_metadata = {
47+
**(parameters.request_metadata or {}),
48+
**payload,
49+
}
50+
return a2a_request, parameters
51+
52+
53+
greet_agent = RemoteA2aAgent(
54+
name="greet_agent",
55+
description="Greets the user using a name taken from session state.",
56+
agent_card=(
57+
f"http://localhost:8001/a2a/greet_agent{AGENT_CARD_WELL_KNOWN_PATH}"
58+
),
59+
config=A2aRemoteAgentConfig(
60+
request_interceptors=[
61+
RequestInterceptor(before_request=_forward_state_as_a2a_metadata),
62+
]
63+
),
64+
)
65+
66+
67+
def _seed_state(callback_context: CallbackContext) -> None:
68+
"""Seed demo session state so the remote agent has something to greet."""
69+
callback_context.state.setdefault("user_name", "Alice")
70+
71+
72+
root_agent = Agent(
73+
model="gemini-2.5-flash",
74+
name="root_agent",
75+
instruction=(
76+
"You are a helpful assistant. When the user asks to be greeted,"
77+
" delegate to the greet_agent sub-agent."
78+
),
79+
sub_agents=[greet_agent],
80+
before_agent_callback=_seed_state,
81+
)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from . import agent
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"capabilities": {},
3+
"defaultInputModes": ["text/plain"],
4+
"defaultOutputModes": ["application/json"],
5+
"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.",
6+
"name": "greet_agent",
7+
"skills": [
8+
{
9+
"id": "greet_user",
10+
"name": "Greet User",
11+
"description": "Greets the user by name using a value provided via session state / A2A request metadata.",
12+
"tags": ["greeting", "state", "a2a", "metadata"]
13+
}
14+
],
15+
"url": "http://localhost:8001/a2a/greet_agent",
16+
"version": "1.0.0"
17+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from google.adk.agents.callback_context import CallbackContext
16+
from google.adk.agents.llm_agent import Agent
17+
18+
19+
def _inject_metadata_into_state(callback_context: CallbackContext) -> None:
20+
"""Expand incoming A2A metadata into the session state.
21+
22+
ADK's request_converter places A2A request metadata under
23+
`run_config.custom_metadata['a2a_metadata']`. We copy those entries into
24+
session state so that the agent's `instruction` template can resolve
25+
placeholders like `{user_name}` from values the caller provided.
26+
"""
27+
run_config = callback_context.run_config
28+
if run_config is None or not run_config.custom_metadata:
29+
return
30+
a2a_metadata = run_config.custom_metadata.get("a2a_metadata") or {}
31+
for key, value in a2a_metadata.items():
32+
callback_context.state[key] = value
33+
34+
35+
root_agent = Agent(
36+
model="gemini-2.5-flash",
37+
name="greet_agent",
38+
description="Greets the user using a name provided in session state.",
39+
instruction=(
40+
"Greet the user exactly in the following format, without any extra"
41+
" text:\n"
42+
"Hello {user_name}! How are you doing today?"
43+
),
44+
before_agent_callback=_inject_metadata_into_state,
45+
)

0 commit comments

Comments
 (0)