This is the Workbench Agent itself: a Spring Boot service that exposes A2A-T endpoints
- * (message:send, message:stream) via the {@code spring-boot-a2a-starter} auto-configuration. The
+ * (message:send, message:stream) via the {@code spring-boot-starter} auto-configuration. The
* business logic lives in {@link
* dev.openan.workflow.engine.examples.workbench.SpringWorkbenchExecutor} (implements {@code
* AgentExecutor}).
diff --git a/samples/src/test/java/dev/openan/workflow/engine/examples/testsupport/OfflineA2ATLlmClient.java b/samples/src/test/java/dev/openan/workflow/engine/examples/testsupport/OfflineA2ATLlmClient.java
index 02eef70..d942270 100644
--- a/samples/src/test/java/dev/openan/workflow/engine/examples/testsupport/OfflineA2ATLlmClient.java
+++ b/samples/src/test/java/dev/openan/workflow/engine/examples/testsupport/OfflineA2ATLlmClient.java
@@ -206,6 +206,7 @@ private static String label(String source, String name) {
/** Only actual inputs, never explanatory examples or template guidance. */
private static String validationInput(String prompt) {
+ prompt = prompt.replace("\r\n", "\n").replace('\r', '\n');
int start = prompt.indexOf("[input]\n");
if (start >= 0) {
start += "[input]\n".length();
diff --git a/spring-boot-starter/pom.xml b/spring-boot-starter/pom.xml
index 4a96494..4340889 100644
--- a/spring-boot-starter/pom.xml
+++ b/spring-boot-starter/pom.xml
@@ -12,10 +12,10 @@
The supplied client already applies TLS, protocol observation, and A2A error-envelope
+ * handling. Subclasses may decorate it to integrate an HTTP gateway (for example, URI rewriting
+ * or gateway-specific headers) while preserving those behaviors. The default implementation
+ * returns the client unchanged.
+ *
+ * @param httpClient fully configured transport client
+ * @return the client to use; never {@code null}
+ */
+ protected A2AHttpClient customizeHttpClient(A2AHttpClient httpClient) {
+ return Objects.requireNonNull(httpClient, "httpClient");
}
private record StreamClientKey(String agentName, String contextId) {}
diff --git a/workflow-engine/src/test/java/dev/openan/workflow/engine/client/RemoteA2AErrorResponseTest.java b/workflow-engine/src/test/java/dev/openan/workflow/engine/client/RemoteA2AErrorResponseTest.java
index c157baf..04be1a9 100644
--- a/workflow-engine/src/test/java/dev/openan/workflow/engine/client/RemoteA2AErrorResponseTest.java
+++ b/workflow-engine/src/test/java/dev/openan/workflow/engine/client/RemoteA2AErrorResponseTest.java
@@ -30,6 +30,7 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
+import org.a2aproject.sdk.client.http.A2AHttpClient;
import org.a2aproject.sdk.client.http.JdkA2AHttpClient;
import org.a2aproject.sdk.spec.*;
import org.junit.jupiter.api.Test;
@@ -191,4 +192,51 @@ void nonStreamingResponsesPreserveTheStandardA2AError() throws Exception {
server.stop(0);
}
}
+
+ @Test
+ void runtimeUsesTheHttpClientCustomizationHookWithoutBypassingErrorDetection() throws Exception {
+ HttpServer server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
+ server.createContext("/", exchange -> {
+ exchange.getRequestBody().readAllBytes();
+ byte[] body = ("data: " + error(400) + "\n\n").getBytes(StandardCharsets.UTF_8);
+ exchange.getResponseHeaders().set("Content-Type", "text/event-stream");
+ exchange.sendResponseHeaders(200, 0);
+ exchange.getResponseBody().write(body);
+ exchange.close();
+ });
+ server.start();
+ var runtime = new CustomizingRuntime();
+ try {
+ AgentCard card = AgentCard.builder().name("customized-runtime").description("test").version("1")
+ .capabilities(AgentCapabilities.builder().streaming(true).build())
+ .defaultInputModes(List.of("text/plain")).defaultOutputModes(List.of("text/plain"))
+ .skills(List.of()).supportedInterfaces(List.of(new AgentInterface("HTTP+JSON",
+ "http://127.0.0.1:" + server.getAddress().getPort() + "/a2a/json"))).build();
+ MessageSendParams params = MessageSendParams.builder().message(Message.builder()
+ .role(Message.Role.ROLE_USER).messageId("test").parts(new TextPart("diagnose")).build()).build();
+
+ var thrown = assertThrows(RuntimeException.class,
+ () -> runtime.sendMessage(card, params, null, ignored -> {}, null));
+
+ assertNotNull(RemoteA2AErrorException.findIn(thrown));
+ assertEquals(1, runtime.customizations.get());
+ } finally {
+ runtime.close();
+ server.stop(0);
+ }
+ }
+
+ private static final class CustomizingRuntime extends DefaultA2AJavaClientRuntime {
+ private final AtomicInteger customizations = new AtomicInteger();
+
+ private CustomizingRuntime() {
+ super(true, null, 5, "HTTP+JSON");
+ }
+
+ @Override
+ protected A2AHttpClient customizeHttpClient(A2AHttpClient httpClient) {
+ customizations.incrementAndGet();
+ return super.customizeHttpClient(httpClient);
+ }
+ }
}