From a402ae044ac31f3a5161d01e280cce753afed555 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Wed, 11 Feb 2026 12:02:05 +0000 Subject: [PATCH 1/5] feat: Make RequestContext taskId and contextId nullable. This is possible by moving the generation of IDs when not set into the builder, and making sure the builder is used. --- .../server/agentexecution/RequestContext.java | 199 +++++++++++------- .../io/a2a/server/tasks/AgentEmitter.java | 2 +- .../agentexecution/RequestContextTest.java | 124 ++++++++--- 3 files changed, 229 insertions(+), 96 deletions(-) diff --git a/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java b/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java index c82df7846..f94f57ef6 100644 --- a/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java +++ b/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java @@ -74,50 +74,52 @@ */ public class RequestContext { - private @Nullable MessageSendParams params; - private @Nullable String taskId; - private @Nullable String contextId; - private @Nullable Task task; - private List relatedTasks; + private final @Nullable MessageSendParams params; + private final String taskId; + private final String contextId; + private final @Nullable Task task; + private final List relatedTasks; private final @Nullable ServerCallContext callContext; - public RequestContext( + /** + * Constructor with all fields already validated and initialized. + *

+ * Note: Use {@link Builder} instead of calling this constructor directly. + * The builder handles ID generation and validation. + *

+ * + * @param params the message send parameters (can be null for cancel operations) + * @param taskId the task identifier (must not be null) + * @param contextId the context identifier (must not be null) + * @param task the existing task state (null for new conversations) + * @param relatedTasks other tasks in the same context (must not be null, can be empty) + * @param callContext the server call context (can be null) + */ + private RequestContext( @Nullable MessageSendParams params, - @Nullable String taskId, - @Nullable String contextId, + String taskId, + String contextId, @Nullable Task task, - @Nullable List relatedTasks, - @Nullable ServerCallContext callContext) throws InvalidParamsError { + List relatedTasks, + @Nullable ServerCallContext callContext) { this.params = params; this.taskId = taskId; this.contextId = contextId; this.task = task; - this.relatedTasks = relatedTasks == null ? new ArrayList<>() : relatedTasks; + this.relatedTasks = relatedTasks; this.callContext = callContext; - - // If the taskId and contextId were specified, they must match the params - if (params != null) { - if (taskId != null && !taskId.equals(params.message().taskId())) { - throw new InvalidParamsError("bad task id"); - } - this.taskId = checkOrGenerateTaskId(); - if (contextId != null && !contextId.equals(params.message().contextId())) { - throw new InvalidParamsError("bad context id"); - } - this.contextId = checkOrGenerateContextId(); - } } /** * Returns the task identifier. *

- * This is auto-generated (UUID) if not provided by the client in the message parameters. - * It can be null if the context was not created from message parameters. + * This is auto-generated (UUID) by the builder if not provided by the client + * in the message parameters. This value is never null. *

* - * @return the task ID + * @return the task ID (never null) */ - public @Nullable String getTaskId() { + public String getTaskId() { return taskId; } @@ -125,13 +127,13 @@ public RequestContext( * Returns the conversation context identifier. *

* Conversation contexts group related tasks together (e.g., multiple tasks - * in the same user session). This is auto-generated (UUID) if not provided by the client - * in the message parameters. It can be null if the context was not created from message parameters. + * in the same user session). This is auto-generated (UUID) by the builder if + * not provided by the client in the message parameters. This value is never null. *

* - * @return the context ID + * @return the context ID (never null) */ - public @Nullable String getContextId() { + public String getContextId() { return contextId; } @@ -240,48 +242,20 @@ public String getUserInput(String delimiter) { return getMessageText(params.message(), delimiter); } + /** + * Attaches a related task to this context. + *

+ * This is primarily used by the framework to populate related tasks after + * construction. Agent implementations should use {@link #getRelatedTasks()} + * to access related tasks. + *

+ * + * @param task the task to attach + */ public void attachRelatedTask(Task task) { relatedTasks.add(task); } - private @Nullable String checkOrGenerateTaskId() { - if (params == null) { - return taskId; - } - if (taskId == null && params.message().taskId() == null) { - // Message is immutable, create new one with generated taskId - String generatedTaskId = UUID.randomUUID().toString(); - Message updatedMessage = Message.builder(params.message()) - .taskId(generatedTaskId) - .build(); - params = new MessageSendParams(updatedMessage, params.configuration(), params.metadata()); - return generatedTaskId; - } - if (params.message().taskId() != null) { - return params.message().taskId(); - } - return taskId; - } - - private @Nullable String checkOrGenerateContextId() { - if (params == null) { - return contextId; - } - if (contextId == null && params.message().contextId() == null) { - // Message is immutable, create new one with generated contextId - String generatedContextId = UUID.randomUUID().toString(); - Message updatedMessage = Message.builder(params.message()) - .contextId(generatedContextId) - .build(); - params = new MessageSendParams(updatedMessage, params.configuration(), params.metadata()); - return generatedContextId; - } - if (params.message().contextId() != null) { - return params.message().contextId(); - } - return contextId; - } - private String getMessageText(Message message, String delimiter) { List textParts = getTextParts(message.parts()); return String.join(delimiter, textParts); @@ -295,6 +269,18 @@ private List getTextParts(List> parts) { .collect(Collectors.toList()); } + /** + * Builder for creating {@link RequestContext} instances. + *

+ * The builder handles ID generation and validation automatically: + *

    + *
  • TaskId and ContextId are auto-generated (UUID) if not provided
  • + *
  • IDs are validated against message parameters if both are present
  • + *
  • Message parameters are updated with generated IDs
  • + *
  • Related tasks list is initialized to empty list if null
  • + *
+ *

+ */ public static class Builder { private @Nullable MessageSendParams params; private @Nullable String taskId; @@ -357,8 +343,79 @@ public Builder setServerCallContext(@Nullable ServerCallContext serverCallContex return serverCallContext; } - public RequestContext build() { - return new RequestContext(params, taskId, contextId, task, relatedTasks, serverCallContext); + /** + * Builds the RequestContext with ID generation and validation. + * + * @return the constructed RequestContext + * @throws InvalidParamsError if taskId or contextId don't match message parameters + */ + public RequestContext build() throws InvalidParamsError { + // 1. Initialize relatedTasks to empty list if null + List finalRelatedTasks = relatedTasks != null ? relatedTasks : new ArrayList<>(); + + // 2. Determine final IDs and params + String finalTaskId = taskId; + String finalContextId = contextId; + MessageSendParams finalParams = params; + + if (params != null) { + // Validate IDs match message if both are present + if (finalTaskId != null && !finalTaskId.equals(params.message().taskId())) { + throw new InvalidParamsError("bad task id"); + } + if (finalContextId != null && !finalContextId.equals(params.message().contextId())) { + throw new InvalidParamsError("bad context id"); + } + + // Determine what we need to do with IDs + String messageTaskId = params.message().taskId(); + String messageContextId = params.message().contextId(); + + boolean needGenerateTaskId = (finalTaskId == null && messageTaskId == null); + boolean needGenerateContextId = (finalContextId == null && messageContextId == null); + boolean needUpdateMessage = needGenerateTaskId || needGenerateContextId; + + if (needUpdateMessage) { + // Need to generate IDs and update the message + Message.Builder msgBuilder = Message.builder(params.message()); + + if (needGenerateTaskId) { + finalTaskId = UUID.randomUUID().toString(); + msgBuilder.taskId(finalTaskId); + } else { + // Use existing ID from builder or message + finalTaskId = finalTaskId != null ? finalTaskId : messageTaskId; + } + + if (needGenerateContextId) { + finalContextId = UUID.randomUUID().toString(); + msgBuilder.contextId(finalContextId); + } else { + // Use existing ID from builder or message + finalContextId = finalContextId != null ? finalContextId : messageContextId; + } + + finalParams = new MessageSendParams(msgBuilder.build(), + params.configuration(), params.metadata()); + } else { + // No generation needed - use existing IDs + finalTaskId = finalTaskId != null ? finalTaskId : messageTaskId; + finalContextId = finalContextId != null ? finalContextId : messageContextId; + } + } + + // 3. Final ID generation if still null (no params case or params didn't have IDs) + if (finalTaskId == null) { + finalTaskId = UUID.randomUUID().toString(); + } + if (finalContextId == null) { + finalContextId = UUID.randomUUID().toString(); + } + + // 4. At this point, IDs are guaranteed non-null + // Call constructor with finalized values + return new RequestContext(finalParams, finalTaskId, finalContextId, + task, finalRelatedTasks, serverCallContext); } } diff --git a/server-common/src/main/java/io/a2a/server/tasks/AgentEmitter.java b/server-common/src/main/java/io/a2a/server/tasks/AgentEmitter.java index 7c6c97f20..78f6ac2c3 100644 --- a/server-common/src/main/java/io/a2a/server/tasks/AgentEmitter.java +++ b/server-common/src/main/java/io/a2a/server/tasks/AgentEmitter.java @@ -108,7 +108,7 @@ public class AgentEmitter { */ public AgentEmitter(RequestContext context, EventQueue eventQueue) { this.eventQueue = eventQueue; - this.taskId = Assert.checkNotNullParam("taskId",context.getTaskId()); + this.taskId = Assert.checkNotNullParam("taskId",context.getTaskId()); this.contextId = Assert.checkNotNullParam("contextId",context.getContextId()); } diff --git a/server-common/src/test/java/io/a2a/server/agentexecution/RequestContextTest.java b/server-common/src/test/java/io/a2a/server/agentexecution/RequestContextTest.java index ba692d97c..d9c86c314 100644 --- a/server-common/src/test/java/io/a2a/server/agentexecution/RequestContextTest.java +++ b/server-common/src/test/java/io/a2a/server/agentexecution/RequestContextTest.java @@ -35,10 +35,11 @@ private static MessageSendConfiguration defaultConfiguration() { @Test public void testInitWithoutParams() { - RequestContext context = new RequestContext(null, null, null, null, null, null); + RequestContext context = new RequestContext.Builder().build(); + assertNull(context.getMessage()); - assertNull(context.getTaskId()); - assertNull(context.getContextId()); + assertNotNull(context.getTaskId()); // Generated UUID + assertNotNull(context.getContextId()); // Generated UUID assertNull(context.getTask()); assertTrue(context.getRelatedTasks().isEmpty()); } @@ -56,7 +57,9 @@ public void testInitWithParamsNoIds() { .thenReturn(taskId) .thenReturn(contextId); - RequestContext context = new RequestContext(mockParams, null, null, null, null, null); + RequestContext context = new RequestContext.Builder() + .setParams(mockParams) + .build(); // getMessage() returns a new Message with generated IDs, not the original assertNotNull(context.getMessage()); @@ -73,7 +76,10 @@ public void testInitWithTaskId() { var mockMessage = Message.builder().role(Message.Role.USER).parts(List.of(new TextPart(""))).taskId(taskId).build(); var mockParams = MessageSendParams.builder().message(mockMessage).configuration(defaultConfiguration()).build(); - RequestContext context = new RequestContext(mockParams, taskId, null, null, null, null); + RequestContext context = new RequestContext.Builder() + .setParams(mockParams) + .setTaskId(taskId) + .build(); assertEquals(taskId, context.getTaskId()); assertEquals(taskId, mockParams.message().taskId()); @@ -84,7 +90,11 @@ public void testInitWithContextId() { String contextId = "context-456"; var mockMessage = Message.builder().role(Message.Role.USER).parts(List.of(new TextPart(""))).contextId(contextId).build(); var mockParams = MessageSendParams.builder().message(mockMessage).configuration(defaultConfiguration()).build(); - RequestContext context = new RequestContext(mockParams, null, contextId, null, null, null); + + RequestContext context = new RequestContext.Builder() + .setParams(mockParams) + .setContextId(contextId) + .build(); assertEquals(contextId, context.getContextId()); assertEquals(contextId, mockParams.message().contextId()); @@ -96,7 +106,12 @@ public void testInitWithBothIds() { String contextId = "context-456"; var mockMessage = Message.builder().role(Message.Role.USER).parts(List.of(new TextPart(""))).taskId(taskId).contextId(contextId).build(); var mockParams = MessageSendParams.builder().message(mockMessage).configuration(defaultConfiguration()).build(); - RequestContext context = new RequestContext(mockParams, taskId, contextId, null, null, null); + + RequestContext context = new RequestContext.Builder() + .setParams(mockParams) + .setTaskId(taskId) + .setContextId(contextId) + .build(); assertEquals(taskId, context.getTaskId()); assertEquals(taskId, mockParams.message().taskId()); @@ -110,14 +125,17 @@ public void testInitWithTask() { var mockTask = Task.builder().id("task-123").contextId("context-456").status(new TaskStatus(TaskState.COMPLETED)).build(); var mockParams = MessageSendParams.builder().message(mockMessage).configuration(defaultConfiguration()).build(); - RequestContext context = new RequestContext(mockParams, null, null, mockTask, null, null); + RequestContext context = new RequestContext.Builder() + .setParams(mockParams) + .setTask(mockTask) + .build(); assertEquals(mockTask, context.getTask()); } @Test public void testGetUserInputNoParams() { - RequestContext context = new RequestContext(null, null, null, null, null, null); + RequestContext context = new RequestContext.Builder().build(); assertEquals("", context.getUserInput(null)); } @@ -125,7 +143,7 @@ public void testGetUserInputNoParams() { public void testAttachRelatedTask() { var mockTask = Task.builder().id("task-123").contextId("context-456").status(new TaskStatus(TaskState.COMPLETED)).build(); - RequestContext context = new RequestContext(null, null, null, null, null, null); + RequestContext context = new RequestContext.Builder().build(); assertEquals(0, context.getRelatedTasks().size()); context.attachRelatedTask(mockTask); @@ -144,7 +162,9 @@ public void testCheckOrGenerateTaskIdWithExistingTaskId() { var mockMessage = Message.builder().role(Message.Role.USER).parts(List.of(new TextPart(""))).taskId(existingId).build(); var mockParams = MessageSendParams.builder().message(mockMessage).configuration(defaultConfiguration()).build(); - RequestContext context = new RequestContext(mockParams, null, null, null, null, null); + RequestContext context = new RequestContext.Builder() + .setParams(mockParams) + .build(); assertEquals(existingId, context.getTaskId()); assertEquals(existingId, mockParams.message().taskId()); @@ -157,7 +177,9 @@ public void testCheckOrGenerateContextIdWithExistingContextId() { var mockMessage = Message.builder().role(Message.Role.USER).parts(List.of(new TextPart(""))).contextId(existingId).build(); var mockParams = MessageSendParams.builder().message(mockMessage).configuration(defaultConfiguration()).build(); - RequestContext context = new RequestContext(mockParams, null, null, null, null, null); + RequestContext context = new RequestContext.Builder() + .setParams(mockParams) + .build(); assertEquals(existingId, context.getContextId()); assertEquals(existingId, mockParams.message().contextId()); @@ -170,7 +192,11 @@ public void testInitRaisesErrorOnTaskIdMismatch() { var mockTask = Task.builder().id("task-123").contextId("context-456").status(new TaskStatus(TaskState.COMPLETED)).build(); InvalidParamsError error = assertThrows(InvalidParamsError.class, () -> - new RequestContext(mockParams, "wrong-task-id", null, mockTask, null, null)); + new RequestContext.Builder() + .setParams(mockParams) + .setTaskId("wrong-task-id") + .setTask(mockTask) + .build()); assertTrue(error.getMessage().contains("bad task id")); } @@ -182,7 +208,12 @@ public void testInitRaisesErrorOnContextIdMismatch() { var mockTask = Task.builder().id("task-123").contextId("context-456").status(new TaskStatus(TaskState.COMPLETED)).build(); InvalidParamsError error = assertThrows(InvalidParamsError.class, () -> - new RequestContext(mockParams, mockTask.id(), "wrong-context-id", mockTask, null, null)); + new RequestContext.Builder() + .setParams(mockParams) + .setTaskId(mockTask.id()) + .setContextId("wrong-context-id") + .setTask(mockTask) + .build()); assertTrue(error.getMessage().contains("bad context id")); } @@ -195,7 +226,9 @@ public void testWithRelatedTasksProvided() { relatedTasks.add(mockTask); relatedTasks.add(mock(Task.class)); - RequestContext context = new RequestContext(null, null, null, null, relatedTasks, null); + RequestContext context = new RequestContext.Builder() + .setRelatedTasks(relatedTasks) + .build(); assertEquals(relatedTasks, context.getRelatedTasks()); assertEquals(2, context.getRelatedTasks().size()); @@ -203,7 +236,7 @@ public void testWithRelatedTasksProvided() { @Test public void testMessagePropertyWithoutParams() { - RequestContext context = new RequestContext(null, null, null, null, null, null); + RequestContext context = new RequestContext.Builder().build(); assertNull(context.getMessage()); } @@ -212,7 +245,10 @@ public void testMessagePropertyWithParams() { var mockMessage = Message.builder().role(Message.Role.USER).parts(List.of(new TextPart(""))).build(); var mockParams = MessageSendParams.builder().message(mockMessage).configuration(defaultConfiguration()).build(); - RequestContext context = new RequestContext(mockParams, null, null, null, null, null); + RequestContext context = new RequestContext.Builder() + .setParams(mockParams) + .build(); + // getMessage() returns a new Message with generated IDs, not the original assertNotNull(context.getMessage()); assertEquals(mockMessage.role(), context.getMessage().role()); @@ -228,7 +264,9 @@ public void testInitWithExistingIdsInMessage() { .taskId(existingTaskId).contextId(existingContextId).build(); var mockParams = MessageSendParams.builder().message(mockMessage).configuration(defaultConfiguration()).build(); - RequestContext context = new RequestContext(mockParams, null, null, null, null, null); + RequestContext context = new RequestContext.Builder() + .setParams(mockParams) + .build(); assertEquals(existingTaskId, context.getTaskId()); assertEquals(existingContextId, context.getContextId()); @@ -240,8 +278,11 @@ public void testInitWithTaskIdAndExistingTaskIdMatch() { var mockParams = MessageSendParams.builder().message(mockMessage).configuration(defaultConfiguration()).build(); var mockTask = Task.builder().id("task-123").contextId("context-456").status(new TaskStatus(TaskState.COMPLETED)).build(); - - RequestContext context = new RequestContext(mockParams, mockTask.id(), null, mockTask, null, null); + RequestContext context = new RequestContext.Builder() + .setParams(mockParams) + .setTaskId(mockTask.id()) + .setTask(mockTask) + .build(); assertEquals(mockTask.id(), context.getTaskId()); assertEquals(mockTask, context.getTask()); @@ -253,8 +294,12 @@ public void testInitWithContextIdAndExistingContextIdMatch() { var mockParams = MessageSendParams.builder().message(mockMessage).configuration(defaultConfiguration()).build(); var mockTask = Task.builder().id("task-123").contextId("context-456").status(new TaskStatus(TaskState.COMPLETED)).build(); - - RequestContext context = new RequestContext(mockParams, mockTask.id(), mockTask.contextId(), mockTask, null, null); + RequestContext context = new RequestContext.Builder() + .setParams(mockParams) + .setTaskId(mockTask.id()) + .setContextId(mockTask.contextId()) + .setTask(mockTask) + .build(); assertEquals(mockTask.contextId(), context.getContextId()); assertEquals(mockTask, context.getTask()); @@ -265,7 +310,10 @@ void testMessageBuilderGeneratesId() { var mockMessage = Message.builder().role(Message.Role.USER).parts(List.of(new TextPart(""))).build(); var mockParams = MessageSendParams.builder().message(mockMessage).configuration(defaultConfiguration()).build(); - RequestContext context = new RequestContext(mockParams, null, null, null, null, null); + RequestContext context = new RequestContext.Builder() + .setParams(mockParams) + .build(); + assertNotNull(mockMessage.messageId()); assertFalse(mockMessage.messageId().isEmpty()); } @@ -275,7 +323,35 @@ void testMessageBuilderUsesProvidedId() { var mockMessage = Message.builder().messageId("123").role(Message.Role.USER).parts(List.of(new TextPart(""))).build(); var mockParams = MessageSendParams.builder().message(mockMessage).configuration(defaultConfiguration()).build(); - RequestContext context = new RequestContext(mockParams, null, null, null, null, null); + RequestContext context = new RequestContext.Builder() + .setParams(mockParams) + .build(); + assertEquals("123", mockMessage.messageId()); } + + @Test + public void testBuilderGeneratesIdsWhenNoParams() { + RequestContext context = new RequestContext.Builder() + .build(); + + assertNotNull(context.getTaskId()); + assertNotNull(context.getContextId()); + assertFalse(context.getTaskId().isEmpty()); + assertFalse(context.getContextId().isEmpty()); + } + + @Test + public void testBuilderPreservesProvidedIdsWhenNoParams() { + String providedTaskId = "my-task-id"; + String providedContextId = "my-context-id"; + + RequestContext context = new RequestContext.Builder() + .setTaskId(providedTaskId) + .setContextId(providedContextId) + .build(); + + assertEquals(providedTaskId, context.getTaskId()); + assertEquals(providedContextId, context.getContextId()); + } } From d67da17475fd0866c3a577c5599fd4f4ad051b43 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Wed, 11 Feb 2026 12:45:38 +0000 Subject: [PATCH 2/5] Review fixes --- .../server/agentexecution/RequestContext.java | 87 +++++++++---------- .../agentexecution/RequestContextTest.java | 49 +++++++++++ 2 files changed, 89 insertions(+), 47 deletions(-) diff --git a/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java b/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java index f94f57ef6..cf2cb2928 100644 --- a/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java +++ b/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java @@ -273,13 +273,13 @@ private List getTextParts(List> parts) { * Builder for creating {@link RequestContext} instances. *

* The builder handles ID generation and validation automatically: + *

*
    *
  • TaskId and ContextId are auto-generated (UUID) if not provided
  • *
  • IDs are validated against message parameters if both are present
  • *
  • Message parameters are updated with generated IDs
  • *
  • Related tasks list is initialized to empty list if null
  • *
- *

*/ public static class Builder { private @Nullable MessageSendParams params; @@ -353,66 +353,59 @@ public RequestContext build() throws InvalidParamsError { // 1. Initialize relatedTasks to empty list if null List finalRelatedTasks = relatedTasks != null ? relatedTasks : new ArrayList<>(); - // 2. Determine final IDs and params + // 2. Determine final IDs using coalesce pattern: builder → message → generate String finalTaskId = taskId; String finalContextId = contextId; MessageSendParams finalParams = params; if (params != null) { - // Validate IDs match message if both are present - if (finalTaskId != null && !finalTaskId.equals(params.message().taskId())) { + String messageTaskId = params.message().taskId(); + String messageContextId = params.message().contextId(); + + // Validate: if both builder and message provide an ID, they must match + if (finalTaskId != null && messageTaskId != null && !finalTaskId.equals(messageTaskId)) { throw new InvalidParamsError("bad task id"); } - if (finalContextId != null && !finalContextId.equals(params.message().contextId())) { + if (finalContextId != null && messageContextId != null && !finalContextId.equals(messageContextId)) { throw new InvalidParamsError("bad context id"); } - // Determine what we need to do with IDs - String messageTaskId = params.message().taskId(); - String messageContextId = params.message().contextId(); - - boolean needGenerateTaskId = (finalTaskId == null && messageTaskId == null); - boolean needGenerateContextId = (finalContextId == null && messageContextId == null); - boolean needUpdateMessage = needGenerateTaskId || needGenerateContextId; - - if (needUpdateMessage) { - // Need to generate IDs and update the message - Message.Builder msgBuilder = Message.builder(params.message()); - - if (needGenerateTaskId) { - finalTaskId = UUID.randomUUID().toString(); - msgBuilder.taskId(finalTaskId); - } else { - // Use existing ID from builder or message - finalTaskId = finalTaskId != null ? finalTaskId : messageTaskId; - } - - if (needGenerateContextId) { - finalContextId = UUID.randomUUID().toString(); - msgBuilder.contextId(finalContextId); - } else { - // Use existing ID from builder or message - finalContextId = finalContextId != null ? finalContextId : messageContextId; - } - - finalParams = new MessageSendParams(msgBuilder.build(), + // Coalesce: prefer builder ID, fall back to message ID, generate if both null + if (finalTaskId == null) { + finalTaskId = messageTaskId; + } + if (finalTaskId == null) { + finalTaskId = UUID.randomUUID().toString(); + } + + if (finalContextId == null) { + finalContextId = messageContextId; + } + if (finalContextId == null) { + finalContextId = UUID.randomUUID().toString(); + } + + // Update message if final IDs differ from message IDs + // This ensures getMessage().taskId() matches getTaskId() + if (!finalTaskId.equals(messageTaskId) || !finalContextId.equals(messageContextId)) { + Message updatedMessage = Message.builder(params.message()) + .taskId(finalTaskId) + .contextId(finalContextId) + .build(); + finalParams = new MessageSendParams(updatedMessage, params.configuration(), params.metadata()); - } else { - // No generation needed - use existing IDs - finalTaskId = finalTaskId != null ? finalTaskId : messageTaskId; - finalContextId = finalContextId != null ? finalContextId : messageContextId; } - } - - // 3. Final ID generation if still null (no params case or params didn't have IDs) - if (finalTaskId == null) { - finalTaskId = UUID.randomUUID().toString(); - } - if (finalContextId == null) { - finalContextId = UUID.randomUUID().toString(); + } else { + // No params - generate IDs if not provided by builder + if (finalTaskId == null) { + finalTaskId = UUID.randomUUID().toString(); + } + if (finalContextId == null) { + finalContextId = UUID.randomUUID().toString(); + } } - // 4. At this point, IDs are guaranteed non-null + // 3. At this point, IDs are guaranteed non-null and consistent // Call constructor with finalized values return new RequestContext(finalParams, finalTaskId, finalContextId, task, finalRelatedTasks, serverCallContext); diff --git a/server-common/src/test/java/io/a2a/server/agentexecution/RequestContextTest.java b/server-common/src/test/java/io/a2a/server/agentexecution/RequestContextTest.java index d9c86c314..e90f6d698 100644 --- a/server-common/src/test/java/io/a2a/server/agentexecution/RequestContextTest.java +++ b/server-common/src/test/java/io/a2a/server/agentexecution/RequestContextTest.java @@ -354,4 +354,53 @@ public void testBuilderPreservesProvidedIdsWhenNoParams() { assertEquals(providedTaskId, context.getTaskId()); assertEquals(providedContextId, context.getContextId()); } + + @Test + public void testBuilderUpdatesMessageWithBuilderIds() { + // Regression test for Gemini review: ensure message gets updated when builder provides IDs + String builderTaskId = "builder-task-id"; + String builderContextId = "builder-context-id"; + + // Message has no IDs, but builder provides them + var mockMessage = Message.builder().role(Message.Role.USER).parts(List.of(new TextPart(""))).build(); + var mockParams = MessageSendParams.builder().message(mockMessage).configuration(defaultConfiguration()).build(); + + RequestContext context = new RequestContext.Builder() + .setParams(mockParams) + .setTaskId(builderTaskId) + .setContextId(builderContextId) + .build(); + + // Both context and message should have the builder IDs + assertEquals(builderTaskId, context.getTaskId()); + assertEquals(builderContextId, context.getContextId()); + assertEquals(builderTaskId, context.getMessage().taskId()); // KEY: message must be updated + assertEquals(builderContextId, context.getMessage().contextId()); // KEY: message must be updated + } + + @Test + public void testMessageIdsTakePrecedenceWhenBothPresent() { + // When both builder and message provide IDs, they must match (or throw) + String sharedTaskId = "shared-task-id"; + String sharedContextId = "shared-context-id"; + + var mockMessage = Message.builder() + .role(Message.Role.USER) + .parts(List.of(new TextPart(""))) + .taskId(sharedTaskId) + .contextId(sharedContextId) + .build(); + var mockParams = MessageSendParams.builder().message(mockMessage).configuration(defaultConfiguration()).build(); + + RequestContext context = new RequestContext.Builder() + .setParams(mockParams) + .setTaskId(sharedTaskId) // Same as message + .setContextId(sharedContextId) // Same as message + .build(); + + assertEquals(sharedTaskId, context.getTaskId()); + assertEquals(sharedContextId, context.getContextId()); + assertEquals(sharedTaskId, context.getMessage().taskId()); + assertEquals(sharedContextId, context.getMessage().contextId()); + } } From fe69c579f2975c9e85fc8187fa9b8cf3c132a782 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Wed, 11 Feb 2026 12:52:41 +0000 Subject: [PATCH 3/5] Review 2 --- .../server/agentexecution/RequestContext.java | 82 +++++++------------ .../agentexecution/RequestContextTest.java | 32 ++++++++ 2 files changed, 63 insertions(+), 51 deletions(-) diff --git a/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java b/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java index cf2cb2928..22d1482a4 100644 --- a/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java +++ b/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java @@ -353,60 +353,40 @@ public RequestContext build() throws InvalidParamsError { // 1. Initialize relatedTasks to empty list if null List finalRelatedTasks = relatedTasks != null ? relatedTasks : new ArrayList<>(); - // 2. Determine final IDs using coalesce pattern: builder → message → generate - String finalTaskId = taskId; - String finalContextId = contextId; - MessageSendParams finalParams = params; + // 2. Extract message IDs upfront (or null if no params) + String messageTaskId = params != null ? params.message().taskId() : null; + String messageContextId = params != null ? params.message().contextId() : null; + + // 3. Validate: if both builder and message provide an ID, they must match + if (taskId != null && messageTaskId != null && !taskId.equals(messageTaskId)) { + throw new InvalidParamsError("bad task id"); + } + if (contextId != null && messageContextId != null && !contextId.equals(messageContextId)) { + throw new InvalidParamsError("bad context id"); + } - if (params != null) { - String messageTaskId = params.message().taskId(); - String messageContextId = params.message().contextId(); - - // Validate: if both builder and message provide an ID, they must match - if (finalTaskId != null && messageTaskId != null && !finalTaskId.equals(messageTaskId)) { - throw new InvalidParamsError("bad task id"); - } - if (finalContextId != null && messageContextId != null && !finalContextId.equals(messageContextId)) { - throw new InvalidParamsError("bad context id"); - } - - // Coalesce: prefer builder ID, fall back to message ID, generate if both null - if (finalTaskId == null) { - finalTaskId = messageTaskId; - } - if (finalTaskId == null) { - finalTaskId = UUID.randomUUID().toString(); - } - - if (finalContextId == null) { - finalContextId = messageContextId; - } - if (finalContextId == null) { - finalContextId = UUID.randomUUID().toString(); - } - - // Update message if final IDs differ from message IDs - // This ensures getMessage().taskId() matches getTaskId() - if (!finalTaskId.equals(messageTaskId) || !finalContextId.equals(messageContextId)) { - Message updatedMessage = Message.builder(params.message()) - .taskId(finalTaskId) - .contextId(finalContextId) - .build(); - finalParams = new MessageSendParams(updatedMessage, - params.configuration(), params.metadata()); - } - } else { - // No params - generate IDs if not provided by builder - if (finalTaskId == null) { - finalTaskId = UUID.randomUUID().toString(); - } - if (finalContextId == null) { - finalContextId = UUID.randomUUID().toString(); - } + // 4. Determine final IDs using coalesce pattern: builder → message → generate + String finalTaskId = taskId != null ? taskId : + messageTaskId != null ? messageTaskId : + UUID.randomUUID().toString(); + + String finalContextId = contextId != null ? contextId : + messageContextId != null ? messageContextId : + UUID.randomUUID().toString(); + + // 5. Update params if message needs to be updated with final IDs + MessageSendParams finalParams = params; + if (params != null && (!finalTaskId.equals(messageTaskId) || !finalContextId.equals(messageContextId))) { + Message updatedMessage = Message.builder(params.message()) + .taskId(finalTaskId) + .contextId(finalContextId) + .build(); + // Preserve all original fields including tenant + finalParams = new MessageSendParams(updatedMessage, + params.configuration(), params.metadata(), params.tenant()); } - // 3. At this point, IDs are guaranteed non-null and consistent - // Call constructor with finalized values + // 6. Call constructor with finalized values (IDs guaranteed non-null) return new RequestContext(finalParams, finalTaskId, finalContextId, task, finalRelatedTasks, serverCallContext); } diff --git a/server-common/src/test/java/io/a2a/server/agentexecution/RequestContextTest.java b/server-common/src/test/java/io/a2a/server/agentexecution/RequestContextTest.java index e90f6d698..d116c99cb 100644 --- a/server-common/src/test/java/io/a2a/server/agentexecution/RequestContextTest.java +++ b/server-common/src/test/java/io/a2a/server/agentexecution/RequestContextTest.java @@ -403,4 +403,36 @@ public void testMessageIdsTakePrecedenceWhenBothPresent() { assertEquals(sharedTaskId, context.getMessage().taskId()); assertEquals(sharedContextId, context.getMessage().contextId()); } + + @Test + public void testBuilderPreservesTenantWhenUpdatingMessage() { + // Regression test for Gemini review: ensure tenant is preserved when message is updated + String tenantId = "customer-123"; + String builderTaskId = "builder-task-id"; + + var mockMessage = Message.builder().role(Message.Role.USER).parts(List.of(new TextPart(""))).build(); + var mockParams = MessageSendParams.builder() + .message(mockMessage) + .configuration(defaultConfiguration()) + .tenant(tenantId) + .build(); + + RequestContext context = new RequestContext.Builder() + .setParams(mockParams) + .setTaskId(builderTaskId) // Forces message update + .build(); + + // Tenant must be preserved in the updated params + assertNotNull(context.getMessage()); + assertEquals(builderTaskId, context.getMessage().taskId()); + + // Verify tenant wasn't lost during message update + MessageSendParams resultParams = new MessageSendParams( + context.getMessage(), + mockParams.configuration(), + mockParams.metadata(), + mockParams.tenant() + ); + assertEquals(tenantId, mockParams.tenant()); + } } From 1e02b6f00b509a968bd02ab348d393ceebfc6697 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Wed, 11 Feb 2026 13:03:13 +0000 Subject: [PATCH 4/5] Review 3 --- .../a2a/server/agentexecution/RequestContext.java | 13 +++++++++++++ .../server/agentexecution/RequestContextTest.java | 12 +++--------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java b/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java index 22d1482a4..676cb50a4 100644 --- a/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java +++ b/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java @@ -211,6 +211,19 @@ public List getRelatedTasks() { return callContext; } + /** + * Returns the tenant identifier from the request parameters. + *

+ * The tenant is used in multi-tenant environments to identify which + * customer or organization the request belongs to. + *

+ * + * @return the tenant identifier, or null if no params or tenant not set + */ + public @Nullable String getTenant() { + return params != null ? params.tenant() : null; + } + /** * Extracts all text content from the message and joins with the specified delimiter. *

diff --git a/server-common/src/test/java/io/a2a/server/agentexecution/RequestContextTest.java b/server-common/src/test/java/io/a2a/server/agentexecution/RequestContextTest.java index d116c99cb..0fc9c16ca 100644 --- a/server-common/src/test/java/io/a2a/server/agentexecution/RequestContextTest.java +++ b/server-common/src/test/java/io/a2a/server/agentexecution/RequestContextTest.java @@ -422,17 +422,11 @@ public void testBuilderPreservesTenantWhenUpdatingMessage() { .setTaskId(builderTaskId) // Forces message update .build(); - // Tenant must be preserved in the updated params + // Verify the message was updated with builder's task ID assertNotNull(context.getMessage()); assertEquals(builderTaskId, context.getMessage().taskId()); - // Verify tenant wasn't lost during message update - MessageSendParams resultParams = new MessageSendParams( - context.getMessage(), - mockParams.configuration(), - mockParams.metadata(), - mockParams.tenant() - ); - assertEquals(tenantId, mockParams.tenant()); + // KEY: Verify tenant wasn't lost during message update + assertEquals(tenantId, context.getTenant()); } } From 1095b425bbda770b2e198737ef0750040cac6660 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Wed, 11 Feb 2026 13:46:27 +0000 Subject: [PATCH 5/5] Review 4 --- .../java/io/a2a/server/agentexecution/RequestContext.java | 8 ++++++-- .../src/main/java/io/a2a/server/tasks/AgentEmitter.java | 5 ++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java b/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java index 676cb50a4..beee9bf7e 100644 --- a/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java +++ b/server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java @@ -395,8 +395,12 @@ public RequestContext build() throws InvalidParamsError { .contextId(finalContextId) .build(); // Preserve all original fields including tenant - finalParams = new MessageSendParams(updatedMessage, - params.configuration(), params.metadata(), params.tenant()); + finalParams = MessageSendParams.builder() + .message(updatedMessage) + .configuration(params.configuration()) + .metadata(params.metadata()) + .tenant(params.tenant()) + .build(); } // 6. Call constructor with finalized values (IDs guaranteed non-null) diff --git a/server-common/src/main/java/io/a2a/server/tasks/AgentEmitter.java b/server-common/src/main/java/io/a2a/server/tasks/AgentEmitter.java index 78f6ac2c3..6088022b6 100644 --- a/server-common/src/main/java/io/a2a/server/tasks/AgentEmitter.java +++ b/server-common/src/main/java/io/a2a/server/tasks/AgentEmitter.java @@ -18,7 +18,6 @@ import io.a2a.spec.TaskStatus; import io.a2a.spec.TaskStatusUpdateEvent; import io.a2a.spec.TextPart; -import io.a2a.util.Assert; import org.jspecify.annotations.Nullable; /** @@ -108,8 +107,8 @@ public class AgentEmitter { */ public AgentEmitter(RequestContext context, EventQueue eventQueue) { this.eventQueue = eventQueue; - this.taskId = Assert.checkNotNullParam("taskId",context.getTaskId()); - this.contextId = Assert.checkNotNullParam("contextId",context.getContextId()); + this.taskId = context.getTaskId(); + this.contextId = context.getContextId(); } private void updateStatus(TaskState taskState) {