Skip to content

Commit a92f54e

Browse files
authored
feat(server): expose SendMessageRequest metadata to AgentExecutor via RequestContext (#564)
Adds an optional readonly `metadata: Record<string, unknown>` property to `RequestContext`, populated from `SendMessageRequest.metadata` in both `sendMessage` and `sendMessageStream`. - The property is appended as a trailing optional constructor parameter, so existing `RequestContext` construction sites are unaffected. - It is `undefined` when the request carries no metadata — no behavior change for existing executors. - Two tests added to `default_request_handler.spec.ts`: request metadata reaches the executor on both the unary and streaming send paths (exercised with the spec's own `a2a-service-parameters` pattern), and the property stays `undefined` when the request carries none. # Description Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [x] Follow the [`CONTRIBUTING` Guide](https://github.com/google-a2a/a2a-js/blob/main/CONTRIBUTING.md). - [x] Make your Pull Request title in the <https://www.conventionalcommits.org/> specification. - Important Prefixes for [release-please](https://github.com/googleapis/release-please): - `fix:` which represents bug fixes, and correlates to a [SemVer](https://semver.org/) patch. - `feat:` represents a new feature, and correlates to a SemVer minor. - `feat!:`, or `fix!:`, `refactor!:`, etc., which represent a breaking change (indicated by the `!`) and will result in a SemVer major. - [x] Ensure the tests and linter pass - [x] Appropriate docs were updated (if necessary) Fixes #563 🦕
1 parent a005d2a commit a92f54e

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 ? structuredClone(metadata) : undefined;
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)