Skip to content

Commit 9608c03

Browse files
fix: call tasks/result when input_required to deliver side-channel messages
Add input_required handling to the polling loop in requestStream(). When task status is input_required, call tasks/result to deliver queued messages (elicitation, sampling) via SSE and block until terminal.
1 parent a64cee7 commit 9608c03

File tree

2 files changed

+81
-0
lines changed

2 files changed

+81
-0
lines changed

src/integration-tests/taskLifecycle.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1604,4 +1604,77 @@ describe('Task Lifecycle Integration Tests', () => {
16041604
await transport.close();
16051605
});
16061606
});
1607+
1608+
describe('callToolStream with elicitation', () => {
1609+
it('should deliver elicitation via callToolStream and complete task', async () => {
1610+
const client = new Client(
1611+
{
1612+
name: 'test-client',
1613+
version: '1.0.0'
1614+
},
1615+
{
1616+
capabilities: {
1617+
elicitation: {}
1618+
}
1619+
}
1620+
);
1621+
1622+
// Track elicitation request receipt
1623+
let elicitationReceived = false;
1624+
let elicitationMessage = '';
1625+
1626+
// Set up elicitation handler on client
1627+
client.setRequestHandler(ElicitRequestSchema, async request => {
1628+
elicitationReceived = true;
1629+
elicitationMessage = request.params.message;
1630+
1631+
return {
1632+
action: 'accept' as const,
1633+
content: {
1634+
userName: 'StreamUser'
1635+
}
1636+
};
1637+
});
1638+
1639+
const transport = new StreamableHTTPClientTransport(baseUrl);
1640+
await client.connect(transport);
1641+
1642+
// Use callToolStream instead of raw request()
1643+
const stream = client.experimental.tasks.callToolStream({ name: 'input-task', arguments: {} }, CallToolResultSchema, {
1644+
task: { ttl: 60000 }
1645+
});
1646+
1647+
// Collect all stream messages
1648+
const messages: Array<{ type: string; task?: unknown; result?: unknown; error?: unknown }> = [];
1649+
for await (const message of stream) {
1650+
messages.push(message);
1651+
}
1652+
1653+
// Verify stream yielded expected message types
1654+
expect(messages.length).toBeGreaterThanOrEqual(2);
1655+
1656+
// First message should be taskCreated
1657+
expect(messages[0].type).toBe('taskCreated');
1658+
expect(messages[0].task).toBeDefined();
1659+
1660+
// Should have a taskStatus message
1661+
const statusMessages = messages.filter(m => m.type === 'taskStatus');
1662+
expect(statusMessages.length).toBeGreaterThanOrEqual(1);
1663+
1664+
// Last message should be result
1665+
const lastMessage = messages[messages.length - 1];
1666+
expect(lastMessage.type).toBe('result');
1667+
expect(lastMessage.result).toBeDefined();
1668+
1669+
// Verify elicitation was received and processed
1670+
expect(elicitationReceived).toBe(true);
1671+
expect(elicitationMessage).toContain('What is your name?');
1672+
1673+
// Verify result content
1674+
const result = lastMessage.result as { content: Array<{ type: string; text: string }> };
1675+
expect(result.content).toEqual([{ type: 'text', text: 'Hello, StreamUser!' }]);
1676+
1677+
await transport.close();
1678+
}, 15000);
1679+
});
16071680
});

src/shared/protocol.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,14 @@ export abstract class Protocol<SendRequestT extends Request, SendNotificationT e
10211021
return;
10221022
}
10231023

1024+
// When input_required, call tasks/result to deliver queued messages
1025+
// (elicitation, sampling) via SSE and block until terminal
1026+
if (task.status === 'input_required') {
1027+
const result = await this.getTaskResult({ taskId }, resultSchema, options);
1028+
yield { type: 'result', result };
1029+
return;
1030+
}
1031+
10241032
// Wait before polling again
10251033
const pollInterval = task.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1000;
10261034
await new Promise(resolve => setTimeout(resolve, pollInterval));

0 commit comments

Comments
 (0)