Skip to content

Commit f9abd0e

Browse files
fix: enable service injection in A2ASendMessageExecutor
Updated A2ASendMessageExecutor and A2ARemoteConfiguration to support dependency injection of session, artifact, and memory services. This allows persistent service implementations to replace hard-coded in-memory versions. Changes: - Modified A2ASendMessageExecutor constructor to accept BaseSessionService, BaseArtifactService, and BaseMemoryService parameters - Updated A2ARemoteConfiguration to autowire service beans with fallback to in-memory defaults when custom beans are not provided - Added comprehensive test coverage for service injection scenarios Tasks: [x] Update A2ASendMessageExecutor to accept injected services [x] Modify A2ARemoteConfiguration to autowire service beans [x] Add unit tests for A2ASendMessageExecutor [x] Add integration tests for service injection [x] Add Spring configuration tests for custom services
1 parent b66e4a5 commit f9abd0e

File tree

8 files changed

+654
-15
lines changed

8 files changed

+654
-15
lines changed

a2a/src/main/java/com/google/adk/a2a/A2ASendMessageExecutor.java

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
import com.google.adk.a2a.converters.ResponseConverter;
99
import com.google.adk.agents.BaseAgent;
1010
import com.google.adk.agents.RunConfig;
11-
import com.google.adk.artifacts.InMemoryArtifactService;
11+
import com.google.adk.artifacts.BaseArtifactService;
1212
import com.google.adk.events.Event;
13-
import com.google.adk.memory.InMemoryMemoryService;
13+
import com.google.adk.memory.BaseMemoryService;
1414
import com.google.adk.runner.Runner;
15-
import com.google.adk.sessions.InMemorySessionService;
15+
import com.google.adk.sessions.BaseSessionService;
1616
import com.google.adk.sessions.Session;
1717
import com.google.common.collect.ImmutableList;
1818
import com.google.genai.types.Content;
@@ -51,29 +51,63 @@ Single<ImmutableList<Event>> execute(
5151
String invocationId);
5252
}
5353

54-
private final InMemorySessionService sessionService;
54+
private final BaseSessionService sessionService;
5555
private final String appName;
5656
@Nullable private final Runner runner;
5757
@Nullable private final Duration agentTimeout;
5858
private static final RunConfig DEFAULT_RUN_CONFIG =
5959
RunConfig.builder().setStreamingMode(RunConfig.StreamingMode.NONE).setMaxLlmCalls(20).build();
6060

