This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.22.x by this push:
new c50f9259b307 CAMEL-25931: Preserve synchronous A2A task identity on
4.22.x (#28552)
c50f9259b307 is described below
commit c50f9259b3078444acdf8eff3e2c53b16c1695f2
Author: Luigi De Masi <[email protected]>
AuthorDate: Tue Oct 27 11:12:47 2026 +0100
CAMEL-25931: Preserve synchronous A2A task identity on 4.22.x (#28552)
Co-authored-by: OpenAI Codex <[email protected]>
Signed-off-by: Luigi De Masi <[email protected]>
---
.../apache/camel/component/a2a/A2AConsumer.java | 7 +-
.../camel/component/a2a/A2AConsumerTest.java | 120 +++++++++++++++++++++
2 files changed, 126 insertions(+), 1 deletion(-)
diff --git
a/components/camel-ai/camel-a2a/src/main/java/org/apache/camel/component/a2a/A2AConsumer.java
b/components/camel-ai/camel-a2a/src/main/java/org/apache/camel/component/a2a/A2AConsumer.java
index 1f7f35d2eda0..7ada2a4ad220 100644
---
a/components/camel-ai/camel-a2a/src/main/java/org/apache/camel/component/a2a/A2AConsumer.java
+++
b/components/camel-ai/camel-a2a/src/main/java/org/apache/camel/component/a2a/A2AConsumer.java
@@ -1235,7 +1235,12 @@ public class A2AConsumer extends DefaultConsumer {
request.getConfiguration());
if (response.isTaskResponse()) {
- Task task = response.getTask();
+ // Route processing may overwrite the identity headers when
calling another agent.
+ Task task = Task.builder(response.getTask())
+ .id(taskId)
+ .contextId(contextId)
+ .build();
+ response.setTask(task);
store.put(task.id(), task);
if (task.status() != null) {
store.notifySubscribers(task.id(),
diff --git
a/components/camel-ai/camel-a2a/src/test/java/org/apache/camel/component/a2a/A2AConsumerTest.java
b/components/camel-ai/camel-a2a/src/test/java/org/apache/camel/component/a2a/A2AConsumerTest.java
index 3bcc02d676cf..22307c42465b 100644
---
a/components/camel-ai/camel-a2a/src/test/java/org/apache/camel/component/a2a/A2AConsumerTest.java
+++
b/components/camel-ai/camel-a2a/src/test/java/org/apache/camel/component/a2a/A2AConsumerTest.java
@@ -22,6 +22,7 @@ import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@@ -67,12 +68,16 @@ import org.apache.camel.support.DefaultExchange;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -272,6 +277,121 @@ class A2AConsumerTest {
assertEquals("ctx-789", response.getTask().contextId());
}
+ @ParameterizedTest
+ @CsvSource({
+ "rest, false, false", "rest, false, true", "rest, true, false",
"rest, true, true",
+ "jsonrpc, false, false", "jsonrpc, false, true", "jsonrpc, true,
false", "jsonrpc, true, true"
+ })
+ void synchronousTaskRetainsConsumerIdentity(String binding, boolean
existingTask, boolean removeHeaders) throws Exception {
+ A2AConfiguration config = new A2AConfiguration();
+ config.setProtocolBinding(binding);
+ A2AEndpoint endpoint = createEndpoint("a2a:identity-agent",
"identity-agent", config);
+ A2ATaskStore store = endpoint.getTaskStore();
+ if (existingTask) {
+ store.put("consumer-task",
Task.builder().id("consumer-task").contextId("consumer-context")
+ .status(new TaskStatus(TaskState.INPUT_REQUIRED)).build());
+ }
+ AtomicReference<String> taskId = new AtomicReference<>();
+ AtomicReference<String> contextId = new AtomicReference<>();
+ List<String> notifiedTaskIds = new ArrayList<>();
+ List<TaskStatusUpdateEvent> notifications = new ArrayList<>();
+ A2AConsumer consumer = new A2AConsumer(endpoint, exchange -> {
+ taskId.set(exchange.getMessage().getHeader(A2AConstants.TASK_ID,
String.class));
+
contextId.set(exchange.getMessage().getHeader(A2AConstants.CONTEXT_ID,
String.class));
+ store.addSubscriber(taskId.get(), (id, event) -> {
+ notifiedTaskIds.add(id);
+ notifications.add(event.getStatusUpdate());
+ });
+ if (removeHeaders) {
+ exchange.getMessage().removeHeader(A2AConstants.TASK_ID);
+ exchange.getMessage().removeHeader(A2AConstants.CONTEXT_ID);
+ } else {
+ // Simulate the identity headers written by a downstream A2A
producer.
+ exchange.getMessage().setHeader(A2AConstants.TASK_ID,
"peer-task");
+ exchange.getMessage().setHeader(A2AConstants.CONTEXT_ID,
"peer-context");
+ }
+ exchange.getMessage().setHeader(A2AConstants.TASK_STATE,
"TASK_STATE_FAILED");
+ exchange.getMessage().setBody("Agent response");
+ });
+
+ SendMessageRequest request = new SendMessageRequest();
+
request.setMessage(Message.builder().messageId("identity-request").role(Message.Role.ROLE_USER)
+ .taskId(existingTask ? "consumer-task" :
null).parts(List.of(new TextPart("Hello"))).build());
+ if (existingTask) {
+ SendMessageConfiguration configuration = new
SendMessageConfiguration();
+ configuration.setHistoryLength(0);
+ request.setConfiguration(configuration);
+ }
+ SendMessageResponse response = sendSynchronousMessage(consumer,
binding, request);
+
+ assertTrue(response.isTaskResponse());
+ Task task = response.getTask();
+ assertNotNull(taskId.get());
+ assertNotNull(contextId.get());
+ assertEquals(taskId.get(), task.id());
+ assertEquals(contextId.get(), task.contextId());
+ if (existingTask) {
+ assertEquals("consumer-task", task.id());
+ assertEquals("consumer-context", task.contextId());
+ }
+ assertEquals(TaskState.FAILED, task.status().state());
+ assertEquals(List.of(new TextPart("Agent response")),
task.status().message().parts());
+ assertEquals(existingTask ? 0 : 1, task.history().size());
+ assertEquals(task, store.get(task.id()));
+ assertNull(store.get("peer-task"));
+ assertEquals(List.of(task.id()), notifiedTaskIds);
+ assertEquals(1, notifications.size());
+ TaskStatusUpdateEvent notification = notifications.get(0);
+ assertEquals(task.id(), notification.taskId());
+ assertEquals(task.contextId(), notification.contextId());
+ assertEquals(task.status(), notification.status());
+ assertTrue(notification.isFinal());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "rest", "jsonrpc" })
+ void synchronousMessageReplyStillUsesLegacyResponseType(String binding)
throws Exception {
+ A2AConfiguration config = new A2AConfiguration();
+ config.setProtocolBinding(binding);
+ A2AEndpoint endpoint = createEndpoint("a2a:message-agent",
"message-agent", config);
+ AtomicReference<String> taskId = new AtomicReference<>();
+ A2AConsumer consumer = new A2AConsumer(endpoint, exchange -> {
+ taskId.set(exchange.getMessage().getHeader(A2AConstants.TASK_ID,
String.class));
+ exchange.getMessage().setHeader(A2AConstants.TASK_ID, "peer-task");
+ exchange.getMessage().setHeader(A2AConstants.CONTEXT_ID,
"peer-context");
+ exchange.getMessage().setHeader(A2AConstants.RESPONSE_TYPE,
"message");
+ exchange.getMessage().setBody("Message response");
+ });
+ SendMessageRequest request = new SendMessageRequest();
+
request.setMessage(Message.builder().messageId("message-request").role(Message.Role.ROLE_USER)
+ .parts(List.of(new TextPart("Hello"))).build());
+
+ SendMessageResponse response = sendSynchronousMessage(consumer,
binding, request);
+
+ assertTrue(response.isMessageResponse());
+ assertEquals(List.of(new TextPart("Message response")),
response.getMessage().parts());
+ assertNotNull(taskId.get());
+ assertNull(endpoint.getTaskStore().get(taskId.get()));
+ assertNull(endpoint.getTaskStore().get("peer-task"));
+ }
+
+ private SendMessageResponse sendSynchronousMessage(
+ A2AConsumer consumer, String binding, SendMessageRequest request)
+ throws Exception {
+ Exchange exchange = new DefaultExchange(context);
+ if ("jsonrpc".equals(binding)) {
+
exchange.getMessage().setBody(OBJECT_MAPPER.writeValueAsBytes(Map.of(
+ "jsonrpc", "2.0", "id", 1, "method", "SendMessage",
"params", request)));
+ consumer.handleJsonRpcDispatch(exchange);
+ JsonNode response =
OBJECT_MAPPER.readTree(exchange.getMessage().getBody(byte[].class));
+ assertNull(response.get("error"), response.toString());
+ return OBJECT_MAPPER.treeToValue(response.get("result"),
SendMessageResponse.class);
+ }
+
exchange.getMessage().setBody(OBJECT_MAPPER.writeValueAsBytes(request));
+ consumer.handleSendMessage(exchange);
+ return
OBJECT_MAPPER.readValue(exchange.getMessage().getBody(byte[].class),
SendMessageResponse.class);
+ }
+
@Test
void handleSendMessageWithPayloadDataFormat() throws Exception {
A2AConfiguration config = new A2AConfiguration();