Skip to content

Commit 9655c59

Browse files
committed
SEP-XXXX: Task Continuity
1 parent 838d6f6 commit 9655c59

1 file changed

Lines changed: 263 additions & 0 deletions

File tree

seps/XXXX-task-continuity.md

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
# SEP-XXXX: Task Continuity
2+
3+
- **Status**: Draft
4+
- **Type**: Standards Track
5+
- **Created**: 2026-03-02
6+
- **Author(s)**: Luca Chang (@LucaButBoring)
7+
- **Sponsor**: None
8+
- **PR**: https://github.com/modelcontextprotocol/specification/pull/{NUMBER}
9+
10+
## Abstract
11+
12+
To resolve existing ambiguity around the `input_required` and `tasks/result` flows for tasks, and to accomodate [SEP-2322 Multi Round-Trip Requests](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322), this SEP introduces a consolidated `tasks/continue` method that absorbs the responsibilities of the entire task-polling lifecycle into a single method and inlines the final result/error into `tasks/get`. This SEP then removes the associated requirements around `input_required` and removes `tasks/result` to simplify implementations.
13+
14+
## Motivation
15+
16+
**Tasks** were introduced in an experimental state in the `2025-11-25` specification release, serving as an alternate execution mode for certain request types (tool calls, elicitation, and sampling) to enable polling for the result of a task-augmented operation. This is done according to the following process, using tool calls as an example:
17+
18+
1. The client issues a `tools/call` request to the server, declaring the `task` parameter to indicate that the server should create a task to represent that work.
19+
1. The server returns a `CreateTaskResult` with the task info for the client to look up the result later. This contains a suggested polling interval that the client should follow to avoid overwhelming the server.
20+
1. The client polls `tasks/get` repeatedly according to the suggested polling interval.
21+
1. If the task status is ever `input_required` during this phase, the client prematurely issues a `tasks/result` call to the server, which is expected to block until the task result is available:
22+
1. The server sends some request message to the client concurrently with this premature request. In stdio, this is effectively meaningless, but in Streamable HTTP, this allows the server to open an SSE side channel to send that pending request on.
23+
1. The client receives the request and sends a response, according to the standard conventions of the transport in use. This is completely disconnected from the (still-ongoing) `tasks/result` call.
24+
1. Once the server receives the result it needs, it transitions the task back to the `working` status.
25+
1. Once the task status is `completed`, the client issues a `tasks/result` call to retrieve the final result.
26+
1. If the client still has an active `tasks/result` call from a prior `input_required` status, it will receive the result as the result to that open request.
27+
28+
This flow has several problems, most of which are related to the `input_required` status transition:
29+
30+
1. Prematurely invoking `tasks/result` is unintuitive and it only done to accomodate the possibility of no other SSE streams being open in Streamable HTTP.
31+
1. The fact that `tasks/result` blocks until completion is [even less intuitive](https://github.com/modelcontextprotocol/java-sdk/pull/755#issuecomment-3806079033).
32+
1. Clients need to issue an additional request after encountering the `completed` status just to retrieve the final task result.
33+
1. When a task reaches the `completed` status after this point, the server needs to identify all open `tasks/result` requests for that task to appropriately close them with the final task result, introducing unnecessary architectural complexity by mandating some sort of internal push-based messaging, which defies the intent of tasks' polling-based design.
34+
35+
## Specification
36+
37+
The following changes will be made to the tasks specification:
38+
39+
1. We will introduce a consolidated `tasks/continue` method to handle the standard polling lifecycle. This single method will handle retrieving task statuses and results simultaneously, and will additionally act as the eventual carrier for receiver-to-requestor requests for the purposes of [SEP-2322 Multi Round-Trip Requests](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322).
40+
1. We will remove the requirement that requestors react to the `input_required` status by prematurely invoking `tasks/result` to side-channel requests on an SSE stream in the Streamable HTTP transport.
41+
1. We will inline the final task result or error into the `Task` shape, bringing that into `tasks/get` and all notifications by extension.
42+
1. We will remove `tasks/result`.
43+
44+
### Getting Tasks
45+
46+
We will inline the final result or error into the `Task` shape:
47+
48+
```ts
49+
export interface Task {
50+
// Existing fields...
51+
/**
52+
* This is the result shape of the original request, e.g. CallToolResult.
53+
* This field is only defined when the task status is "completed".
54+
*/
55+
result?: Result;
56+
/**
57+
* This is the error associated with the original request.
58+
* This field is only defined when the task status is "failed". If a task
59+
* is "cancelled", its statusMessage field will communicate that information.
60+
*/
61+
error?: Error;
62+
}
63+
```
64+
65+
This makes these fields present in the result of `tasks/get` and in all task status notifications.
66+
67+
### Task Continuation
68+
69+
Task continuation represents an evolution to how the task-polling lifecycle is advanced, reducing the flow to just two method calls:
70+
71+
1. The initial task-augmented request (e.g. `tools/call` with the `task` parameter declared).
72+
1. The new `tasks/continue` request, which is polled continuously until the final task result is available.
73+
74+
Task continuation involves its own request and response types, which are identical to those of `tasks/get`:
75+
76+
**Request Schema**
77+
78+
```ts
79+
/**
80+
* A request to continue a task.
81+
*
82+
* @category `tasks/continue`
83+
*/
84+
export interface ContinueTaskRequest extends JSONRPCRequest {
85+
method: "tasks/continue";
86+
params: {
87+
/**
88+
* The task identifier to query.
89+
*/
90+
taskId: string;
91+
};
92+
}
93+
```
94+
95+
**Response Schema**
96+
97+
```ts
98+
/**
99+
* The response to a tasks/continue request.
100+
*
101+
* @category `tasks/continue`
102+
*/
103+
export interface ContinueTaskResult extends Result, Task {}
104+
```
105+
106+
**Message Flow**
107+
108+
The basic task lifecycle sequence diagram will be updated as follows:
109+
110+
```diff
111+
sequenceDiagram
112+
participant C as Client (Requestor)
113+
participant S as Server (Receiver)
114+
Note over C,S: 1. Task Creation
115+
C->>S: Request with task field (ttl)
116+
activate S
117+
S->>C: CreateTaskResult (taskId, status: working, ttl, pollInterval)
118+
deactivate S
119+
Note over C,S: 2. Task Polling
120+
- C->>S: tasks/get (taskId)
121+
+ C->>S: tasks/continue (taskId)
122+
activate S
123+
S->>C: working
124+
deactivate S
125+
Note over S: Task processing continues...
126+
- C->>S: tasks/get (taskId)
127+
+ C->>S: tasks/continue (taskId)
128+
activate S
129+
S->>C: working
130+
deactivate S
131+
Note over S: Task completes
132+
- C->>S: tasks/get (taskId)
133+
+ C->>S: tasks/continue (taskId)
134+
activate S
135+
S->>C: completed
136+
deactivate S
137+
Note over C,S: 3. Result Retrieval
138+
- C->>S: tasks/result (taskId)
139+
+ C->>S: tasks/continue (taskId)
140+
activate S
141+
S->>C: Result content
142+
deactivate S
143+
Note over C,S: 4. Cleanup
144+
Note over S: After ttl period from creation, task is cleaned up
145+
```
146+
147+
### Input Required Status
148+
149+
The requirement that requestors invoke `tasks/result` prematurely after encountering `input_required` will be removed:
150+
151+
```diff
152+
1. When the task receiver has messages for the requestor that are necessary to complete the task, the receiver **SHOULD** move the task to the `input_required` status.
153+
1. The receiver **MUST** include the `io.modelcontextprotocol/related-task` metadata in the request to associate it with the task.
154+
- 1. When the requestor encounters the `input_required` status, it **SHOULD** preemptively call `tasks/result`.
155+
1. When the receiver receives all required input, the task **SHOULD** transition out of `input_required` status (typically back to `working`).
156+
```
157+
158+
### Result Retrieval
159+
160+
The `tasks/result` operation will be removed in favor of using the inlined values in the result of `tasks/get`.
161+
162+
## Rationale
163+
164+
### Interactions with SEP-2322
165+
166+
Looking forward, there are additional problems the existing flow has when considered in conjunction with [SEP-2322 Multi Round-Trip Requests](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2322) (MRTR):
167+
168+
1. The SSE side-channeling approach will become inconsistent with MRTR's request-rejection approach. It would be strange for server-to-client message delivery to fundamentally differ between tasks and non-tasks.
169+
1. If we maintain the existing flow for tasks, we need to determine which request MRTR's `InputRequests` object is rejected with:
170+
1. Rejecting the initial task-augmented request leaves the client without knowledge of the task ID to continue polling.
171+
1. Rejecting `tasks/get` conflicts with use cases in which we just want to retrieve the task status without answering server-to-client requests.
172+
1. Rejecting `tasks/result` makes the most sense and is most similar to the existing flow, but that would be a breaking change that removes the blocking requirement, which deserves its own SEP to evaluate (this is that SEP). Furthermore, it maintains the semantic strangeness of `tasks/result` being invoked to receive server-to-client requests, rather than to actually retrieve the final result.
173+
174+
The suggested flow for MRTR will be to reject `tasks/continue` instead of any of the existing methods; that is, calling `tasks/continue` while the `input_required` status is in effect will return something like this:
175+
176+
```json
177+
{
178+
"id": 2,
179+
"jsonrpc": "2.0",
180+
"result": {
181+
"inputRequests": {
182+
"echo_input": {
183+
"method": "elicitation/create",
184+
"params": {
185+
"mode": "form",
186+
"message": "Please provide the input string to echo back",
187+
"requestedSchema": {
188+
"type": "object",
189+
"properties": {
190+
"input": { "type": "string" }
191+
},
192+
"required": ["input"]
193+
}
194+
}
195+
}
196+
}
197+
}
198+
}
199+
```
200+
201+
In response, the client will _continue_ the task by providing appropriate response values in a follow-up request:
202+
203+
```json
204+
{
205+
"jsonrpc": "2.0",
206+
"id": 3,
207+
"method": "tasks/continue",
208+
"params": {
209+
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
210+
"inputResponses": {
211+
"echo_input": {
212+
"action": "accept",
213+
"content": {
214+
"input": "Hello World!"
215+
}
216+
}
217+
}
218+
}
219+
}
220+
```
221+
222+
And finally, the server will accept that client request with a standard task status response, as would be expected under MRTR:
223+
224+
```json
225+
{
226+
"jsonrpc": "2.0",
227+
"id": 3,
228+
"result": {
229+
"taskId": "786512e2-9e0d-44bd-8f29-789f320fe840",
230+
"status": "working",
231+
"createdAt": "2025-11-25T10:30:00Z",
232+
"lastUpdatedAt": "2025-11-25T10:50:00Z",
233+
"ttl": 60000,
234+
"pollInterval": 5000
235+
}
236+
}
237+
```
238+
239+
### Keeping `tasks/get`
240+
241+
If we're introducing a new method that encapsulates the entire task-polling lifecycle, **why should we keep `tasks/get`**?
242+
243+
While `tasks/continue` owns the full task-polling lifecycle, `tasks/get` still owns single-task state lookups. This is particularly relevant under MRTR (SEP-2322), as this method becomes the only way to fetch the state of a task _without first being expected to handle input requests_.
244+
245+
The alternative would be forcing clients that just want the task status (for e.g. UI displays) to handle the pending requests, first.
246+
247+
### Removing `tasks/result`
248+
249+
Removing methods is not done lightly, but was the logical conclusion of this change after inlining the result/error into `tasks/get`. As an alternative, we could have left `tasks/get` unchanged and left `tasks/result` in solely as a method for late result retrieval. This would have incentivized using `tasks/continue` as a general method of retrieving the task state and result simultaneously, rendering `tasks/result` obsolete regardless.
250+
251+
Furthermore, the greatest flaw of `tasks/result` today is its blocking requirement, and leaving that in place would leave the general implementation headache of dealing with that unsolved. If we chose to instead remove the blocking requirement in favor of an "incomplete" error, we could get away with leaving `tasks/result` in place in a deprecated state, but then we would be making a breaking change to it anyways already.
252+
253+
## Backward Compatibility
254+
255+
This change is not backwards-compatible with the existing implementation of tasks, particularly due to removing `tasks/result`.
256+
257+
## Security Implications
258+
259+
No new security implications.
260+
261+
## Reference Implementation
262+
263+
To be provided.

0 commit comments

Comments
 (0)