Skip to content

Commit 45d3cb3

Browse files
YuqiGuo105hemasekhar-p
authored andcommitted
feat: support metadata providers for remote A2A messages
1 parent aab51ef commit 45d3cb3

4 files changed

Lines changed: 372 additions & 9 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.google.adk.a2a.agent;
17+
18+
import com.google.adk.agents.InvocationContext;
19+
import io.a2a.spec.Message;
20+
import java.util.Map;
21+
import org.jspecify.annotations.Nullable;
22+
23+
/** Supplies application-selected metadata for an outgoing A2A message. */
24+
@FunctionalInterface
25+
public interface A2ARequestMetadataProvider {
26+
27+
/**
28+
* Returns JSON-serializable metadata after the message's parts and identifiers have been set.
29+
* Returning {@code null} leaves the message unchanged. An exception aborts sending the message.
30+
*
31+
* <p>The result is attached to {@link Message#getMetadata()}, not request-level metadata or
32+
* authentication headers. On an ADK receiver it is available through the incoming A2A message; it
33+
* is not automatically copied into session state or {@code RunConfig.customMetadata()}.
34+
*/
35+
@Nullable Map<String, Object> provide(
36+
InvocationContext invocationContext, Message outgoingMessage);
37+
}

‎a2a/src/main/java/com/google/adk/a2a/agent/RemoteA2AAgent.java‎

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@
4949
import io.reactivex.rxjava3.core.Flowable;
5050
import io.reactivex.rxjava3.core.FlowableEmitter;
5151
import java.util.ArrayList;
52+
import java.util.LinkedHashMap;
5253
import java.util.List;
54+
import java.util.Map;
5355
import java.util.Objects;
5456
import java.util.Optional;
5557
import java.util.function.BiConsumer;
@@ -84,6 +86,7 @@ public class RemoteA2AAgent extends BaseAgent {
8486
private final Client a2aClient;
8587
private String description;
8688
private final boolean streaming;
89+
private final @Nullable A2ARequestMetadataProvider requestMetadataProvider;
8790

8891
// Internal constructor used by builder
8992
private RemoteA2AAgent(Builder builder) {
@@ -117,6 +120,7 @@ private RemoteA2AAgent(Builder builder) {
117120
this.description = this.agentCard.description();
118121
}
119122
this.streaming = builder.streaming && this.agentCard.capabilities().streaming();
123+
this.requestMetadataProvider = builder.requestMetadataProvider;
120124
}
121125

122126
public static Builder builder() {
@@ -133,6 +137,7 @@ public static class Builder {
133137
private List<Callbacks.BeforeAgentCallback> beforeAgentCallback;
134138
private List<Callbacks.AfterAgentCallback> afterAgentCallback;
135139
private boolean streaming;
140+
private @Nullable A2ARequestMetadataProvider requestMetadataProvider;
136141

137142
@CanIgnoreReturnValue
138143
public Builder streaming(boolean streaming) {
@@ -182,6 +187,16 @@ public Builder a2aClient(Client a2aClient) {
182187
return this;
183188
}
184189

190+
/**
191+
* Sets an optional provider called once for each outgoing message, after its parts and
192+
* identifiers have been populated. Session state is never forwarded automatically.
193+
*/
194+
@CanIgnoreReturnValue
195+
public Builder requestMetadataProvider(@Nullable A2ARequestMetadataProvider provider) {
196+
this.requestMetadataProvider = provider;
197+
return this;
198+
}
199+
185200
public RemoteA2AAgent build() {
186201
return new RemoteA2AAgent(this);
187202
}
@@ -198,19 +213,30 @@ private Message.Builder newA2AMessage(
198213

199214
private Message prepareMessage(InvocationContext invocationContext) {
200215
Event userCall = EventConverter.findUserFunctionCall(invocationContext.session().events());
216+
Message message;
201217
if (userCall != null) {
202218
ImmutableList<io.a2a.spec.Part<?>> parts =
203219
EventConverter.contentToParts(userCall.content(), userCall.partial().orElse(false));
204-
return newA2AMessage(invocationContext, Message.Role.USER, parts)
205-
.taskId(EventConverter.taskId(userCall))
206-
.contextId(EventConverter.contextId(userCall))
207-
.build();
220+
message =
221+
newA2AMessage(invocationContext, Message.Role.USER, parts)
222+
.taskId(EventConverter.taskId(userCall))
223+
.contextId(EventConverter.contextId(userCall))
224+
.build();
225+
} else {
226+
message =
227+
newA2AMessage(
228+
invocationContext,
229+
Message.Role.USER,
230+
EventConverter.messagePartsFromContext(invocationContext))
231+
.build();
232+
}
233+
if (requestMetadataProvider == null) {
234+
return message;
208235
}
209-
return newA2AMessage(
210-
invocationContext,
211-
Message.Role.USER,
212-
EventConverter.messagePartsFromContext(invocationContext))
213-
.build();
236+
Map<String, Object> metadata = requestMetadataProvider.provide(invocationContext, message);
237+
return metadata == null
238+
? message
239+
: new Message.Builder(message).metadata(new LinkedHashMap<>(metadata)).build();
214240
}
215241

216242
@Override

0 commit comments

Comments
 (0)