fix: retrieve stored result from tasks/result for failed tasks (#1930)
Co-authored-by: Christian-Sidak <csidak@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Konstantin Konstantinov <KKonstantinov@users.noreply.github.com> Co-authored-by: Konstantin Konstantinov <konstantin@mach5technology.com>
This commit is contained in:
parent
b8886e77f6
commit
bdfd7f0154
3 changed files with 51 additions and 5 deletions
5
.changeset/fix-failed-task-result-retrieval.md
Normal file
5
.changeset/fix-failed-task-result-retrieval.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@modelcontextprotocol/core': patch
|
||||
---
|
||||
|
||||
Fix `requestStream` to call `tasks/result` for failed tasks instead of yielding a hardcoded `ProtocolError`. When a task reaches the `failed` terminal status, the stream now retrieves and yields the actual stored result (matching the behavior for `completed` tasks), as required by the spec.
|
||||
|
|
@ -302,15 +302,12 @@ export class TaskManager {
|
|||
|
||||
if (isTerminal(task.status)) {
|
||||
switch (task.status) {
|
||||
case 'completed': {
|
||||
case 'completed':
|
||||
case 'failed': {
|
||||
const result = await this.getTaskResult({ taskId }, resultSchema, options);
|
||||
yield { type: 'result', result };
|
||||
break;
|
||||
}
|
||||
case 'failed': {
|
||||
yield { type: 'error', error: new ProtocolError(ProtocolErrorCode.InternalError, `Task ${taskId} failed`) };
|
||||
break;
|
||||
}
|
||||
case 'cancelled': {
|
||||
yield {
|
||||
type: 'error',
|
||||
|
|
|
|||
|
|
@ -1502,6 +1502,50 @@ describe('Task Lifecycle Integration Tests', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('callToolStream with failed task', () => {
|
||||
it('should yield stored result (isError: true) when task fails, not a generic ProtocolError', async () => {
|
||||
const client = new Client(
|
||||
{
|
||||
name: 'test-client',
|
||||
version: '1.0.0'
|
||||
},
|
||||
{
|
||||
capabilities: { tasks: {} }
|
||||
}
|
||||
);
|
||||
|
||||
const transport = new StreamableHTTPClientTransport(baseUrl);
|
||||
await client.connect(transport);
|
||||
|
||||
// Use callToolStream with shouldFail: true so the tool stores a failed result
|
||||
const stream = client.experimental.tasks.callToolStream(
|
||||
{ name: 'long-task', arguments: { duration: 100, shouldFail: true } },
|
||||
{ task: { ttl: 60_000 } }
|
||||
);
|
||||
|
||||
// Collect all stream messages
|
||||
const messages: Array<{ type: string; task?: unknown; result?: unknown; error?: unknown }> = [];
|
||||
for await (const message of stream) {
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
// First message should be taskCreated
|
||||
expect(messages[0]!.type).toBe('taskCreated');
|
||||
|
||||
// Last message must be 'result' (carrying the stored isError content),
|
||||
// NOT 'error' (which would mean the generic hardcoded ProtocolError was returned)
|
||||
const lastMessage = messages.at(-1)!;
|
||||
expect(lastMessage.type).toBe('result');
|
||||
|
||||
// The stored result should contain isError: true and the real failure content
|
||||
const result = lastMessage.result as { content: Array<{ type: string; text: string }>; isError: boolean };
|
||||
expect(result.isError).toBe(true);
|
||||
expect(result.content).toEqual([{ type: 'text', text: 'Task failed as requested' }]);
|
||||
|
||||
await transport.close();
|
||||
}, 15_000);
|
||||
});
|
||||
|
||||
describe('callToolStream with elicitation', () => {
|
||||
it('should deliver elicitation via callToolStream and complete task', async () => {
|
||||
const client = new Client(
|
||||
|
|
|
|||
Loading…
Reference in a new issue