Skip to content

Commit d54e659

Browse files
boblangleyclaude
andcommitted
feat(server): expose SendMessageRequest metadata to AgentExecutor via RequestContext
The A2A specification defines SendMessageRequest.metadata as 'a flexible key-value map for passing additional context or parameters', and the extension mechanism encourages carrying extension data in request metadata. DefaultRequestHandler previously dropped this field before invoking the AgentExecutor, leaving server-side agents no way to read it. RequestContext gains an optional readonly metadata property, threaded from params.metadata in both sendMessage and sendMessageStream. Existing RequestContext call sites are unaffected (new trailing optional parameter). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent a005d2a commit d54e659

3 files changed

Lines changed: 155 additions & 5 deletions

File tree

src/server/agent_execution/request_context.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,29 @@ export class RequestContext {
88
public readonly task?: Task;
99
public readonly referenceTasks?: Task[];
1010
public readonly context: ServerCallContext;
11+
/**
12+
* The request-level metadata from the originating `SendMessageRequest`,
13+
* when provided. This is the spec's "flexible key-value map for passing
14+
* additional context or parameters" and is distinct from
15+
* `userMessage.metadata`.
16+
*/
17+
public readonly metadata?: Record<string, unknown>;
1118

1219
constructor(
1320
userMessage: Message,
1421
taskId: string,
1522
contextId: string,
1623
context: ServerCallContext,
1724
task?: Task,
18-
referenceTasks?: Task[]
25+
referenceTasks?: Task[],
26+
metadata?: Record<string, unknown>
1927
) {
2028
this.userMessage = userMessage;
2129
this.taskId = taskId;
2230
this.contextId = contextId;
2331
this.context = context;
2432
this.task = task;
2533
this.referenceTasks = referenceTasks;
34+
this.metadata = metadata;
2635
}
2736
}

