Skip to content

Commit 57ba264

Browse files
MiloszSobczykcopybara-github
authored andcommitted
fix: propagate A2A request metadata into the run config in AgentExecutor
During A2A communication, `AgentExecutor` invoked the 4-argument `Runner.runAsync`, which hardcodes `stateDelta = null` and silently dropped the caller's incoming request metadata. Route the incoming request metadata (`RequestContext.getParams().metadata()`) into `RunConfig.customMetadata` under the `a2a_metadata` key, so downstream processing can read it. This mirrors ADK Python and Go, which pass A2A request metadata through the run config and leave the session state delta null, rather than writing framework IDs into session state. Add tests for the metadata and no-metadata cases. PiperOrigin-RevId: 945055254
1 parent 29c01fd commit 57ba264

2 files changed

Lines changed: 81 additions & 1 deletion

File tree

a2a/src/main/java/com/google/adk/a2a/executor/AgentExecutor.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.google.adk.a2a.converters.EventConverter;
2121
import com.google.adk.a2a.converters.PartConverter;
2222
import com.google.adk.agents.BaseAgent;
23+
import com.google.adk.agents.RunConfig;
2324
import com.google.adk.apps.App;
2425
import com.google.adk.artifacts.BaseArtifactService;
2526
import com.google.adk.events.Event;
@@ -38,6 +39,7 @@
3839
import io.a2a.spec.Artifact;
3940
import io.a2a.spec.InvalidAgentResponseError;
4041
import io.a2a.spec.Message;
42+
import io.a2a.spec.MessageSendParams;
4143
import io.a2a.spec.Part;
4244
import io.a2a.spec.TaskArtifactUpdateEvent;
4345
import io.a2a.spec.TaskState;
@@ -63,6 +65,7 @@
6365
public class AgentExecutor implements io.a2a.server.agentexecution.AgentExecutor {
6466
private static final Logger logger = LoggerFactory.getLogger(AgentExecutor.class);
6567
private static final String USER_ID_PREFIX = "A2A_USER_";
68+
private static final String A2A_METADATA_KEY = "a2a_metadata";
6669
private final Map<String, Disposable> activeTasks = new ConcurrentHashMap<>();
6770
private final Runner.Builder runnerBuilder;
6871
private final AgentExecutorConfig agentExecutorConfig;
@@ -217,7 +220,7 @@ public void execute(RequestContext ctx, EventQueue eventQueue) {
217220
getUserId(ctx),
218221
session.id(),
219222
content,
220-
agentExecutorConfig.runConfig());
223+
runConfigWithA2aMetadata(ctx));
221224
});
222225
})
223226
.concatMap(
@@ -273,6 +276,23 @@ private String getUserId(RequestContext ctx) {
273276
return USER_ID_PREFIX + ctx.getContextId();
274277
}
275278

279+
/**
280+
* Returns the configured run config enriched with the caller's incoming A2A request metadata
281+
* under the {@code a2a_metadata} key, so downstream processing can read it via {@link
282+
* RunConfig#customMetadata()}.
283+
*/
284+
private RunConfig runConfigWithA2aMetadata(RequestContext ctx) {
285+
RunConfig runConfig = agentExecutorConfig.runConfig();
286+
MessageSendParams params = ctx.getParams();
287+
Map<String, Object> requestMetadata = params == null ? null : params.metadata();
288+
if (requestMetadata == null || requestMetadata.isEmpty()) {
289+
return runConfig;
290+
}
291+
Map<String, Object> customMetadata = new HashMap<>(runConfig.customMetadata());
292+
customMetadata.put(A2A_METADATA_KEY, requestMetadata);
293+
return runConfig.toBuilder().customMetadata(customMetadata).build();
294+
}
295+
276296
private Maybe<Session> prepareSession(
277297
RequestContext ctx, String appName, BaseSessionService service) {
278298
return service

a2a/src/test/java/com/google/adk/a2a/executor/AgentExecutorTest.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import com.google.adk.agents.BaseAgent;
1414
import com.google.adk.agents.InvocationContext;
15+
import com.google.adk.agents.RunConfig;
1516
import com.google.adk.apps.App;
1617
import com.google.adk.artifacts.InMemoryArtifactService;
1718
import com.google.adk.events.Event;
@@ -24,6 +25,7 @@
2425
import io.a2a.server.agentexecution.RequestContext;
2526
import io.a2a.server.events.EventQueue;
2627
import io.a2a.spec.Message;
28+
import io.a2a.spec.MessageSendParams;
2729
import io.a2a.spec.TaskArtifactUpdateEvent;
2830
import io.a2a.spec.TaskState;
2931
import io.a2a.spec.TaskStatus;
@@ -342,6 +344,62 @@ public void execute_runnerSucceeds_registerCompletedTaskFails_noFailedTaskRegist
342344
assertThat(statusEvents).isEmpty();
343345
}
344346

347+
@Test
348+
public void execute_propagatesRequestMetadataIntoRunConfig() {
349+
testAgent.setEventsToEmit(Flowable.empty());
350+
AgentExecutor executor =
351+
new AgentExecutor.Builder()
352+
.agentExecutorConfig(AgentExecutorConfig.builder().build())
353+
.app(App.builder().name("test_app").rootAgent(testAgent).build())
354+
.sessionService(new InMemorySessionService())
355+
.artifactService(new InMemoryArtifactService())
356+
.build();
357+
358+
Message message =
359+
new Message.Builder()
360+
.messageId("msg-1")
361+
.role(Message.Role.USER)
362+
.parts(ImmutableList.of(new TextPart("trigger")))
363+
.build();
364+
MessageSendParams params =
365+
new MessageSendParams.Builder()
366+
.message(message)
367+
.metadata(ImmutableMap.of("key", "value"))
368+
.build();
369+
RequestContext ctx = mock(RequestContext.class);
370+
when(ctx.getMessage()).thenReturn(message);
371+
when(ctx.getTaskId()).thenReturn("task-1");
372+
when(ctx.getContextId()).thenReturn("ctx-1");
373+
when(ctx.getParams()).thenReturn(params);
374+
375+
executor.execute(ctx, eventQueue);
376+
377+
// The runner passes the enriched run config down to the agent's invocation context.
378+
RunConfig runConfig = testAgent.lastInvocationContext.runConfig();
379+
assertThat(runConfig.customMetadata())
380+
.containsEntry("a2a_metadata", ImmutableMap.of("key", "value"));
381+
}
382+
383+
@Test
384+
public void execute_withoutRequestMetadata_leavesRunConfigCustomMetadataEmpty() {
385+
testAgent.setEventsToEmit(Flowable.empty());
386+
AgentExecutor executor =
387+
new AgentExecutor.Builder()
388+
.agentExecutorConfig(AgentExecutorConfig.builder().build())
389+
.app(App.builder().name("test_app").rootAgent(testAgent).build())
390+
.sessionService(new InMemorySessionService())
391+
.artifactService(new InMemoryArtifactService())
392+
.build();
393+
394+
// createRequestContext() does not stub getParams(), mirroring a request with no metadata.
395+
RequestContext ctx = createRequestContext();
396+
397+
executor.execute(ctx, eventQueue);
398+
399+
RunConfig runConfig = testAgent.lastInvocationContext.runConfig();
400+
assertThat(runConfig.customMetadata()).doesNotContainKey("a2a_metadata");
401+
}
402+
345403
private RequestContext createRequestContext() {
346404
Message message =
347405
new Message.Builder()
@@ -507,6 +565,7 @@ public void execute_withDefaultArtifactPerRun_emitsMessageAndLastChunk() {
507565

508566
private static final class TestAgent extends BaseAgent {
509567
private Flowable<Event> eventsToEmit;
568+
private volatile InvocationContext lastInvocationContext;
510569

511570
TestAgent() {
512571
this(Flowable.empty());
@@ -524,6 +583,7 @@ void setEventsToEmit(Flowable<Event> events) {
524583

525584
@Override
526585
protected Flowable<Event> runAsyncImpl(InvocationContext invocationContext) {
586+
this.lastInvocationContext = invocationContext;
527587
return eventsToEmit;
528588
}
529589

0 commit comments

Comments
 (0)