@nitrostack/core 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +74 -40
- package/dist/core/builders.d.ts.map +1 -1
- package/dist/core/builders.js +16 -1
- package/dist/core/builders.js.map +1 -1
- package/dist/core/decorators.d.ts +8 -0
- package/dist/core/decorators.d.ts.map +1 -1
- package/dist/core/decorators.js.map +1 -1
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/resource.d.ts +6 -4
- package/dist/core/resource.d.ts.map +1 -1
- package/dist/core/resource.js +26 -17
- package/dist/core/resource.js.map +1 -1
- package/dist/core/server.d.ts +26 -0
- package/dist/core/server.d.ts.map +1 -1
- package/dist/core/server.js +283 -4
- package/dist/core/server.js.map +1 -1
- package/dist/core/task.d.ts +280 -0
- package/dist/core/task.d.ts.map +1 -0
- package/dist/core/task.js +414 -0
- package/dist/core/task.js.map +1 -0
- package/dist/core/tool.d.ts +10 -0
- package/dist/core/tool.d.ts.map +1 -1
- package/dist/core/tool.js +7 -0
- package/dist/core/tool.js.map +1 -1
- package/dist/core/types.d.ts +21 -0
- package/dist/core/types.d.ts.map +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { Logger } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Task status values as per MCP spec
|
|
4
|
+
*
|
|
5
|
+
* Lifecycle:
|
|
6
|
+
* - `working` → `input_required` | `completed` | `failed` | `cancelled`
|
|
7
|
+
* - `input_required` → `working` | `completed` | `failed` | `cancelled`
|
|
8
|
+
* - `completed`, `failed`, `cancelled` are terminal — no further transitions
|
|
9
|
+
*/
|
|
10
|
+
export type TaskStatus = 'working' | 'input_required' | 'completed' | 'failed' | 'cancelled';
|
|
11
|
+
/**
|
|
12
|
+
* Terminal statuses — tasks in these states cannot transition further
|
|
13
|
+
*/
|
|
14
|
+
export declare const TERMINAL_STATUSES: TaskStatus[];
|
|
15
|
+
/**
|
|
16
|
+
* Check if a status is terminal
|
|
17
|
+
*/
|
|
18
|
+
export declare function isTerminalStatus(status: TaskStatus): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Task data structure as per MCP spec
|
|
21
|
+
*/
|
|
22
|
+
export interface TaskData {
|
|
23
|
+
/** Unique identifier for the task */
|
|
24
|
+
taskId: string;
|
|
25
|
+
/** Current state of the task execution */
|
|
26
|
+
status: TaskStatus;
|
|
27
|
+
/** Optional human-readable message describing the current state */
|
|
28
|
+
statusMessage?: string;
|
|
29
|
+
/** ISO 8601 timestamp when the task was created */
|
|
30
|
+
createdAt: string;
|
|
31
|
+
/** ISO 8601 timestamp when the task status was last updated */
|
|
32
|
+
lastUpdatedAt: string;
|
|
33
|
+
/** Time in milliseconds from creation before task may be deleted */
|
|
34
|
+
ttl: number | null;
|
|
35
|
+
/** Suggested time in milliseconds between status checks */
|
|
36
|
+
pollInterval?: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Task parameters that can be sent with a task-augmented request
|
|
40
|
+
*/
|
|
41
|
+
export interface TaskParams {
|
|
42
|
+
/** Requested TTL in milliseconds */
|
|
43
|
+
ttl?: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Related task metadata for associating messages with tasks
|
|
47
|
+
*/
|
|
48
|
+
export interface RelatedTaskMeta {
|
|
49
|
+
taskId: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* CreateTaskResult — returned when a task-augmented request is accepted
|
|
53
|
+
*/
|
|
54
|
+
export interface CreateTaskResult {
|
|
55
|
+
task: TaskData;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Task execution support level for tools
|
|
59
|
+
*/
|
|
60
|
+
export type TaskSupportLevel = 'required' | 'optional' | 'forbidden';
|
|
61
|
+
/**
|
|
62
|
+
* TaskManager handles the full lifecycle of MCP tasks.
|
|
63
|
+
*
|
|
64
|
+
* It provides:
|
|
65
|
+
* - In-memory task storage with TTL-based cleanup
|
|
66
|
+
* - Task creation, status updates, and result retrieval
|
|
67
|
+
* - Cancellation support
|
|
68
|
+
* - Status change notifications via callbacks
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* const taskManager = new TaskManager({ logger, defaultTtl: 60000 });
|
|
73
|
+
*
|
|
74
|
+
* // Create a task for a long-running operation
|
|
75
|
+
* const task = taskManager.createTask({ ttl: 120000 });
|
|
76
|
+
*
|
|
77
|
+
* // Update status as work progresses
|
|
78
|
+
* taskManager.updateStatus(task.taskId, 'working', 'Processing step 2 of 5');
|
|
79
|
+
*
|
|
80
|
+
* // Complete with result
|
|
81
|
+
* taskManager.completeTask(task.taskId, { data: 'result' });
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export declare class TaskManager {
|
|
85
|
+
private tasks;
|
|
86
|
+
private logger;
|
|
87
|
+
private defaultTtl;
|
|
88
|
+
private defaultPollInterval;
|
|
89
|
+
private cleanupInterval?;
|
|
90
|
+
private onStatusChange?;
|
|
91
|
+
constructor(options: {
|
|
92
|
+
logger: Logger;
|
|
93
|
+
/** Default TTL in ms (default: 300000 = 5 minutes) */
|
|
94
|
+
defaultTtl?: number;
|
|
95
|
+
/** Default poll interval in ms (default: 2000 = 2 seconds) */
|
|
96
|
+
defaultPollInterval?: number;
|
|
97
|
+
/** Callback fired on every status change (for notifications) */
|
|
98
|
+
onStatusChange?: (taskData: TaskData) => void;
|
|
99
|
+
});
|
|
100
|
+
/**
|
|
101
|
+
* Create a new task
|
|
102
|
+
*/
|
|
103
|
+
createTask(params?: TaskParams, toolName?: string): TaskData;
|
|
104
|
+
/**
|
|
105
|
+
* Get task data by ID
|
|
106
|
+
* @throws Error if task not found
|
|
107
|
+
*/
|
|
108
|
+
getTask(taskId: string): TaskData;
|
|
109
|
+
/**
|
|
110
|
+
* Update task status
|
|
111
|
+
* @throws Error if transition is invalid
|
|
112
|
+
*/
|
|
113
|
+
updateStatus(taskId: string, status: TaskStatus, statusMessage?: string): TaskData;
|
|
114
|
+
/**
|
|
115
|
+
* Complete a task with a successful result
|
|
116
|
+
*/
|
|
117
|
+
completeTask(taskId: string, result: unknown, statusMessage?: string): TaskData;
|
|
118
|
+
/**
|
|
119
|
+
* Fail a task with an error
|
|
120
|
+
*/
|
|
121
|
+
failTask(taskId: string, error: {
|
|
122
|
+
code: number;
|
|
123
|
+
message: string;
|
|
124
|
+
data?: unknown;
|
|
125
|
+
}, statusMessage?: string): TaskData;
|
|
126
|
+
/**
|
|
127
|
+
* Cancel a task
|
|
128
|
+
* @throws Error if task is already in a terminal state
|
|
129
|
+
*/
|
|
130
|
+
cancelTask(taskId: string): TaskData;
|
|
131
|
+
/**
|
|
132
|
+
* Get the result of a completed task.
|
|
133
|
+
* If the task is still working, this blocks until it reaches a terminal state.
|
|
134
|
+
*/
|
|
135
|
+
getResult(taskId: string): Promise<{
|
|
136
|
+
result?: unknown;
|
|
137
|
+
error?: {
|
|
138
|
+
code: number;
|
|
139
|
+
message: string;
|
|
140
|
+
data?: unknown;
|
|
141
|
+
};
|
|
142
|
+
}>;
|
|
143
|
+
/**
|
|
144
|
+
* Get the AbortSignal for a task (useful for cancellation-aware handlers)
|
|
145
|
+
*/
|
|
146
|
+
getAbortSignal(taskId: string): AbortSignal;
|
|
147
|
+
/**
|
|
148
|
+
* List all tasks with optional cursor-based pagination
|
|
149
|
+
*/
|
|
150
|
+
listTasks(cursor?: string, limit?: number): {
|
|
151
|
+
tasks: TaskData[];
|
|
152
|
+
nextCursor?: string;
|
|
153
|
+
};
|
|
154
|
+
/**
|
|
155
|
+
* Check if a task exists
|
|
156
|
+
*/
|
|
157
|
+
hasTask(taskId: string): boolean;
|
|
158
|
+
/**
|
|
159
|
+
* Cleanup expired tasks
|
|
160
|
+
*/
|
|
161
|
+
private cleanupExpiredTasks;
|
|
162
|
+
/**
|
|
163
|
+
* Get internal entry or throw
|
|
164
|
+
*/
|
|
165
|
+
private getEntry;
|
|
166
|
+
/**
|
|
167
|
+
* Validate a status transition
|
|
168
|
+
*/
|
|
169
|
+
private validateTransition;
|
|
170
|
+
/**
|
|
171
|
+
* Stop the cleanup interval (call on server shutdown)
|
|
172
|
+
*/
|
|
173
|
+
destroy(): void;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* TaskContext provides a developer-friendly interface for working with tasks
|
|
177
|
+
* inside tool handlers. It wraps the TaskManager and provides simple methods
|
|
178
|
+
* to update progress and check for cancellation.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```typescript
|
|
182
|
+
* const tool = new Tool({
|
|
183
|
+
* name: 'process_data',
|
|
184
|
+
* taskSupport: 'optional',
|
|
185
|
+
* handler: async (input, context) => {
|
|
186
|
+
* const task = context.task; // TaskContext if task-augmented
|
|
187
|
+
*
|
|
188
|
+
* if (task) {
|
|
189
|
+
* task.updateProgress('Starting data processing...');
|
|
190
|
+
*
|
|
191
|
+
* for (let i = 0; i < items.length; i++) {
|
|
192
|
+
* task.throwIfCancelled(); // Throws if client cancelled
|
|
193
|
+
* await processItem(items[i]);
|
|
194
|
+
* task.updateProgress(`Processed ${i + 1}/${items.length} items`);
|
|
195
|
+
* }
|
|
196
|
+
* }
|
|
197
|
+
*
|
|
198
|
+
* return { processed: items.length };
|
|
199
|
+
* }
|
|
200
|
+
* });
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
export declare class TaskContext {
|
|
204
|
+
private taskManager;
|
|
205
|
+
private _taskId;
|
|
206
|
+
private _abortSignal;
|
|
207
|
+
constructor(taskManager: TaskManager, taskId: string);
|
|
208
|
+
/** The task ID */
|
|
209
|
+
get taskId(): string;
|
|
210
|
+
/** AbortSignal that triggers when the task is cancelled */
|
|
211
|
+
get abortSignal(): AbortSignal;
|
|
212
|
+
/** Whether the task has been cancelled */
|
|
213
|
+
get isCancelled(): boolean;
|
|
214
|
+
/**
|
|
215
|
+
* Update the task status message (keeps status as 'working')
|
|
216
|
+
*/
|
|
217
|
+
updateProgress(message: string): void;
|
|
218
|
+
/**
|
|
219
|
+
* Transition to input_required status
|
|
220
|
+
*/
|
|
221
|
+
requestInput(message: string): void;
|
|
222
|
+
/**
|
|
223
|
+
* Throw a TaskCancelledError if the task has been cancelled.
|
|
224
|
+
* Call this periodically in long-running handlers to support cancellation.
|
|
225
|
+
*/
|
|
226
|
+
throwIfCancelled(): void;
|
|
227
|
+
/**
|
|
228
|
+
* Get the current task data
|
|
229
|
+
*/
|
|
230
|
+
getTaskData(): TaskData;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Task not found error (maps to JSON-RPC -32602)
|
|
234
|
+
*/
|
|
235
|
+
export declare class TaskNotFoundError extends Error {
|
|
236
|
+
readonly taskId: string;
|
|
237
|
+
readonly code = -32602;
|
|
238
|
+
constructor(taskId: string);
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Task already in terminal state error (maps to JSON-RPC -32602)
|
|
242
|
+
*/
|
|
243
|
+
export declare class TaskAlreadyTerminalError extends Error {
|
|
244
|
+
readonly taskId: string;
|
|
245
|
+
readonly status: TaskStatus;
|
|
246
|
+
readonly code = -32602;
|
|
247
|
+
constructor(taskId: string, status: TaskStatus);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Invalid task status transition error
|
|
251
|
+
*/
|
|
252
|
+
export declare class InvalidTaskTransitionError extends Error {
|
|
253
|
+
readonly fromStatus: TaskStatus;
|
|
254
|
+
readonly toStatus: TaskStatus;
|
|
255
|
+
readonly code = -32603;
|
|
256
|
+
constructor(fromStatus: TaskStatus, toStatus: TaskStatus);
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Task cancelled error (thrown by throwIfCancelled())
|
|
260
|
+
*/
|
|
261
|
+
export declare class TaskCancelledError extends Error {
|
|
262
|
+
readonly taskId: string;
|
|
263
|
+
constructor(taskId: string);
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Task augmentation required error (maps to JSON-RPC -32600)
|
|
267
|
+
*/
|
|
268
|
+
export declare class TaskAugmentationRequiredError extends Error {
|
|
269
|
+
readonly code = -32600;
|
|
270
|
+
constructor();
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Task expired error (maps to JSON-RPC -32602)
|
|
274
|
+
*/
|
|
275
|
+
export declare class TaskExpiredError extends Error {
|
|
276
|
+
readonly taskId: string;
|
|
277
|
+
readonly code = -32602;
|
|
278
|
+
constructor(taskId: string);
|
|
279
|
+
}
|
|
280
|
+
//# sourceMappingURL=task.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"task.d.ts","sourceRoot":"","sources":["../../src/core/task.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
+
/**
|
|
3
|
+
* Terminal statuses — tasks in these states cannot transition further
|
|
4
|
+
*/
|
|
5
|
+
export const TERMINAL_STATUSES = ['completed', 'failed', 'cancelled'];
|
|
6
|
+
/**
|
|
7
|
+
* Check if a status is terminal
|
|
8
|
+
*/
|
|
9
|
+
export function isTerminalStatus(status) {
|
|
10
|
+
return TERMINAL_STATUSES.includes(status);
|
|
11
|
+
}
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Task Manager
|
|
14
|
+
// ============================================================================
|
|
15
|
+
/**
|
|
16
|
+
* TaskManager handles the full lifecycle of MCP tasks.
|
|
17
|
+
*
|
|
18
|
+
* It provides:
|
|
19
|
+
* - In-memory task storage with TTL-based cleanup
|
|
20
|
+
* - Task creation, status updates, and result retrieval
|
|
21
|
+
* - Cancellation support
|
|
22
|
+
* - Status change notifications via callbacks
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const taskManager = new TaskManager({ logger, defaultTtl: 60000 });
|
|
27
|
+
*
|
|
28
|
+
* // Create a task for a long-running operation
|
|
29
|
+
* const task = taskManager.createTask({ ttl: 120000 });
|
|
30
|
+
*
|
|
31
|
+
* // Update status as work progresses
|
|
32
|
+
* taskManager.updateStatus(task.taskId, 'working', 'Processing step 2 of 5');
|
|
33
|
+
*
|
|
34
|
+
* // Complete with result
|
|
35
|
+
* taskManager.completeTask(task.taskId, { data: 'result' });
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export class TaskManager {
|
|
39
|
+
tasks = new Map();
|
|
40
|
+
logger;
|
|
41
|
+
defaultTtl;
|
|
42
|
+
defaultPollInterval;
|
|
43
|
+
cleanupInterval;
|
|
44
|
+
onStatusChange;
|
|
45
|
+
constructor(options) {
|
|
46
|
+
this.logger = options.logger;
|
|
47
|
+
this.defaultTtl = options.defaultTtl ?? 300000; // 5 minutes
|
|
48
|
+
this.defaultPollInterval = options.defaultPollInterval ?? 2000;
|
|
49
|
+
this.onStatusChange = options.onStatusChange;
|
|
50
|
+
// Start periodic cleanup of expired tasks
|
|
51
|
+
this.cleanupInterval = setInterval(() => this.cleanupExpiredTasks(), 30000);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Create a new task
|
|
55
|
+
*/
|
|
56
|
+
createTask(params, toolName) {
|
|
57
|
+
const taskId = uuidv4();
|
|
58
|
+
const now = new Date().toISOString();
|
|
59
|
+
const ttl = params?.ttl ?? this.defaultTtl;
|
|
60
|
+
let resolveCompletion;
|
|
61
|
+
const completionPromise = new Promise((resolve) => {
|
|
62
|
+
resolveCompletion = resolve;
|
|
63
|
+
});
|
|
64
|
+
const data = {
|
|
65
|
+
taskId,
|
|
66
|
+
status: 'working',
|
|
67
|
+
createdAt: now,
|
|
68
|
+
lastUpdatedAt: now,
|
|
69
|
+
ttl,
|
|
70
|
+
pollInterval: this.defaultPollInterval,
|
|
71
|
+
};
|
|
72
|
+
const entry = {
|
|
73
|
+
data,
|
|
74
|
+
abortController: new AbortController(),
|
|
75
|
+
completionPromise,
|
|
76
|
+
resolveCompletion: resolveCompletion,
|
|
77
|
+
toolName,
|
|
78
|
+
};
|
|
79
|
+
this.tasks.set(taskId, entry);
|
|
80
|
+
this.logger.info(`Task created: ${taskId}`, { toolName });
|
|
81
|
+
return { ...data };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get task data by ID
|
|
85
|
+
* @throws Error if task not found
|
|
86
|
+
*/
|
|
87
|
+
getTask(taskId) {
|
|
88
|
+
const entry = this.getEntry(taskId);
|
|
89
|
+
return { ...entry.data };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Update task status
|
|
93
|
+
* @throws Error if transition is invalid
|
|
94
|
+
*/
|
|
95
|
+
updateStatus(taskId, status, statusMessage) {
|
|
96
|
+
const entry = this.getEntry(taskId);
|
|
97
|
+
// Validate transition
|
|
98
|
+
this.validateTransition(entry.data.status, status);
|
|
99
|
+
entry.data.status = status;
|
|
100
|
+
entry.data.lastUpdatedAt = new Date().toISOString();
|
|
101
|
+
if (statusMessage !== undefined) {
|
|
102
|
+
entry.data.statusMessage = statusMessage;
|
|
103
|
+
}
|
|
104
|
+
// If terminal, resolve the completion promise
|
|
105
|
+
if (isTerminalStatus(status)) {
|
|
106
|
+
entry.resolveCompletion();
|
|
107
|
+
}
|
|
108
|
+
this.logger.info(`Task ${taskId} status: ${status}`, { statusMessage });
|
|
109
|
+
// Fire notification callback
|
|
110
|
+
if (this.onStatusChange) {
|
|
111
|
+
this.onStatusChange({ ...entry.data });
|
|
112
|
+
}
|
|
113
|
+
return { ...entry.data };
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Complete a task with a successful result
|
|
117
|
+
*/
|
|
118
|
+
completeTask(taskId, result, statusMessage) {
|
|
119
|
+
const entry = this.getEntry(taskId);
|
|
120
|
+
entry.result = result;
|
|
121
|
+
return this.updateStatus(taskId, 'completed', statusMessage ?? 'Task completed successfully');
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Fail a task with an error
|
|
125
|
+
*/
|
|
126
|
+
failTask(taskId, error, statusMessage) {
|
|
127
|
+
const entry = this.getEntry(taskId);
|
|
128
|
+
entry.error = error;
|
|
129
|
+
return this.updateStatus(taskId, 'failed', statusMessage ?? `Task failed: ${error.message}`);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Cancel a task
|
|
133
|
+
* @throws Error if task is already in a terminal state
|
|
134
|
+
*/
|
|
135
|
+
cancelTask(taskId) {
|
|
136
|
+
const entry = this.getEntry(taskId);
|
|
137
|
+
if (isTerminalStatus(entry.data.status)) {
|
|
138
|
+
throw new TaskAlreadyTerminalError(taskId, entry.data.status);
|
|
139
|
+
}
|
|
140
|
+
// Signal cancellation
|
|
141
|
+
entry.abortController.abort();
|
|
142
|
+
return this.updateStatus(taskId, 'cancelled', 'The task was cancelled by request.');
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Get the result of a completed task.
|
|
146
|
+
* If the task is still working, this blocks until it reaches a terminal state.
|
|
147
|
+
*/
|
|
148
|
+
async getResult(taskId) {
|
|
149
|
+
const entry = this.getEntry(taskId);
|
|
150
|
+
// If not terminal, wait for completion
|
|
151
|
+
if (!isTerminalStatus(entry.data.status)) {
|
|
152
|
+
await entry.completionPromise;
|
|
153
|
+
}
|
|
154
|
+
// Re-fetch after awaiting (status may have changed)
|
|
155
|
+
const current = this.getEntry(taskId);
|
|
156
|
+
if (current.error) {
|
|
157
|
+
return { error: current.error };
|
|
158
|
+
}
|
|
159
|
+
return { result: current.result };
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Get the AbortSignal for a task (useful for cancellation-aware handlers)
|
|
163
|
+
*/
|
|
164
|
+
getAbortSignal(taskId) {
|
|
165
|
+
const entry = this.getEntry(taskId);
|
|
166
|
+
return entry.abortController.signal;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* List all tasks with optional cursor-based pagination
|
|
170
|
+
*/
|
|
171
|
+
listTasks(cursor, limit = 50) {
|
|
172
|
+
const allTasks = Array.from(this.tasks.values())
|
|
173
|
+
.map(e => ({ ...e.data }))
|
|
174
|
+
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
|
175
|
+
let startIndex = 0;
|
|
176
|
+
if (cursor) {
|
|
177
|
+
const cursorIndex = allTasks.findIndex(t => t.taskId === cursor);
|
|
178
|
+
if (cursorIndex === -1) {
|
|
179
|
+
throw new TaskNotFoundError(cursor);
|
|
180
|
+
}
|
|
181
|
+
startIndex = cursorIndex + 1;
|
|
182
|
+
}
|
|
183
|
+
const page = allTasks.slice(startIndex, startIndex + limit);
|
|
184
|
+
const nextCursor = startIndex + limit < allTasks.length
|
|
185
|
+
? allTasks[startIndex + limit - 1]?.taskId
|
|
186
|
+
: undefined;
|
|
187
|
+
return { tasks: page, nextCursor };
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Check if a task exists
|
|
191
|
+
*/
|
|
192
|
+
hasTask(taskId) {
|
|
193
|
+
return this.tasks.has(taskId);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Cleanup expired tasks
|
|
197
|
+
*/
|
|
198
|
+
cleanupExpiredTasks() {
|
|
199
|
+
const now = Date.now();
|
|
200
|
+
for (const [taskId, entry] of this.tasks.entries()) {
|
|
201
|
+
if (entry.data.ttl === null)
|
|
202
|
+
continue; // Unlimited TTL
|
|
203
|
+
const createdTime = new Date(entry.data.createdAt).getTime();
|
|
204
|
+
if (now - createdTime > entry.data.ttl) {
|
|
205
|
+
this.tasks.delete(taskId);
|
|
206
|
+
this.logger.debug(`Expired task cleaned up: ${taskId}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Get internal entry or throw
|
|
212
|
+
*/
|
|
213
|
+
getEntry(taskId) {
|
|
214
|
+
const entry = this.tasks.get(taskId);
|
|
215
|
+
if (!entry) {
|
|
216
|
+
throw new TaskNotFoundError(taskId);
|
|
217
|
+
}
|
|
218
|
+
return entry;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Validate a status transition
|
|
222
|
+
*/
|
|
223
|
+
validateTransition(from, to) {
|
|
224
|
+
if (isTerminalStatus(from)) {
|
|
225
|
+
throw new InvalidTaskTransitionError(from, to);
|
|
226
|
+
}
|
|
227
|
+
// Same-status self-transitions are allowed for progress message updates
|
|
228
|
+
// (e.g., working → working just to update statusMessage)
|
|
229
|
+
if (from === to)
|
|
230
|
+
return;
|
|
231
|
+
// Valid transitions:
|
|
232
|
+
// working → input_required | completed | failed | cancelled
|
|
233
|
+
// input_required → working | completed | failed | cancelled
|
|
234
|
+
const validTransitions = {
|
|
235
|
+
working: ['input_required', 'completed', 'failed', 'cancelled'],
|
|
236
|
+
input_required: ['working', 'completed', 'failed', 'cancelled'],
|
|
237
|
+
};
|
|
238
|
+
const allowed = validTransitions[from];
|
|
239
|
+
if (!allowed || !allowed.includes(to)) {
|
|
240
|
+
throw new InvalidTaskTransitionError(from, to);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Stop the cleanup interval (call on server shutdown)
|
|
245
|
+
*/
|
|
246
|
+
destroy() {
|
|
247
|
+
if (this.cleanupInterval) {
|
|
248
|
+
clearInterval(this.cleanupInterval);
|
|
249
|
+
this.cleanupInterval = undefined;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// ============================================================================
|
|
254
|
+
// Task Context Helper
|
|
255
|
+
// ============================================================================
|
|
256
|
+
/**
|
|
257
|
+
* TaskContext provides a developer-friendly interface for working with tasks
|
|
258
|
+
* inside tool handlers. It wraps the TaskManager and provides simple methods
|
|
259
|
+
* to update progress and check for cancellation.
|
|
260
|
+
*
|
|
261
|
+
* @example
|
|
262
|
+
* ```typescript
|
|
263
|
+
* const tool = new Tool({
|
|
264
|
+
* name: 'process_data',
|
|
265
|
+
* taskSupport: 'optional',
|
|
266
|
+
* handler: async (input, context) => {
|
|
267
|
+
* const task = context.task; // TaskContext if task-augmented
|
|
268
|
+
*
|
|
269
|
+
* if (task) {
|
|
270
|
+
* task.updateProgress('Starting data processing...');
|
|
271
|
+
*
|
|
272
|
+
* for (let i = 0; i < items.length; i++) {
|
|
273
|
+
* task.throwIfCancelled(); // Throws if client cancelled
|
|
274
|
+
* await processItem(items[i]);
|
|
275
|
+
* task.updateProgress(`Processed ${i + 1}/${items.length} items`);
|
|
276
|
+
* }
|
|
277
|
+
* }
|
|
278
|
+
*
|
|
279
|
+
* return { processed: items.length };
|
|
280
|
+
* }
|
|
281
|
+
* });
|
|
282
|
+
* ```
|
|
283
|
+
*/
|
|
284
|
+
export class TaskContext {
|
|
285
|
+
taskManager;
|
|
286
|
+
_taskId;
|
|
287
|
+
_abortSignal;
|
|
288
|
+
constructor(taskManager, taskId) {
|
|
289
|
+
this.taskManager = taskManager;
|
|
290
|
+
this._taskId = taskId;
|
|
291
|
+
this._abortSignal = taskManager.getAbortSignal(taskId);
|
|
292
|
+
}
|
|
293
|
+
/** The task ID */
|
|
294
|
+
get taskId() {
|
|
295
|
+
return this._taskId;
|
|
296
|
+
}
|
|
297
|
+
/** AbortSignal that triggers when the task is cancelled */
|
|
298
|
+
get abortSignal() {
|
|
299
|
+
return this._abortSignal;
|
|
300
|
+
}
|
|
301
|
+
/** Whether the task has been cancelled */
|
|
302
|
+
get isCancelled() {
|
|
303
|
+
return this._abortSignal.aborted;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Update the task status message (keeps status as 'working')
|
|
307
|
+
*/
|
|
308
|
+
updateProgress(message) {
|
|
309
|
+
try {
|
|
310
|
+
this.taskManager.updateStatus(this._taskId, 'working', message);
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// Task may have been cancelled/cleaned up — ignore
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Transition to input_required status
|
|
318
|
+
*/
|
|
319
|
+
requestInput(message) {
|
|
320
|
+
this.taskManager.updateStatus(this._taskId, 'input_required', message);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Throw a TaskCancelledError if the task has been cancelled.
|
|
324
|
+
* Call this periodically in long-running handlers to support cancellation.
|
|
325
|
+
*/
|
|
326
|
+
throwIfCancelled() {
|
|
327
|
+
if (this._abortSignal.aborted) {
|
|
328
|
+
throw new TaskCancelledError(this._taskId);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Get the current task data
|
|
333
|
+
*/
|
|
334
|
+
getTaskData() {
|
|
335
|
+
return this.taskManager.getTask(this._taskId);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
// ============================================================================
|
|
339
|
+
// Task Errors
|
|
340
|
+
// ============================================================================
|
|
341
|
+
/**
|
|
342
|
+
* Task not found error (maps to JSON-RPC -32602)
|
|
343
|
+
*/
|
|
344
|
+
export class TaskNotFoundError extends Error {
|
|
345
|
+
taskId;
|
|
346
|
+
code = -32602;
|
|
347
|
+
constructor(taskId) {
|
|
348
|
+
super(`Failed to retrieve task: Task not found`);
|
|
349
|
+
this.taskId = taskId;
|
|
350
|
+
this.name = 'TaskNotFoundError';
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Task already in terminal state error (maps to JSON-RPC -32602)
|
|
355
|
+
*/
|
|
356
|
+
export class TaskAlreadyTerminalError extends Error {
|
|
357
|
+
taskId;
|
|
358
|
+
status;
|
|
359
|
+
code = -32602;
|
|
360
|
+
constructor(taskId, status) {
|
|
361
|
+
super(`Cannot cancel task: already in terminal status '${status}'`);
|
|
362
|
+
this.taskId = taskId;
|
|
363
|
+
this.status = status;
|
|
364
|
+
this.name = 'TaskAlreadyTerminalError';
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Invalid task status transition error
|
|
369
|
+
*/
|
|
370
|
+
export class InvalidTaskTransitionError extends Error {
|
|
371
|
+
fromStatus;
|
|
372
|
+
toStatus;
|
|
373
|
+
code = -32603;
|
|
374
|
+
constructor(fromStatus, toStatus) {
|
|
375
|
+
super(`Invalid task status transition: ${fromStatus} → ${toStatus}`);
|
|
376
|
+
this.fromStatus = fromStatus;
|
|
377
|
+
this.toStatus = toStatus;
|
|
378
|
+
this.name = 'InvalidTaskTransitionError';
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Task cancelled error (thrown by throwIfCancelled())
|
|
383
|
+
*/
|
|
384
|
+
export class TaskCancelledError extends Error {
|
|
385
|
+
taskId;
|
|
386
|
+
constructor(taskId) {
|
|
387
|
+
super(`Task ${taskId} was cancelled`);
|
|
388
|
+
this.taskId = taskId;
|
|
389
|
+
this.name = 'TaskCancelledError';
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Task augmentation required error (maps to JSON-RPC -32600)
|
|
394
|
+
*/
|
|
395
|
+
export class TaskAugmentationRequiredError extends Error {
|
|
396
|
+
code = -32600;
|
|
397
|
+
constructor() {
|
|
398
|
+
super('Task augmentation required for tools/call requests');
|
|
399
|
+
this.name = 'TaskAugmentationRequiredError';
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Task expired error (maps to JSON-RPC -32602)
|
|
404
|
+
*/
|
|
405
|
+
export class TaskExpiredError extends Error {
|
|
406
|
+
taskId;
|
|
407
|
+
code = -32602;
|
|
408
|
+
constructor(taskId) {
|
|
409
|
+
super(`Failed to retrieve task: Task has expired`);
|
|
410
|
+
this.taskId = taskId;
|
|
411
|
+
this.name = 'TaskExpiredError';
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
//# sourceMappingURL=task.js.map
|