src/server/request_handler/default_request_handler.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ export class DefaultRequestHandler implements A2ARequestHandler {
143143

144144
private async _createRequestContext(
145145
incomingMessage: Message,
146-
context: ServerCallContext
146+
context: ServerCallContext,
147+
requestMetadata?: Record<string, unknown>
147148
): Promise<RequestContext> {
148149
let task: Task | undefined;
149150
let referenceTasks: Task[] | undefined;
@@ -220,7 +221,15 @@ export class DefaultRequestHandler implements A2ARequestHandler {
220221
contextId,
221222
taskId,
222223
};
223-
return new RequestContext(messageForContext, taskId, contextId, context, task, referenceTasks);
224+
return new RequestContext(
225+
messageForContext,
226+
taskId,
227+
contextId,
228+
context,
229+
task,
230+
referenceTasks,
231+
requestMetadata
232+
);
224233
}
225234

226235
private async _processEvents(
@@ -558,7 +567,11 @@ export class DefaultRequestHandler implements A2ARequestHandler {
558567
const resultManager = new ResultManager(this.taskStore, context);
559568
resultManager.setContext(incomingMessage);
560569

561-
const requestContext = await this._createRequestContext(incomingMessage, context);
570+
const requestContext = await this._createRequestContext(
571+
incomingMessage,
572+
context,
573+
params.metadata
574+
);
562575
const taskId = requestContext.taskId;
563576
const finalMessageForAgent = requestContext.userMessage;
564577

@@ -652,7 +665,11 @@ export class DefaultRequestHandler implements A2ARequestHandler {
652665
const resultManager = new ResultManager(this.taskStore, context);
653666
resultManager.setContext(incomingMessage);
654667

655-
const requestContext = await this._createRequestContext(incomingMessage, context);
668+
const requestContext = await this._createRequestContext(
669+
incomingMessage,
670+
context,
671+
params.metadata
672+
);
656673
const taskId = requestContext.taskId;
657674

658675
const eventBus = this.eventBusManager.createOrGetByTaskId(taskId);

test/server/default_request_handler.spec.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3543,6 +3543,130 @@ describe('DefaultRequestHandler as A2ARequestHandler', () => {
35433543
expect(capturedRequestContext?.context?.user).to.be.an.instanceOf(UnauthenticatedUser);
35443544
});
35453545

3546+
it('should expose SendMessageRequest metadata to agentExecutor via RequestContext', async () => {
3547+
const requestMetadata = {
3548+
'a2a-service-parameters': { 'A2A-Extensions': 'https://example.com/extensions/sample/v1' },
3549+
traceId: 'trace-123',
3550+
};
3551+
3552+
const params: SendMessageRequest = {
3553+
tenant: '',
3554+
metadata: requestMetadata,
3555+
configuration: undefined,
3556+
message: {
3557+
messageId: 'msg-request-metadata',
3558+
role: Role.ROLE_USER,
3559+
parts: [
3560+
{
3561+
content: { $case: 'text', value: 'Verify request metadata.' },
3562+
filename: '',
3563+
mediaType: 'text/plain',
3564+
metadata: undefined,
3565+
},
3566+
],
3567+
contextId: 'metadata-context-id',
3568+
taskId: '',
3569+
extensions: [],
3570+
referenceTaskIds: [],
3571+
metadata: {},
3572+
},
3573+
};
3574+
3575+
let capturedRequestContext: RequestContext | undefined;
3576+
(mockAgentExecutor.execute as unknown as Mock).mockImplementation(
3577+
async (ctx: RequestContext, bus: ExecutionEventBus) => {
3578+
capturedRequestContext = ctx;
3579+
bus.publish(
3580+
AgentEvent.task({
3581+
id: ctx.taskId,
3582+
contextId: ctx.contextId,
3583+
status: {
3584+
state: TaskState.TASK_STATE_COMPLETED,
3585+
message: undefined,
3586+
timestamp: undefined,
3587+
},
3588+
artifacts: [],
3589+
history: [],
3590+
metadata: {},
3591+
})
3592+
);
3593+
bus.finished();
3594+
}
3595+
);
3596+
3597+
await handler.sendMessage(params, serverCallContext);
3598+
expect(capturedRequestContext?.metadata).to.deep.equal(
3599+
requestMetadata,
3600+
'sendMessage should thread request metadata into RequestContext'
3601+
);
3602+
3603+
capturedRequestContext = undefined;
3604+
const streamParams: SendMessageRequest = {
3605+
...params,
3606+
message: { ...params.message!, messageId: 'msg-request-metadata-stream' },
3607+
};
3608+
for await (const event of handler.sendMessageStream(streamParams, serverCallContext)) {
3609+
void event; // drain the stream
3610+
}
3611+
expect(capturedRequestContext?.metadata).to.deep.equal(
3612+
requestMetadata,
3613+
'sendMessageStream should thread request metadata into RequestContext'
3614+
);
3615+
});
3616+
3617+
it('should leave RequestContext metadata undefined when the request carries none', async () => {
3618+
const params: SendMessageRequest = {
3619+
tenant: '',
3620+
metadata: undefined,
3621+
configuration: undefined,
3622+
message: {
3623+
messageId: 'msg-no-request-metadata',
3624+
role: Role.ROLE_USER,
3625+
parts: [
3626+
{
3627+
content: { $case: 'text', value: 'No request metadata.' },
3628+
filename: '',
3629+
mediaType: 'text/plain',
3630+
metadata: undefined,
3631+
},
3632+
],
3633+
contextId: 'no-metadata-context-id',
3634+
taskId: '',
3635+
extensions: [],
3636+
referenceTaskIds: [],
3637+
metadata: {},
3638+
},
3639+
};
3640+
3641+
let capturedRequestContext: RequestContext | undefined;
3642+
(mockAgentExecutor.execute as unknown as Mock).mockImplementation(
3643+
async (ctx: RequestContext, bus: ExecutionEventBus) => {
3644+
capturedRequestContext = ctx;
3645+
bus.publish(
3646+
AgentEvent.task({
3647+
id: ctx.taskId,
3648+
contextId: ctx.contextId,
3649+
status: {
3650+
state: TaskState.TASK_STATE_COMPLETED,
3651+
message: undefined,
3652+
timestamp: undefined,
3653+
},
3654+
artifacts: [],
3655+
history: [],
3656+
metadata: {},
3657+
})
3658+
);
3659+
bus.finished();
3660+
}
3661+
);
3662+
3663+
await handler.sendMessage(params, serverCallContext);
3664+
expect(capturedRequestContext?.metadata).to.equal(
3665+
undefined,
3666+
'RequestContext metadata should be undefined when the request has none'
3667+
);
3668+
});
3669+
35463670
describe('getAuthenticatedExtendedAgentCard tests', async () => {
35473671
class A2AUser implements User {
35483672
constructor(private _isAuthenticated: boolean) {}

0 commit comments

Comments
 (0)