@nitrostack/core 1.0.1 → 1.0.3

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.
@@ -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":"AACA,OAAO,EAAE,MAAM,EAA+B,MAAM,YAAY,CAAC;AAMjE;;;;;;;GAOG;AACH,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,gBAAgB,GAAG,WAAW,GAAG,QAAQ,GAAG,WAAW,CAAC;AAE7F;;GAEG;AACH,eAAO,MAAM,iBAAiB,EAAE,UAAU,EAAyC,CAAC;AAEpF;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAE5D;AAMD;;GAEG;AACH,MAAM,WAAW,QAAQ;IACrB,qCAAqC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,MAAM,EAAE,UAAU,CAAC;IACnB,mEAAmE;IACnE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;IAClB,+DAA+D;IAC/D,aAAa,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACvB,oCAAoC;IACpC,GAAG,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC5B,MAAM,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,QAAQ,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,UAAU,GAAG,WAAW,CAAC;AA8BrE;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,WAAW;IACpB,OAAO,CAAC,KAAK,CAAqC;IAClD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,mBAAmB,CAAS;IACpC,OAAO,CAAC,eAAe,CAAC,CAAiC;IACzD,OAAO,CAAC,cAAc,CAAC,CAA+B;gBAE1C,OAAO,EAAE;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,sDAAsD;QACtD,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,8DAA8D;QAC9D,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B,gEAAgE;QAChE,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAC;KACjD;IAUD;;OAEG;IACH,UAAU,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,QAAQ;IAiC5D;;;OAGG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ;IAKjC;;;OAGG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,QAAQ;IA2BlF;;OAEG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,QAAQ;IAM/E;;OAEG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,OAAO,CAAA;KAAE,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,QAAQ;IAMpH;;;OAGG;IACH,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ;IAapC;;;OAGG;IACG,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE,OAAO,CAAA;SAAE,CAAA;KAAE,CAAC;IAkBzH;;OAEG;IACH,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW;IAK3C;;OAEG;IACH,SAAS,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,GAAE,MAAW,GAAG;QAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE;IAsB1F;;OAEG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAIhC;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAa3B;;OAEG;IACH,OAAO,CAAC,QAAQ;IAQhB;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAuB1B;;OAEG;IACH,OAAO,IAAI,IAAI;CAMlB;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,WAAW;IACpB,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,YAAY,CAAc;gBAEtB,WAAW,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM;IAMpD,kBAAkB;IAClB,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,2DAA2D;IAC3D,IAAI,WAAW,IAAI,WAAW,CAE7B;IAED,0CAA0C;IAC1C,IAAI,WAAW,IAAI,OAAO,CAEzB;IAED;;OAEG;IACH,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAQrC;;OAEG;IACH,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAInC;;;OAGG;IACH,gBAAgB,IAAI,IAAI;IAMxB;;OAEG;IACH,WAAW,IAAI,QAAQ;CAG1B;AAMD;;GAEG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;aAEZ,MAAM,EAAE,MAAM;IAD1C,SAAgB,IAAI,UAAU;gBACF,MAAM,EAAE,MAAM;CAI7C;AAED;;GAEG;AACH,qBAAa,wBAAyB,SAAQ,KAAK;aAEnB,MAAM,EAAE,MAAM;aAAkB,MAAM,EAAE,UAAU;IAD9E,SAAgB,IAAI,UAAU;gBACF,MAAM,EAAE,MAAM,EAAkB,MAAM,EAAE,UAAU;CAIjF;AAED;;GAEG;AACH,qBAAa,0BAA2B,SAAQ,KAAK;aAErB,UAAU,EAAE,UAAU;aAAkB,QAAQ,EAAE,UAAU;IADxF,SAAgB,IAAI,UAAU;gBACF,UAAU,EAAE,UAAU,EAAkB,QAAQ,EAAE,UAAU;CAI3F;AAED;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;aACb,MAAM,EAAE,MAAM;gBAAd,MAAM,EAAE,MAAM;CAI7C;AAED;;GAEG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;IACpD,SAAgB,IAAI,UAAU;;CAKjC;AAED;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;aAEX,MAAM,EAAE,MAAM;IAD1C,SAAgB,IAAI,UAAU;gBACF,MAAM,EAAE,MAAM;CAI7C"}
@@ -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