61-
public A2ASendMessageExecutor(InMemorySessionService sessionService, String appName) {
61+
public A2ASendMessageExecutor(BaseSessionService sessionService, String appName) {
6262
this.sessionService = sessionService;
6363
this.appName = appName;
6464
this.runner = null;
6565
this.agentTimeout = null;
6666
}
6767

68-
public A2ASendMessageExecutor(BaseAgent agent, String appName, Duration agentTimeout) {
69-
InMemorySessionService sessionService = new InMemorySessionService();
68+
/**
69+
* Creates an A2A send message executor with explicit service dependencies.
70+
*
71+
* <p>This constructor requires all service implementations to be provided explicitly, enabling
72+
* flexible deployment configurations (e.g., persistent sessions, distributed artifacts).
73+
*
74+
* <p><strong>Note:</strong> In version 0.5.1, the constructor signature changed to require
75+
* explicit service injection. Previously, services were created internally as in-memory
76+
* implementations.
77+
*
78+
* <p><strong>For Spring Boot applications:</strong> Use {@link
79+
* com.google.adk.webservice.A2ARemoteConfiguration} which automatically provides service beans
80+
* with sensible defaults. Direct instantiation is typically only needed for custom frameworks or
81+
* testing.
82+
*
83+
* <p>Example usage:
84+
*
85+
* <pre>{@code
86+
* A2ASendMessageExecutor executor = new A2ASendMessageExecutor(
87+
* myAgent,
88+
* "my-app",
89+
* Duration.ofSeconds(30),
90+
* new InMemorySessionService(), // or DatabaseSessionService for persistence
91+
* new InMemoryArtifactService(), // or S3ArtifactService for distributed storage
92+
* new InMemoryMemoryService()); // or RedisMemoryService for shared state
93+
* }</pre>
94+
*
95+
* @param agent the agent to execute when processing messages
96+
* @param appName the application name used for session identification
97+
* @param agentTimeout maximum duration to wait for agent execution before timing out
98+
* @param sessionService service for managing conversation sessions (required, non-null)
99+
* @param artifactService service for storing and retrieving artifacts (required, non-null)
100+
* @param memoryService service for managing agent memory/state (required, non-null)
101+
*/
102+
public A2ASendMessageExecutor(
103+
BaseAgent agent,
104+
String appName,
105+
Duration agentTimeout,
106+
BaseSessionService sessionService,
107+
BaseArtifactService artifactService,
108+
BaseMemoryService memoryService) {
70109
Runner runnerInstance =
71-
new Runner(
72-
agent,
73-
appName,
74-
new InMemoryArtifactService(),
75-
sessionService,
76-
new InMemoryMemoryService());
110+
new Runner(agent, appName, artifactService, sessionService, memoryService);
77111
this.sessionService = sessionService;
78112
this.appName = appName;
79113
this.runner = runnerInstance;
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
package com.google.adk.a2a;
2+
3+
import static com.google.common.truth.Truth.assertThat;
4+
5+
import com.google.adk.agents.BaseAgent;
6+
import com.google.adk.agents.InvocationContext;
7+
import com.google.adk.artifacts.InMemoryArtifactService;
8+
import com.google.adk.events.Event;
9+
import com.google.adk.memory.InMemoryMemoryService;
10+
import com.google.adk.sessions.InMemorySessionService;
11+
import com.google.common.collect.ImmutableList;
12+
import com.google.genai.types.Content;
13+
import com.google.genai.types.Part;
14+
import io.a2a.spec.Message;
15+
import io.a2a.spec.TextPart;
16+
import io.reactivex.rxjava3.core.Flowable;
17+
import io.reactivex.rxjava3.core.Single;
18+
import java.time.Duration;
19+
import java.util.List;
20+
import java.util.UUID;
21+
import org.junit.Test;
22+
import org.junit.runner.RunWith;
23+
import org.junit.runners.JUnit4;
24+
25+
@RunWith(JUnit4.class)
26+
public class A2ASendMessageExecutorAdvancedTest {
27+
28+
@Test
29+
public void execute_withCustomStrategy_usesStrategy() {
30+
InMemorySessionService sessionService = new InMemorySessionService();
31+
32+
A2ASendMessageExecutor executor = new A2ASendMessageExecutor(sessionService, "test-app");
33+
34+
A2ASendMessageExecutor.AgentExecutionStrategy customStrategy =
35+
(userId, sessionId, userContent, runConfig, invocationId) -> {
36+
Event customEvent =
37+
Event.builder()
38+
.id(UUID.randomUUID().toString())
39+
.invocationId(invocationId)
40+
.author("agent")
41+
.content(
42+
Content.builder()
43+
.role("model")
44+
.parts(
45+
ImmutableList.of(
46+
Part.builder().text("Custom strategy response").build()))
47+
.build())
48+
.build();
49+
return Single.just(ImmutableList.of(customEvent));
50+
};
51+
52+
Message request =
53+
new Message.Builder()
54+
.messageId("msg-1")
55+
.contextId("ctx-1")
56+
.role(Message.Role.USER)
57+
.parts(List.of(new TextPart("Test")))
58+
.build();
59+
60+
Message response = executor.execute(request, customStrategy).blockingGet();
61+
62+
assertThat(response).isNotNull();
63+
assertThat(response.getParts()).isNotEmpty();
64+
assertThat(((TextPart) response.getParts().get(0)).getText())
65+
.contains("Custom strategy response");
66+
}
67+
68+
@Test
69+
public void execute_withNullMessage_generatesDefaultContext() {
70+
BaseAgent agent = createSimpleAgent();
71+
InMemorySessionService sessionService = new InMemorySessionService();
72+
73+
A2ASendMessageExecutor executor =
74+
new A2ASendMessageExecutor(
75+
agent,
76+
"test-app",
77+
Duration.ofSeconds(30),
78+
sessionService,
79+
new InMemoryArtifactService(),
80+
new InMemoryMemoryService());
81+
82+
Message response = executor.execute(null).blockingGet();
83+
84+
assertThat(response).isNotNull();
85+
assertThat(response.getContextId()).isNotNull();
86+
assertThat(response.getContextId()).isNotEmpty();
87+
}
88+
89+
@Test
90+
public void execute_withEmptyContextId_generatesNewContext() {
91+
BaseAgent agent = createSimpleAgent();
92+
InMemorySessionService sessionService = new InMemorySessionService();
93+
94+
A2ASendMessageExecutor executor =
95+
new A2ASendMessageExecutor(
96+
agent,
97+
"test-app",
98+
Duration.ofSeconds(30),
99+
sessionService,
100+
new InMemoryArtifactService(),
101+
new InMemoryMemoryService());
102+
103+
Message request =
104+
new Message.Builder()
105+
.messageId("msg-1")
106+
.role(Message.Role.USER)
107+
.parts(List.of(new TextPart("Test")))
108+
.build();
109+
110+
Message response = executor.execute(request).blockingGet();
111+
112+
assertThat(response).isNotNull();
113+
assertThat(response.getContextId()).isNotNull();
114+
assertThat(response.getContextId()).isNotEmpty();
115+
}
116+
117+
@Test
118+
public void execute_withProvidedContextId_preservesContext() {
119+
BaseAgent agent = createSimpleAgent();
120+
InMemorySessionService sessionService = new InMemorySessionService();
121+
122+
A2ASendMessageExecutor executor =
123+
new A2ASendMessageExecutor(
124+
agent,
125+
"test-app",
126+
Duration.ofSeconds(30),
127+
sessionService,
128+
new InMemoryArtifactService(),
129+
new InMemoryMemoryService());
130+
131+
String contextId = "my-custom-context";
132+
Message request =
133+
new Message.Builder()
134+
.messageId("msg-1")
135+
.contextId(contextId)
136+
.role(Message.Role.USER)
137+
.parts(List.of(new TextPart("Test")))
138+
.build();
139+
140+
Message response = executor.execute(request).blockingGet();
141+
142+
assertThat(response).isNotNull();
143+
assertThat(response.getContextId()).isEqualTo(contextId);
144+
}
145+
146+
@Test
147+
public void execute_multipleRequests_maintainsSession() {
148+
BaseAgent agent = createSimpleAgent();
149+
InMemorySessionService sessionService = new InMemorySessionService();
150+
151+
A2ASendMessageExecutor executor =
152+
new A2ASendMessageExecutor(
153+
agent,
154+
"test-app",
155+
Duration.ofSeconds(30),
156+
sessionService,
157+
new InMemoryArtifactService(),
158+
new InMemoryMemoryService());
159+
160+
String contextId = "persistent-context";
161+
162+
Message request1 =
163+
new Message.Builder()
164+
.messageId("msg-1")
165+
.contextId(contextId)
166+
.role(Message.Role.USER)
167+
.parts(List.of(new TextPart("First message")))
168+
.build();
169+
170+
Message response1 = executor.execute(request1).blockingGet();
171+
assertThat(response1.getContextId()).isEqualTo(contextId);
172+
173+
Message request2 =
174+
new Message.Builder()
175+
.messageId("msg-2")
176+
.contextId(contextId)
177+
.role(Message.Role.USER)
178+
.parts(List.of(new TextPart("Second message")))
179+
.build();
180+
181+
Message response2 = executor.execute(request2).blockingGet();
182+
assertThat(response2.getContextId()).isEqualTo(contextId);
183+
}
184+
185+
@Test
186+
public void execute_withoutRunnerConfig_throwsException() {
187+
InMemorySessionService sessionService = new InMemorySessionService();
188+
189+
A2ASendMessageExecutor executor = new A2ASendMessageExecutor(sessionService, "test-app");
190+
191+
Message request =
192+
new Message.Builder()
193+
.messageId("msg-1")
194+
.contextId("ctx-1")
195+
.role(Message.Role.USER)
196+
.parts(List.of(new TextPart("Test")))
197+
.build();
198+
199+
try {
200+
executor.execute(request).blockingGet();
201+
assertThat(false).isTrue();
202+
} catch (IllegalStateException e) {
203+
assertThat(e.getMessage()).contains("Runner-based handle invoked without configured runner");
204+
}
205+
}
206+
207+
@Test
208+
public void execute_errorInStrategy_returnsErrorResponse() {
209+
InMemorySessionService sessionService = new InMemorySessionService();
210+
211+
A2ASendMessageExecutor executor = new A2ASendMessageExecutor(sessionService, "test-app");
212+
213+
A2ASendMessageExecutor.AgentExecutionStrategy failingStrategy =
214+
(userId, sessionId, userContent, runConfig, invocationId) -> {
215+
return Single.error(new RuntimeException("Strategy failed"));
216+
};
217+
218+
Message request =
219+
new Message.Builder()
220+
.messageId("msg-1")
221+
.contextId("ctx-1")
222+
.role(Message.Role.USER)
223+
.parts(List.of(new TextPart("Test")))
224+
.build();
225+
226+
Message response = executor.execute(request, failingStrategy).blockingGet();
227+
228+
assertThat(response).isNotNull();
229+
assertThat(response.getParts()).isNotEmpty();
230+
assertThat(((TextPart) response.getParts().get(0)).getText()).contains("Error:");
231+
assertThat(((TextPart) response.getParts().get(0)).getText()).contains("Strategy failed");
232+
}
233+
234+
private BaseAgent createSimpleAgent() {
235+
return new BaseAgent("test", "test agent", ImmutableList.of(), null, null) {
236+
@Override
237+
protected Flowable<Event> runAsyncImpl(InvocationContext ctx) {
238+
return Flowable.just(
239+
Event.builder()
240+
.content(
241+
Content.builder()
242+
.role("model")
243+
.parts(
244+
ImmutableList.of(
245+
com.google.genai.types.Part.builder().text("Response").build()))
246+
.build())
247+
.build());
248+
}
249+
250+
@Override
251+
protected Flowable<Event> runLiveImpl(InvocationContext ctx) {
252+
return Flowable.empty();
253+
}
254+
};
255+
}
256+
}

0 commit comments

Comments
 (0)