RemoteA2AAgent never attaches Message.metadata() - no way to propagate any custom data to the remote agent
Maintainer thường phản hồi trong vòng 1 ngày
@hemasekhar-p đang làm issue này rồi.
Từ ngày 18/8/2026.
Đánh giá
Issue này chưa được đánh giá.
Mô tả
Is your feature request related to a problem? Please describe.
RemoteA2AAgent builds the outbound io.a2a.spec.Message via prepareMessage() / newA2AMessage(),
but neither method ever calls Message.Builder#metadata(...). This means there is currently no way
for a Java ADK application to pass any custom data - session.state(), a user id, a tenant id, an
auth-scoped identifier the remote agent's tools need - to a remote A2A agent. The remote agent's tools
receive the conversation content only; anything the calling agent knows about the current user/session
is silently unavailable on the other side of the A2A boundary.
This is a real limitation for a common pattern: a tool on the remote agent needs to resolve a resource
(e.g. an OAuth access token) that is looked up by a caller-supplied identifier (e.g. user_id) stored
in session.state(). In-process sub-agent calls get this for free (session.state() is shared); A2A
calls get nothing.
Note this is not the same as two issues I filed previously, and I want to make the distinction
explicit so it's easy to keep this one scoped:
- #1240 (fixed in
410ff810) is about the receiving side:AgentExecutorused to silently drop
incomingMessageSendParams.metadata()instead of routing it intoRunConfig.customMetadata().
That's fixed - a receiving agent can now reada2a_metadatafromRunConfig.customMetadata()and,
via a nativebeforeAgentCallback, copy whatever it needs intosession.state(). - #1258 is about
RemoteA2AAgenthardcoding the 4th argument (ClientCallContext) of
a2aClient.sendMessage(...)tonull, which breaks transport-level concerns (HTTP header
resolution, credential services used to authenticate the A2A call itself). - This issue is about a third, independent gap: even with both of the above fixed, the
Messageobject itself - the 1st argument tosendMessage, built byprepareMessage()- never
gets.metadata(...)attached at all. There is no code path, and no builder hook, to put anything
there. Fixing #1258 alone would not address this:ClientCallContextandMessageare separate
parameters built independently.
Describe the solution you'd like
adk-python's RemoteA2aAgent already solves exactly this with an opt-in callback:
# src/google/adk/agents/remote_a2a_agent.py:146-148
a2a_request_meta_provider: Optional[
Callable[[InvocationContext, A2AMessage], dict[str, Any]]
] = None
# src/google/adk/agents/remote_a2a_agent.py:739-742
if self._a2a_request_meta_provider:
parameters.request_metadata = self._a2a_request_meta_provider(
ctx, a2a_request
)
A caller can implement this to explicitly select what to forward, e.g.:
def my_meta_provider(ctx: InvocationContext, message: A2AMessage) -> dict[str, Any]:
return {"user_id": ctx.session.state.get("user_id")}
remote_agent = RemoteA2aAgent(..., a2a_request_meta_provider=my_meta_provider)
I'd like RemoteA2AAgent (Java) to expose the equivalent extension point, e.g.:
@FunctionalInterface
public interface A2ARequestMetadataProvider {
Map<String, Object> provide(InvocationContext invocationContext, Message outgoingMessage);
}
RemoteA2AAgent.builder()
...
.requestMetadataProvider((ctx, message) -> Map.of("user_id", ctx.session().state().get("user_id")))
.build();
and, inside prepareMessage(), call it and attach the result via .metadata(...) on the Message.Builder.
This is deliberately opt-in and lets the caller pick exactly what crosses the A2A boundary - it does not
ask for session.state() to be forwarded automatically or in full, which I understand is intentionally
avoided elsewhere in ADK (per the #1240 resolution comment).
The provider is a plain callback with no fixed/whitelisted set of keys baked into the API - ADK would
simply attach whatever Map<String, Object> the caller's implementation returns. It's entirely up to the
application to decide what to include: a single identifier, several selected keys, or (if it chooses to)
all of session.state(). This mirrors the full flexibility of a2a_request_meta_provider in adk-python -
the library imposes no restriction on which keys or how many can be returned, it only wires the callback
through to Message.metadata().
Minimal reproducible example (runnable today, no external services needed)
Drop this test method into
a2a/src/test/java/com/google/adk/a2a/agent/RemoteA2AAgentTest.java (it uses only fixtures/imports
already present in that file) and run:
mvn -pl a2a test -Dtest=RemoteA2AAgentTest#runAsync_doesNotPropagateSessionStateToOutboundMessage
The test passes today, which is the bug: it proves session.state() - set up exactly the way an
application would populate it via stateDelta before running the agent - never reaches the Message
sent to the remote peer, even though mockClient.sendMessage(...) is the exact call site
prepareMessage() feeds.
@Test
@SuppressWarnings("unchecked") // cast for Mockito
public void runAsync_doesNotPropagateSessionStateToOutboundMessage() {
RemoteA2AAgent agent = createAgent();
// Simulates an application that populated session.state() via stateDelta before this run -
// e.g. a user id a remote tool would need to resolve an OAuth token, exactly as it would for
// an in-process sub-agent call.
Session sessionWithState =
Session.builder("session-state-repro")
.appName("demo")
.userId("user")
.state(ImmutableMap.of("user_id", "user-42", "tenant_id", "tenant-7"))
.events(
ImmutableList.of(
Event.builder()
.id("e1")
.author("user")
.content(
Content.builder()
.role("user")
.parts(ImmutableList.of(Part.builder().text("hello").build()))
.build())
.build()))
.build();
InvocationContext context =
InvocationContext.builder()
.sessionService(new InMemorySessionService())
.artifactService(new InMemoryArtifactService())
.pluginManager(new PluginManager())
.invocationId("invocation-state-repro")
.agent(new TestAgent())
.session(sessionWithState)
.runConfig(RunConfig.builder().build())
.build();
mockStreamResponse(consumer -> consumer.accept(createFinalEvent("ok"), agentCard));
var unused = agent.runAsync(context).toList().blockingGet();
ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class);
verify(mockClient)
.sendMessage(messageCaptor.capture(), any(List.class), any(Consumer.class), any());
Message sentMessage = messageCaptor.getValue();
// BUG: session.state() (user_id, tenant_id) is fully known to invocationContext at this point,
// but prepareMessage()/newA2AMessage() never call .metadata(...), so it never reaches the
// outbound Message. A remote agent's tools have no way to see it - even though the exact same
// data would be visible via session.state() for an in-process sub-agent call.
assertThat(sentMessage.getMetadata()).isAnyOf(null, ImmutableMap.of());
}
Environment
google-adk-a2a: 1.8.0 (currentmain, confirmed present atRemoteA2AAgent.java:196-213
(newA2AMessage/prepareMessage) andRemoteA2AAgent.java:240(thesendMessagecall site))- Java 17
- Ngôn ngữ chính
- Java
- Star
- 1.7k
- Fork
- 431
- Merge trung bình
- 2 ngày 20 giờ
- Pull request đã merge (30 ngày)
- 42
Chuẩn bị môi trường
Khởi chạy dev container của dự án ngay trên trình duyệt, bằng tài khoản GitHub của bạn.
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của google/adk-java
-
[spring-ai] ToolConverter silently drops enum and items from tool parameter schemasCó thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
google/adk-java#1609 · 1 bình luận · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[spring-ai] Streaming responses ending with CJK punctuation (。!?) are misclassified as partial and never persisted to the sessionCó thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 84/100
google/adk-java#1608 · 1 bình luận · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
[spring-ai] Bridge drops reasoning_content (thinking) — surface it as partial events and/or persist itCó thể đã có người làm @hemasekhar-p đã nhận 1 ngày trước. Đang mởneeds review
google/adk-java#1616 · 1 bình luận · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
needs review
Độ khó 5/5 Hơn một tuần Mức phù hợp với người mới 35/100
google/adk-java#1598 · 1 bình luận · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
-
BaseLlmFlow nests each step inside the previous one and overflows the stack after a few hundred LLM callsCó thể đã có người làm @hemasekhar-p đã nhận 8 ngày trước. Đang mởneeds review
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 68/100
google/adk-java#1564 · 1 bình luận · 1 người được giao ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của google/adk-java
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 86/100
-
Make branch and label autocomplete matching locale-independentCó thể đã có người làm Có pull request liên kết đang mở hoặc đã được merge. Đang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 83/100
jenkinsci/gitlab-plugin#1950 ·
-
Place type search does not workĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 65/100
commons-app/apps-android-commons#6984 ·
Maintainer thường phản hồi trong vòng 2 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
Maintainer thường phản hồi trong vòng 1 ngày
-
It's not necessary to copy the memory block in the readWrite() of org.h2.store.fs.mem.FileMemDataĐang mở
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
h2database/h2database#4435 ·
Maintainer thường phản hồi trong vòng 1 ngày