Skip to content

Commit 00fb916

Browse files
fix: ensure user messages are added to existing tasks (#138)
Ensured user messages are added to existing tasks, inline with the Python package: - Added the latest user message to an existing `Task`'s history in the `RequestContext` - Added the test "should return second task with full history if message is sent to an existing, non-terminal task" - Updated `README.md` to check if a `Task` exists before publishing to the `eventBus` # 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 #136 🦕
1 parent 21048fb commit 00fb916

File tree

3 files changed

+192
-22
lines changed

3 files changed

+192
-22
lines changed

README.md

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -163,19 +163,22 @@ class TaskExecutor implements AgentExecutor {
163163
requestContext: RequestContext,
164164
eventBus: ExecutionEventBus
165165
): Promise<void> {
166-
const { taskId, contextId } = requestContext;
167-
168-
// 1. Create and publish the initial task object.
169-
const initialTask: Task = {
170-
kind: "task",
171-
id: taskId,
172-
contextId: contextId,
173-
status: {
174-
state: "submitted",
175-
timestamp: new Date().toISOString(),
176-
},
177-
};
178-
eventBus.publish(initialTask);
166+
const { taskId, contextId, userMessage, task } = requestContext;
167+
168+
// 1. Create and publish the initial task object if it doesn't exist.
169+
if (!task) {
170+
const initialTask: Task = {
171+
kind: "task",
172+
id: taskId,
173+
contextId: contextId,
174+
status: {
175+
state: "submitted",
176+
timestamp: new Date().toISOString(),
177+
},
178+
history: [userMessage]
179+
};
180+
eventBus.publish(initialTask);
181+
}
179182

180183
// 2. Create and publish an artifact.
181184
const artifactUpdate: TaskArtifactUpdateEvent = {
@@ -354,15 +357,22 @@ class StreamingExecutor implements AgentExecutor {
354357
requestContext: RequestContext,
355358
eventBus: ExecutionEventBus
356359
): Promise<void> {
357-
const { taskId, contextId } = requestContext;
358-
359-
// 1. Publish initial 'submitted' state.
360-
eventBus.publish({
361-
kind: "task",
362-
id: taskId,
363-
contextId,
364-
status: { state: "submitted", timestamp: new Date().toISOString() },
365-
});
360+
const { taskId, contextId, userMessage, task } = requestContext;
361+
362+
// 1. Create and publish the initial task object if it doesn't exist.
363+
if (!task) {
364+
const initialTask: Task = {
365+
kind: "task",
366+
id: taskId,
367+
contextId: contextId,
368+
status: {
369+
state: "submitted",
370+
timestamp: new Date().toISOString(),
371+
},
372+
history: [userMessage]
373+
};
374+
eventBus.publish(initialTask);
375+
}
366376

367377
// 2. Publish 'working' state.
368378
eventBus.publish({

src/server/request_handler/default_request_handler.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ export class DefaultRequestHandler implements A2ARequestHandler {
8080
// Throw an error that conforms to the JSON-RPC Invalid Request error specification.
8181
throw A2AError.invalidRequest(`Task ${task.id} is in a terminal state (${task.status.state}) and cannot be modified.`)
8282
}
83+
84+
// Add incomingMessage to history and save the task.
85+
task.history = [...(task.history || []), incomingMessage];
86+
await this.taskStore.save(task);
8387
}
8488

8589
if (incomingMessage.referenceTaskIds && incomingMessage.referenceTaskIds.length > 0) {

test/server/default_request_handler.spec.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,162 @@ describe('DefaultRequestHandler as A2ARequestHandler', () => {
242242
assert.include((nonBlockingTask.status.message?.parts[0] as any).text, errorMessage, 'Error message should be in the status');
243243
});
244244

245+
it('sendMessage: should return second task with full history if message is sent to an existing, non-terminal task', async () => {
246+
const contextId = 'ctx-history-abc';
247+
248+
// First message
249+
const firstMessage = createTestMessage('msg-1', 'Message 1');
250+
firstMessage.contextId = contextId;
251+
const firstParams: MessageSendParams = {
252+
message: firstMessage
253+
};
254+
255+
let taskId: string;
256+
257+
(mockAgentExecutor as MockAgentExecutor).execute.callsFake(async (ctx, bus) => {
258+
taskId = ctx.taskId;
259+
260+
// Publish task creation
261+
bus.publish({
262+
id: taskId,
263+
contextId,
264+
status: { state: "submitted" },
265+
kind: 'task'
266+
});
267+
268+
// Publish working status
269+
bus.publish({
270+
taskId,
271+
contextId,
272+
kind: 'status-update',
273+
status: { state: "working" },
274+
final: false
275+
});
276+
277+
// Mark as input-required with agent response message
278+
bus.publish({
279+
taskId,
280+
contextId,
281+
kind: 'status-update',
282+
status: {
283+
state: "input-required",
284+
message: {
285+
messageId: 'agent-msg-1',
286+
role: 'agent',
287+
parts: [{ kind: 'text', text: 'Response to message 1' }],
288+
kind: 'message',
289+
taskId,
290+
contextId
291+
}
292+
},
293+
final: true
294+
});
295+
bus.finished();
296+
});
297+
298+
const firstResult = await handler.sendMessage(firstParams);
299+
const firstTask = firstResult as Task;
300+
301+
// Check the first result is a task with `input-required` status
302+
assert.equal(firstTask.kind, 'task');
303+
assert.equal(firstTask.status.state, 'input-required');
304+
305+
// Check the history
306+
assert.isDefined(firstTask.history, 'First task should have history');
307+
assert.lengthOf(firstTask.history!, 2, 'First task history should contain user message and agent message');
308+
assert.equal(firstTask.history![0].messageId, 'msg-1', 'First history item should be user message');
309+
assert.equal(firstTask.history![1].messageId, 'agent-msg-1', 'Second history item should be agent message');
310+
311+
// Second message
312+
const secondMessage = createTestMessage('msg-2', 'Message 2');
313+
secondMessage.contextId = contextId;
314+
secondMessage.taskId = firstTask.id;
315+
316+
const secondParams: MessageSendParams = {
317+
message: secondMessage
318+
};
319+
320+
(mockAgentExecutor as MockAgentExecutor).execute.callsFake(async (ctx, bus) => {
321+
// Publish a status update with working state
322+
bus.publish({
323+
taskId,
324+
contextId,
325+
kind: 'status-update',
326+
status: { state: "working" },
327+
final: false
328+
});
329+
330+
// Publish a status update with working state and message
331+
bus.publish({
332+
taskId,
333+
contextId,
334+
kind: 'status-update',
335+
status: {
336+
state: "working",
337+
message: {
338+
messageId: 'agent-msg-2',
339+
role: 'agent',
340+
parts: [{ kind: 'text', text: 'Response to message 2' }],
341+
kind: 'message',
342+
taskId,
343+
contextId
344+
}
345+
},
346+
final: false
347+
});
348+
349+
// Publish an artifact update
350+
bus.publish({
351+
taskId,
352+
contextId,
353+
kind: 'artifact-update',
354+
artifact: {
355+
artifactId: 'artifact-1',
356+
name: 'Test Document',
357+
description: 'A test artifact.',
358+
parts: [{ kind: 'text', text: 'This is the content of the artifact.' }]
359+
}
360+
});
361+
362+
// Mark as completed
363+
bus.publish({
364+
taskId,
365+
contextId,
366+
kind: 'status-update',
367+
status: {
368+
state: "completed"
369+
},
370+
final: true
371+
});
372+
373+
bus.finished();
374+
});
375+
376+
const secondResult = await handler.sendMessage(secondParams);
377+
const secondTask = secondResult as Task;
378+
379+
// Check the second result is a task with `completed` status
380+
assert.equal(secondTask.kind, 'task');
381+
assert.equal(secondTask.id, taskId, 'Should be the same task');
382+
assert.equal(secondTask.status.state, 'completed');
383+
384+
// Check the history
385+
assert.isDefined(secondTask.history, 'Second task should have history');
386+
assert.lengthOf(secondTask.history!, 4, 'Second task history should contain all 4 messages (user1, agent1, user2, agent2)');
387+
assert.equal(secondTask.history![0].messageId, 'msg-1', 'First message should be first user message');
388+
assert.equal((secondTask.history![0].parts[0] as any).text, 'Message 1');
389+
assert.equal(secondTask.history![1].messageId, 'agent-msg-1', 'Second message should be first agent message');
390+
assert.equal((secondTask.history![1].parts[0] as any).text, 'Response to message 1');
391+
assert.equal(secondTask.history![2].messageId, 'msg-2', 'Third message should be second user message');
392+
assert.equal((secondTask.history![2].parts[0] as any).text, 'Message 2');
393+
assert.equal(secondTask.history![3].messageId, 'agent-msg-2', 'Fourth message should be second agent message');
394+
assert.equal((secondTask.history![3].parts[0] as any).text, 'Response to message 2');
395+
assert.equal(secondTask.artifacts![0].artifactId, 'artifact-1', 'Artifact should be the same');
396+
assert.equal(secondTask.artifacts![0].name, 'Test Document', 'Artifact name should be the same');
397+
assert.equal(secondTask.artifacts![0].description, 'A test artifact.', 'Artifact description should be the same');
398+
assert.equal((secondTask.artifacts![0].parts[0] as any).text, 'This is the content of the artifact.', 'Artifact content should be the same');
399+
});
400+
245401
it('sendMessageStream: should stream submitted, working, and completed events', async () => {
246402
const params: MessageSendParams = {
247403
message: createTestMessage('msg-3', 'Stream a task')

0 commit comments

Comments
 (0)