Skip to content

Commit 102d26b

Browse files
authored
feat: Make RequestContext taskId and contextId nullable. (#642)
This is possible by moving the generation of IDs when not set into the builder, and making sure the builder is used.
1 parent 3afb15a commit 102d26b

3 files changed

Lines changed: 295 additions & 98 deletions

File tree

server-common/src/main/java/io/a2a/server/agentexecution/RequestContext.java

Lines changed: 118 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -74,64 +74,66 @@
7474
*/
7575
public class RequestContext {
7676

77-
private @Nullable MessageSendParams params;
78-
private @Nullable String taskId;
79-
private @Nullable String contextId;
80-
private @Nullable Task task;
81-
private List<Task> relatedTasks;
77+
private final @Nullable MessageSendParams params;
78+
private final String taskId;
79+
private final String contextId;
80+
private final @Nullable Task task;
81+
private final List<Task> relatedTasks;
8282
private final @Nullable ServerCallContext callContext;
8383

84-
public RequestContext(
84+
/**
85+
* Constructor with all fields already validated and initialized.
86+
* <p>
87+
* <b>Note:</b> Use {@link Builder} instead of calling this constructor directly.
88+
* The builder handles ID generation and validation.
89+
* </p>
90+
*
91+
* @param params the message send parameters (can be null for cancel operations)
92+
* @param taskId the task identifier (must not be null)
93+
* @param contextId the context identifier (must not be null)
94+
* @param task the existing task state (null for new conversations)
95+
* @param relatedTasks other tasks in the same context (must not be null, can be empty)
96+
* @param callContext the server call context (can be null)
97+
*/
98+
private RequestContext(
8599
@Nullable MessageSendParams params,
86-
@Nullable String taskId,
87-
@Nullable String contextId,
100+
String taskId,
101+
String contextId,
88102
@Nullable Task task,
89-
@Nullable List<Task> relatedTasks,
90-
@Nullable ServerCallContext callContext) throws InvalidParamsError {
103+
List<Task> relatedTasks,
104+
@Nullable ServerCallContext callContext) {
91105
this.params = params;
92106
this.taskId = taskId;
93107
this.contextId = contextId;
94108
this.task = task;
95-
this.relatedTasks = relatedTasks == null ? new ArrayList<>() : relatedTasks;
109+
this.relatedTasks = relatedTasks;
96110
this.callContext = callContext;
97-
98-
// If the taskId and contextId were specified, they must match the params
99-
if (params != null) {
100-
if (taskId != null && !taskId.equals(params.message().taskId())) {
101-
throw new InvalidParamsError("bad task id");
102-
}
103-
this.taskId = checkOrGenerateTaskId();
104-
if (contextId != null && !contextId.equals(params.message().contextId())) {
105-
throw new InvalidParamsError("bad context id");
106-
}
107-
this.contextId = checkOrGenerateContextId();
108-
}
109111
}
110112

111113
/**
112114
* Returns the task identifier.
113115
* <p>
114-
* This is auto-generated (UUID) if not provided by the client in the message parameters.
115-
* It can be null if the context was not created from message parameters.
116+
* This is auto-generated (UUID) by the builder if not provided by the client
117+
* in the message parameters. This value is never null.
116118
* </p>
117119
*
118-
* @return the task ID
120+
* @return the task ID (never null)
119121
*/
120-
public @Nullable String getTaskId() {
122+
public String getTaskId() {
121123
return taskId;
122124
}
123125

124126
/**
125127
* Returns the conversation context identifier.
126128
* <p>
127129
* Conversation contexts group related tasks together (e.g., multiple tasks
128-
* in the same user session). This is auto-generated (UUID) if not provided by the client
129-
* in the message parameters. It can be null if the context was not created from message parameters.
130+
* in the same user session). This is auto-generated (UUID) by the builder if
131+
* not provided by the client in the message parameters. This value is never null.
130132
* </p>
131133
*
132-
* @return the context ID
134+
* @return the context ID (never null)
133135
*/
134-
public @Nullable String getContextId() {
136+
public String getContextId() {
135137
return contextId;
136138
}
137139

@@ -209,6 +211,19 @@ public List<Task> getRelatedTasks() {
209211
return callContext;
210212
}
211213

214+
/**
215+
* Returns the tenant identifier from the request parameters.
216+
* <p>
217+
* The tenant is used in multi-tenant environments to identify which
218+
* customer or organization the request belongs to.
219+
* </p>
220+
*
221+
* @return the tenant identifier, or null if no params or tenant not set
222+
*/
223+
public @Nullable String getTenant() {
224+
return params != null ? params.tenant() : null;
225+
}
226+
212227
/**
213228
* Extracts all text content from the message and joins with the specified delimiter.
214229
* <p>
@@ -240,48 +255,20 @@ public String getUserInput(String delimiter) {
240255
return getMessageText(params.message(), delimiter);
241256
}
242257

258+
/**
259+
* Attaches a related task to this context.
260+
* <p>
261+
* This is primarily used by the framework to populate related tasks after
262+
* construction. Agent implementations should use {@link #getRelatedTasks()}
263+
* to access related tasks.
264+
* </p>
265+
*
266+
* @param task the task to attach
267+
*/
243268
public void attachRelatedTask(Task task) {
244269
relatedTasks.add(task);
245270
}
246271

247-
private @Nullable String checkOrGenerateTaskId() {
248-
if (params == null) {
249-
return taskId;
250-
}
251-
if (taskId == null && params.message().taskId() == null) {
252-
// Message is immutable, create new one with generated taskId
253-
String generatedTaskId = UUID.randomUUID().toString();
254-
Message updatedMessage = Message.builder(params.message())
255-
.taskId(generatedTaskId)
256-
.build();
257-
params = new MessageSendParams(updatedMessage, params.configuration(), params.metadata());
258-
return generatedTaskId;
259-
}
260-
if (params.message().taskId() != null) {
261-
return params.message().taskId();
262-
}
263-
return taskId;
264-
}
265-
266-
private @Nullable String checkOrGenerateContextId() {
267-
if (params == null) {
268-
return contextId;
269-
}
270-
if (contextId == null && params.message().contextId() == null) {
271-
// Message is immutable, create new one with generated contextId
272-
String generatedContextId = UUID.randomUUID().toString();
273-
Message updatedMessage = Message.builder(params.message())
274-
.contextId(generatedContextId)
275-
.build();
276-
params = new MessageSendParams(updatedMessage, params.configuration(), params.metadata());
277-
return generatedContextId;
278-
}
279-
if (params.message().contextId() != null) {
280-
return params.message().contextId();
281-
}
282-
return contextId;
283-
}
284-
285272
private String getMessageText(Message message, String delimiter) {
286273
List<String> textParts = getTextParts(message.parts());
287274
return String.join(delimiter, textParts);
@@ -295,6 +282,18 @@ private List<String> getTextParts(List<Part<?>> parts) {
295282
.collect(Collectors.toList());
296283
}
297284

285+
/**
286+
* Builder for creating {@link RequestContext} instances.
287+
* <p>
288+
* The builder handles ID generation and validation automatically:
289+
* </p>
290+
* <ul>
291+
* <li>TaskId and ContextId are auto-generated (UUID) if not provided</li>
292+
* <li>IDs are validated against message parameters if both are present</li>
293+
* <li>Message parameters are updated with generated IDs</li>
294+
* <li>Related tasks list is initialized to empty list if null</li>
295+
* </ul>
296+
*/
298297
public static class Builder {
299298
private @Nullable MessageSendParams params;
300299
private @Nullable String taskId;
@@ -357,8 +356,56 @@ public Builder setServerCallContext(@Nullable ServerCallContext serverCallContex
357356
return serverCallContext;
358357
}
359358

360-
public RequestContext build() {
361-
return new RequestContext(params, taskId, contextId, task, relatedTasks, serverCallContext);
359+
/**
360+
* Builds the RequestContext with ID generation and validation.
361+
*
362+
* @return the constructed RequestContext
363+
* @throws InvalidParamsError if taskId or contextId don't match message parameters
364+
*/
365+
public RequestContext build() throws InvalidParamsError {
366+
// 1. Initialize relatedTasks to empty list if null
367+
List<Task> finalRelatedTasks = relatedTasks != null ? relatedTasks : new ArrayList<>();
368+
369+
// 2. Extract message IDs upfront (or null if no params)
370+
String messageTaskId = params != null ? params.message().taskId() : null;
371+
String messageContextId = params != null ? params.message().contextId() : null;
372+
373+
// 3. Validate: if both builder and message provide an ID, they must match
374+
if (taskId != null && messageTaskId != null && !taskId.equals(messageTaskId)) {
375+
throw new InvalidParamsError("bad task id");
376+
}
377+
if (contextId != null && messageContextId != null && !contextId.equals(messageContextId)) {
378+
throw new InvalidParamsError("bad context id");
379+
}
380+
381+
// 4. Determine final IDs using coalesce pattern: builder → message → generate
382+
String finalTaskId = taskId != null ? taskId :
383+
messageTaskId != null ? messageTaskId :
384+
UUID.randomUUID().toString();
385+
386+
String finalContextId = contextId != null ? contextId :
387+
messageContextId != null ? messageContextId :
388+
UUID.randomUUID().toString();
389+
390+
// 5. Update params if message needs to be updated with final IDs
391+
MessageSendParams finalParams = params;
392+
if (params != null && (!finalTaskId.equals(messageTaskId) || !finalContextId.equals(messageContextId))) {
393+
Message updatedMessage = Message.builder(params.message())
394+
.taskId(finalTaskId)
395+
.contextId(finalContextId)
396+
.build();
397+
// Preserve all original fields including tenant
398+
finalParams = MessageSendParams.builder()
399+
.message(updatedMessage)
400+
.configuration(params.configuration())
401+
.metadata(params.metadata())
402+
.tenant(params.tenant())
403+
.build();
404+
}
405+
406+
// 6. Call constructor with finalized values (IDs guaranteed non-null)
407+
return new RequestContext(finalParams, finalTaskId, finalContextId,
408+
task, finalRelatedTasks, serverCallContext);
362409
}
363410
}
364411

server-common/src/main/java/io/a2a/server/tasks/AgentEmitter.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import io.a2a.spec.TaskStatus;
1919
import io.a2a.spec.TaskStatusUpdateEvent;
2020
import io.a2a.spec.TextPart;
21-
import io.a2a.util.Assert;
2221
import org.jspecify.annotations.Nullable;
2322

2423
/**
@@ -108,8 +107,8 @@ public class AgentEmitter {
108107
*/
109108
public AgentEmitter(RequestContext context, EventQueue eventQueue) {
110109
this.eventQueue = eventQueue;
111-
this.taskId = Assert.checkNotNullParam("taskId",context.getTaskId());
112-
this.contextId = Assert.checkNotNullParam("contextId",context.getContextId());
110+
this.taskId = context.getTaskId();
111+
this.contextId = context.getContextId();
113112
}
114113

115114
private void updateStatus(TaskState taskState) {

0 commit comments

Comments
 (0)