Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions docs/migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,16 +305,28 @@ new ServerCallContext(requestedExtensions, user);
new ServerCallContext({ requestedExtensions, user, tenant: 'my-tenant', requestedVersion: '1.0' });
```

`RequestContext` constructor parameter order also changed -- `context` moved
from last (optional) to 4th (mandatory):
`RequestContext` now wraps the incoming `SendMessageRequest`,
and `context` moved from last (optional) to 4th (mandatory).
The loose `userMessage` parameter is replaced by `request: SendMessageRequest`;
agent executors read the message via `ctx.userMessage` (convenience accessor
guaranteed non-null) and the full payload -- including `configuration` and
request-level `metadata` -- via `ctx.request`:

```typescript
// v0.3
new RequestContext(userMessage, taskId, contextId, task, referenceTasks, context);
// v1.0
new RequestContext(userMessage, taskId, contextId, context, task, referenceTasks);
new RequestContext(request, taskId, contextId, context, task, referenceTasks);

// Reading from an executor:
ctx.userMessage; // Message -- shorthand for ctx.request.message (non-null)
ctx.request.configuration; // SendMessageConfiguration | undefined -- newly exposed
ctx.request.metadata; // Record<string, unknown> | undefined
```

The wrapped `request` is deep-cloned on construction so mutations inside the
executor cannot leak back to the caller's `SendMessageRequest`.

### 3.4 `ExecutionEventBus` -- Discriminated Event Wrapper

Events must now be wrapped with `AgentEvent` factories:
Expand Down
34 changes: 19 additions & 15 deletions src/server/agent_execution/request_context.ts
Comment thread
JakubWorek marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -1,36 +1,40 @@
import { Message, Task } from '../../index.js';
import { Message, SendMessageRequest, Task } from '../../index.js';
import { ServerCallContext } from '../context.js';

/**
* Holds information about the current request being processed by the server.
*
* Wraps the incoming {@link SendMessageRequest} so agent executors can reach
* the full payload (message, configuration, metadata, tenant) via `request`.
*/
export class RequestContext {
public readonly userMessage: Message;
public readonly request: SendMessageRequest;
public readonly taskId: string;
public readonly contextId: string;
public readonly context: ServerCallContext;
public readonly task?: Task;
public readonly referenceTasks?: Task[];
public readonly context: ServerCallContext;
/**
* The request-level metadata from the originating `SendMessageRequest`,
* when provided. This is the spec's "flexible key-value map for passing
* additional context or parameters" and is distinct from
* `userMessage.metadata`.
*/
public readonly metadata?: Record<string, unknown>;

constructor(
userMessage: Message,
request: SendMessageRequest,
taskId: string,
contextId: string,
context: ServerCallContext,
task?: Task,
referenceTasks?: Task[],
metadata?: Record<string, unknown>
referenceTasks?: Task[]
) {
this.userMessage = userMessage;
if (!request.message) {
throw new Error('RequestContext requires request.message to be set.');
}
this.request = structuredClone(request);
this.taskId = taskId;
this.contextId = contextId;
this.context = context;
this.task = task;
this.referenceTasks = referenceTasks;
this.metadata = metadata ? structuredClone(metadata) : undefined;
}

get userMessage(): Message {
return this.request.message!;
}
}
37 changes: 15 additions & 22 deletions src/server/request_handler/default_request_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,10 +142,13 @@ export class DefaultRequestHandler implements A2ARequestHandler {
}

private async _createRequestContext(
incomingMessage: Message,
context: ServerCallContext,
requestMetadata?: Record<string, unknown>
request: SendMessageRequest,
context: ServerCallContext
): Promise<RequestContext> {
const incomingMessage = request.message;
if (!incomingMessage) {
throw new RequestMalformedError('request.message is required.');
}
let task: Task | undefined;
let referenceTasks: Task[] | undefined;

Expand Down Expand Up @@ -221,15 +224,13 @@ export class DefaultRequestHandler implements A2ARequestHandler {
contextId,
taskId,
};
return new RequestContext(
messageForContext,
taskId,
contextId,
context,
task,
referenceTasks,
requestMetadata
);
// Rebuild the request with the enriched message so downstream
// consumers see the resolved task/context IDs on `userMessage`.
const resolvedRequest: SendMessageRequest = {
...request,
message: messageForContext,
};
return new RequestContext(resolvedRequest, taskId, contextId, context, task, referenceTasks);
}

private async _processEvents(
Expand Down Expand Up @@ -571,11 +572,7 @@ export class DefaultRequestHandler implements A2ARequestHandler {
const resultManager = new ResultManager(this.taskStore, context);
resultManager.setContext(incomingMessage);

const requestContext = await this._createRequestContext(
incomingMessage,
context,
params.metadata
);
const requestContext = await this._createRequestContext(params, context);
const taskId = requestContext.taskId;
const finalMessageForAgent = requestContext.userMessage;

Expand Down Expand Up @@ -669,11 +666,7 @@ export class DefaultRequestHandler implements A2ARequestHandler {
const resultManager = new ResultManager(this.taskStore, context);
resultManager.setContext(incomingMessage);

const requestContext = await this._createRequestContext(
incomingMessage,
context,
params.metadata
);
const requestContext = await this._createRequestContext(params, context);
const taskId = requestContext.taskId;

const eventBus = this.eventBusManager.createOrGetByTaskId(taskId);
Expand Down
6 changes: 3 additions & 3 deletions test/server/default_request_handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3654,7 +3654,7 @@ describe('DefaultRequestHandler as A2ARequestHandler', () => {
);

await handler.sendMessage(params, serverCallContext);
expect(capturedRequestContext?.metadata).to.deep.equal(
expect(capturedRequestContext?.request.metadata).to.deep.equal(
requestMetadata,
'sendMessage should thread request metadata into RequestContext'
);
Expand All @@ -3667,7 +3667,7 @@ describe('DefaultRequestHandler as A2ARequestHandler', () => {
for await (const event of handler.sendMessageStream(streamParams, serverCallContext)) {
void event; // drain the stream
}
expect(capturedRequestContext?.metadata).to.deep.equal(
expect(capturedRequestContext?.request.metadata).to.deep.equal(
requestMetadata,
'sendMessageStream should thread request metadata into RequestContext'
);
Expand Down Expand Up @@ -3720,7 +3720,7 @@ describe('DefaultRequestHandler as A2ARequestHandler', () => {
);

await handler.sendMessage(params, serverCallContext);
expect(capturedRequestContext?.metadata).to.equal(
expect(capturedRequestContext?.request.metadata).to.equal(
undefined,
'RequestContext metadata should be undefined when the request has none'
);
Expand Down
Loading