@anastops/mcp-server 0.1.0 → 1.0.0

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.
Files changed (105) hide show
  1. package/dist/formatters.d.ts.map +1 -1
  2. package/dist/formatters.js +12 -3
  3. package/dist/formatters.js.map +1 -1
  4. package/dist/handlers/agent-handlers.d.ts +8 -0
  5. package/dist/handlers/agent-handlers.d.ts.map +1 -0
  6. package/dist/handlers/agent-handlers.js +184 -0
  7. package/dist/handlers/agent-handlers.js.map +1 -0
  8. package/dist/handlers/artifact-handlers.d.ts +8 -0
  9. package/dist/handlers/artifact-handlers.d.ts.map +1 -0
  10. package/dist/handlers/artifact-handlers.js +122 -0
  11. package/dist/handlers/artifact-handlers.js.map +1 -0
  12. package/dist/handlers/cost-handlers.d.ts +8 -0
  13. package/dist/handlers/cost-handlers.d.ts.map +1 -0
  14. package/dist/handlers/cost-handlers.js +140 -0
  15. package/dist/handlers/cost-handlers.js.map +1 -0
  16. package/dist/handlers/handlers.agent.d.ts +10 -0
  17. package/dist/handlers/handlers.agent.d.ts.map +1 -0
  18. package/dist/handlers/handlers.agent.js +99 -0
  19. package/dist/handlers/handlers.agent.js.map +1 -0
  20. package/dist/handlers/handlers.base.d.ts +82 -0
  21. package/dist/handlers/handlers.base.d.ts.map +1 -0
  22. package/dist/handlers/handlers.base.js +337 -0
  23. package/dist/handlers/handlers.base.js.map +1 -0
  24. package/dist/handlers/handlers.lock.d.ts +8 -0
  25. package/dist/handlers/handlers.lock.d.ts.map +1 -0
  26. package/dist/handlers/handlers.lock.js +111 -0
  27. package/dist/handlers/handlers.lock.js.map +1 -0
  28. package/dist/handlers/handlers.memory.d.ts +11 -0
  29. package/dist/handlers/handlers.memory.d.ts.map +1 -0
  30. package/dist/handlers/handlers.memory.js +122 -0
  31. package/dist/handlers/handlers.memory.js.map +1 -0
  32. package/dist/handlers/handlers.monitoring.d.ts +8 -0
  33. package/dist/handlers/handlers.monitoring.d.ts.map +1 -0
  34. package/dist/handlers/handlers.monitoring.js +99 -0
  35. package/dist/handlers/handlers.monitoring.js.map +1 -0
  36. package/dist/handlers/handlers.orchestration.d.ts +9 -0
  37. package/dist/handlers/handlers.orchestration.d.ts.map +1 -0
  38. package/dist/handlers/handlers.orchestration.js +128 -0
  39. package/dist/handlers/handlers.orchestration.js.map +1 -0
  40. package/dist/handlers/handlers.session.d.ts +18 -0
  41. package/dist/handlers/handlers.session.d.ts.map +1 -0
  42. package/dist/handlers/handlers.session.js +286 -0
  43. package/dist/handlers/handlers.session.js.map +1 -0
  44. package/dist/handlers/handlers.task.d.ts +15 -0
  45. package/dist/handlers/handlers.task.d.ts.map +1 -0
  46. package/dist/handlers/handlers.task.js +753 -0
  47. package/dist/handlers/handlers.task.js.map +1 -0
  48. package/dist/handlers/handlers.utility.d.ts +10 -0
  49. package/dist/handlers/handlers.utility.d.ts.map +1 -0
  50. package/dist/handlers/handlers.utility.js +59 -0
  51. package/dist/handlers/handlers.utility.js.map +1 -0
  52. package/dist/handlers/index.d.ts +18 -0
  53. package/dist/handlers/index.d.ts.map +1 -0
  54. package/dist/handlers/index.js +209 -0
  55. package/dist/handlers/index.js.map +1 -0
  56. package/dist/handlers/lock-handlers.d.ts +8 -0
  57. package/dist/handlers/lock-handlers.d.ts.map +1 -0
  58. package/dist/handlers/lock-handlers.js +154 -0
  59. package/dist/handlers/lock-handlers.js.map +1 -0
  60. package/dist/handlers/memory-handlers.d.ts +8 -0
  61. package/dist/handlers/memory-handlers.d.ts.map +1 -0
  62. package/dist/handlers/memory-handlers.js +76 -0
  63. package/dist/handlers/memory-handlers.js.map +1 -0
  64. package/dist/handlers/orchestration-handlers.d.ts +8 -0
  65. package/dist/handlers/orchestration-handlers.d.ts.map +1 -0
  66. package/dist/handlers/orchestration-handlers.js +113 -0
  67. package/dist/handlers/orchestration-handlers.js.map +1 -0
  68. package/dist/handlers/session-handlers.d.ts +8 -0
  69. package/dist/handlers/session-handlers.d.ts.map +1 -0
  70. package/dist/handlers/session-handlers.js +558 -0
  71. package/dist/handlers/session-handlers.js.map +1 -0
  72. package/dist/handlers/task-handlers.d.ts +8 -0
  73. package/dist/handlers/task-handlers.d.ts.map +1 -0
  74. package/dist/handlers/task-handlers.js +677 -0
  75. package/dist/handlers/task-handlers.js.map +1 -0
  76. package/dist/handlers/tool-definitions.d.ts +2626 -0
  77. package/dist/handlers/tool-definitions.d.ts.map +1 -0
  78. package/dist/handlers/tool-definitions.js +641 -0
  79. package/dist/handlers/tool-definitions.js.map +1 -0
  80. package/dist/handlers/types.d.ts +90 -0
  81. package/dist/handlers/types.d.ts.map +1 -0
  82. package/dist/handlers/types.js +5 -0
  83. package/dist/handlers/types.js.map +1 -0
  84. package/dist/handlers/utility-handlers.d.ts +8 -0
  85. package/dist/handlers/utility-handlers.d.ts.map +1 -0
  86. package/dist/handlers/utility-handlers.js +113 -0
  87. package/dist/handlers/utility-handlers.js.map +1 -0
  88. package/dist/handlers/utils.d.ts +30 -0
  89. package/dist/handlers/utils.d.ts.map +1 -0
  90. package/dist/handlers/utils.js +95 -0
  91. package/dist/handlers/utils.js.map +1 -0
  92. package/dist/handlers.d.ts +17 -2260
  93. package/dist/handlers.d.ts.map +1 -1
  94. package/dist/handlers.js +17 -1836
  95. package/dist/handlers.js.map +1 -1
  96. package/dist/index.js +41 -7
  97. package/dist/index.js.map +1 -1
  98. package/dist/persistence.d.ts.map +1 -1
  99. package/dist/persistence.js +84 -47
  100. package/dist/persistence.js.map +1 -1
  101. package/dist/schemas.d.ts +299 -0
  102. package/dist/schemas.d.ts.map +1 -0
  103. package/dist/schemas.js +334 -0
  104. package/dist/schemas.js.map +1 -0
  105. package/package.json +8 -5
@@ -0,0 +1,753 @@
1
+ /**
2
+ * MCP Tool Handlers - Task Management
3
+ * Handles: task_create, task_queue, task_status, task_complete, task_list, task_execute, task_cancel, task_retry, task_batch_create, task_batch_execute
4
+ */
5
+ import { AdapterRegistry } from '@anastops/adapters';
6
+ import { SessionManager, IntelligentRouter, addBreadcrumb, captureError, ProviderFailoverService, AllProvidersFailedError, } from '@anastops/core';
7
+ import { nanoid } from 'nanoid';
8
+ import { getPersistence } from '../persistence.js';
9
+ import { safePersist, tasks, getTask } from './handlers.base.js';
10
+ // Shared instances
11
+ const router = new IntelligentRouter();
12
+ const registry = AdapterRegistry.getInstance();
13
+ const sessionManager = new SessionManager();
14
+ const failoverService = new ProviderFailoverService(router);
15
+ /**
16
+ * Process the task queue for a session.
17
+ * Starts queued tasks up to the concurrency limit if auto_execute is enabled.
18
+ * Returns the number of tasks started.
19
+ */
20
+ function processTaskQueue(sessionId) {
21
+ // Check if session exists and has auto_execute enabled
22
+ if (!sessionManager.exists(sessionId)) {
23
+ return Promise.resolve(0);
24
+ }
25
+ const session = sessionManager.getSession(sessionId);
26
+ if (session.queue_config.auto_execute !== true) {
27
+ return Promise.resolve(0);
28
+ }
29
+ // Get all tasks for the session
30
+ const sessionTasks = [];
31
+ for (const [, task] of tasks.entries()) {
32
+ if (task.session_id === sessionId) {
33
+ sessionTasks.push(task);
34
+ }
35
+ }
36
+ // Count running tasks
37
+ const runningCount = sessionTasks.filter((t) => t.status === 'running').length;
38
+ const availableSlots = Math.max(0, session.queue_config.concurrency - runningCount);
39
+ if (availableSlots === 0) {
40
+ return Promise.resolve(0);
41
+ }
42
+ // Get queued tasks that have met dependencies
43
+ const taskMap = new Map(sessionTasks.map((t) => [t.id, t]));
44
+ const queuedTasks = sessionTasks
45
+ .filter((t) => t.status === 'queued')
46
+ .filter((task) => {
47
+ // Check if all dependencies are completed
48
+ if (task.dependencies.length === 0)
49
+ return true;
50
+ return task.dependencies.every((depId) => {
51
+ const depTask = taskMap.get(depId);
52
+ return depTask !== undefined && depTask.status === 'completed';
53
+ });
54
+ })
55
+ .sort((a, b) => {
56
+ // Sort by priority (higher first), then by created_at (earlier first)
57
+ if (a.priority !== b.priority)
58
+ return b.priority - a.priority;
59
+ return a.created_at.getTime() - b.created_at.getTime();
60
+ })
61
+ .slice(0, availableSlots);
62
+ // Start tasks (fire and forget)
63
+ let startedCount = 0;
64
+ for (const task of queuedTasks) {
65
+ // Execute task in background - don't await
66
+ void executeTaskInBackground(task.id);
67
+ startedCount++;
68
+ }
69
+ return Promise.resolve(startedCount);
70
+ }
71
+ /**
72
+ * Execute a task in the background.
73
+ * Used by queue processing to start tasks without blocking.
74
+ */
75
+ async function executeTaskInBackground(taskId) {
76
+ try {
77
+ await handleTaskExecuteInternal(taskId);
78
+ }
79
+ catch {
80
+ // Error is already handled and persisted in handleTaskExecuteInternal
81
+ }
82
+ }
83
+ /**
84
+ * Internal task execution logic.
85
+ * Shared between handleTaskExecute and background execution.
86
+ */
87
+ async function handleTaskExecuteInternal(taskId) {
88
+ const task = await getTask(taskId);
89
+ if (task === null) {
90
+ throw new Error(`Task not found: ${taskId}`);
91
+ }
92
+ if (task.status !== 'pending' && task.status !== 'queued') {
93
+ return {
94
+ task_id: taskId,
95
+ status: task.status,
96
+ error: `Task cannot be executed - current status is '${task.status}'`,
97
+ };
98
+ }
99
+ addBreadcrumb('Executing task', { task_id: taskId, provider: task.provider }, 'info', 'task');
100
+ // Update task status to running
101
+ task.status = 'running';
102
+ task.started_at = new Date();
103
+ tasks.set(taskId, task);
104
+ safePersist(getPersistence().saveTask(task));
105
+ try {
106
+ const adapter = registry.get(task.provider);
107
+ if (adapter === undefined) {
108
+ throw new Error(`Provider adapter not found: ${task.provider}`);
109
+ }
110
+ const workingDir = process.env['ANASTOPS_WORKSPACE'] ?? process.cwd();
111
+ const taskInput = task.input;
112
+ const executeRequest = {
113
+ model: task.model,
114
+ prompt: taskInput?.prompt ?? task.description,
115
+ working_dir: workingDir,
116
+ };
117
+ if (taskInput?.agent !== undefined && taskInput.agent !== '') {
118
+ executeRequest.agent = taskInput.agent;
119
+ }
120
+ if (taskInput?.skills !== undefined && taskInput.skills.length > 0) {
121
+ executeRequest.skills = taskInput.skills;
122
+ }
123
+ const response = await adapter.execute(executeRequest, { workingDir });
124
+ task.status = 'completed';
125
+ task.completed_at = new Date();
126
+ task.token_usage = response.usage ?? task.token_usage;
127
+ task.output = {
128
+ content: response.content,
129
+ artifacts: [],
130
+ files_modified: [],
131
+ metadata: { usage: response.usage },
132
+ };
133
+ tasks.set(taskId, task);
134
+ safePersist(getPersistence().saveTask(task));
135
+ addBreadcrumb('Task completed', {
136
+ task_id: taskId,
137
+ tokens_used: response.usage?.total_tokens ?? 0,
138
+ }, 'info', 'task');
139
+ if (sessionManager.exists(task.session_id)) {
140
+ const session = sessionManager.getSession(task.session_id);
141
+ session.metadata = session.metadata ?? {
142
+ total_tokens: 0,
143
+ total_cost: 0,
144
+ agents_used: [],
145
+ files_affected: [],
146
+ tasks_completed: 0,
147
+ tasks_failed: 0,
148
+ };
149
+ session.metadata.tasks_completed = (session.metadata.tasks_completed ?? 0) + 1;
150
+ session.metadata.total_tokens =
151
+ (session.metadata.total_tokens ?? 0) + (response.usage?.total_tokens ?? 0);
152
+ session.metadata.total_cost =
153
+ (session.metadata.total_cost ?? 0) + (response.usage?.cost ?? 0);
154
+ session.updated_at = new Date();
155
+ safePersist(getPersistence().saveSession(session));
156
+ }
157
+ // Process queue to start next tasks
158
+ void processTaskQueue(task.session_id);
159
+ return {
160
+ task_id: taskId,
161
+ status: 'completed',
162
+ result: response,
163
+ };
164
+ }
165
+ catch (error) {
166
+ task.status = 'failed';
167
+ task.completed_at = new Date();
168
+ task.error = error instanceof Error ? error.message : String(error);
169
+ tasks.set(taskId, task);
170
+ safePersist(getPersistence().saveTask(task));
171
+ addBreadcrumb('Task failed', {
172
+ task_id: taskId,
173
+ error: task.error,
174
+ }, 'error', 'task');
175
+ captureError(error, {
176
+ task_id: taskId,
177
+ provider: task.provider,
178
+ type: task.type,
179
+ });
180
+ if (sessionManager.exists(task.session_id)) {
181
+ const session = sessionManager.getSession(task.session_id);
182
+ session.metadata = session.metadata ?? {
183
+ total_tokens: 0,
184
+ total_cost: 0,
185
+ agents_used: [],
186
+ files_affected: [],
187
+ tasks_completed: 0,
188
+ tasks_failed: 0,
189
+ };
190
+ session.metadata.tasks_failed = (session.metadata.tasks_failed ?? 0) + 1;
191
+ session.updated_at = new Date();
192
+ safePersist(getPersistence().saveSession(session));
193
+ }
194
+ // Process queue to start next tasks even after failure
195
+ void processTaskQueue(task.session_id);
196
+ throw error;
197
+ }
198
+ }
199
+ export function handleTaskCreate(args) {
200
+ const taskId = nanoid(21);
201
+ const now = new Date();
202
+ const taskType = args['type'] ?? 'other';
203
+ const sessionId = args['session_id'];
204
+ const description = args['description'];
205
+ addBreadcrumb('Creating task', { session_id: sessionId, type: taskType, description }, 'info', 'task');
206
+ // Store agent and skills in task input for execution
207
+ const taskInput = {
208
+ prompt: args['prompt'],
209
+ context_files: args['context_files'] ?? [],
210
+ };
211
+ if (args['agent'] !== undefined) {
212
+ taskInput.agent = args['agent'];
213
+ }
214
+ if (args['skills'] !== undefined) {
215
+ taskInput.skills = args['skills'];
216
+ }
217
+ const task = {
218
+ id: taskId,
219
+ session_id: sessionId,
220
+ agent_id: null,
221
+ type: taskType,
222
+ status: 'pending',
223
+ description,
224
+ input: taskInput,
225
+ output: null,
226
+ error: null,
227
+ complexity_score: 0,
228
+ routing_tier: 3,
229
+ provider: 'claude',
230
+ model: 'claude-sonnet',
231
+ token_usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, cost: 0 },
232
+ created_at: now,
233
+ started_at: null,
234
+ completed_at: null,
235
+ dependencies: [],
236
+ priority: 5,
237
+ retry_count: 0,
238
+ max_retries: 3,
239
+ failover_history: [],
240
+ failover_enabled: args['disable_failover'] !== true,
241
+ };
242
+ // Route the task with optional overrides
243
+ const routingOverride = {};
244
+ if (args['force_provider'] !== undefined) {
245
+ routingOverride.force_provider = args['force_provider'];
246
+ }
247
+ if (args['force_tier'] !== undefined) {
248
+ routingOverride.force_tier = args['force_tier'];
249
+ }
250
+ const routing = router.route(task, routingOverride);
251
+ task.routing_tier = routing.tier;
252
+ task.provider = routing.provider;
253
+ task.model = routing.model;
254
+ task.complexity_score = routing.complexity_score;
255
+ addBreadcrumb('Task routed', { task_id: taskId, provider: routing.provider, tier: routing.tier }, 'info', 'task');
256
+ tasks.set(taskId, task);
257
+ // Persist to MongoDB
258
+ safePersist(getPersistence().saveTask(task));
259
+ const response = {
260
+ task_id: task.id,
261
+ type: task.type,
262
+ routing: { tier: routing.tier, provider: routing.provider, model: routing.model },
263
+ };
264
+ if (taskInput.agent !== undefined && taskInput.agent !== '') {
265
+ response.agent = taskInput.agent;
266
+ }
267
+ return Promise.resolve(response);
268
+ }
269
+ export async function handleTaskQueue(args) {
270
+ const task = await getTask(args['task_id']);
271
+ if (task === null)
272
+ throw new Error('Task not found');
273
+ task.status = 'queued';
274
+ tasks.set(task.id, task);
275
+ safePersist(getPersistence().saveTask(task));
276
+ // Trigger queue processing to potentially start this task
277
+ const tasksStarted = await processTaskQueue(task.session_id);
278
+ return {
279
+ task_id: task.id,
280
+ status: task.status,
281
+ queue_processed: tasksStarted > 0,
282
+ tasks_started: tasksStarted,
283
+ };
284
+ }
285
+ export async function handleTaskStatus(args) {
286
+ const task = await getTask(args['task_id']);
287
+ if (task === null)
288
+ throw new Error('Task not found');
289
+ return task;
290
+ }
291
+ export async function handleTaskComplete(args) {
292
+ const task = await getTask(args['task_id']);
293
+ if (task === null)
294
+ throw new Error('Task not found');
295
+ task.status = 'completed';
296
+ task.completed_at = new Date();
297
+ task.output = {
298
+ content: args['content'],
299
+ artifacts: args['artifacts'] ?? [],
300
+ files_modified: [],
301
+ metadata: {},
302
+ };
303
+ tasks.set(task.id, task);
304
+ // Persist to MongoDB
305
+ safePersist(getPersistence().saveTask(task));
306
+ return { task_id: task.id, status: task.status };
307
+ }
308
+ export async function handleTaskList(args) {
309
+ // Get from in-memory cache first
310
+ const inMemoryTasks = [];
311
+ for (const [, task] of tasks.entries()) {
312
+ if (args['session_id'] === undefined || task.session_id === args['session_id']) {
313
+ inMemoryTasks.push(task);
314
+ }
315
+ }
316
+ // Also get from MongoDB for persistence
317
+ const persistedTasks = await getPersistence().listTasks({
318
+ ...(args['session_id'] !== undefined && { session_id: args['session_id'] }),
319
+ });
320
+ // Merge: in-memory takes precedence
321
+ const inMemoryIds = new Set(inMemoryTasks.map((t) => t.id));
322
+ const sessionTasks = [...inMemoryTasks, ...persistedTasks.filter((t) => !inMemoryIds.has(t.id))];
323
+ return {
324
+ count: sessionTasks.length,
325
+ tasks: sessionTasks.map((t) => ({
326
+ id: t.id,
327
+ type: t.type,
328
+ status: t.status,
329
+ description: t.description.slice(0, 50),
330
+ })),
331
+ };
332
+ }
333
+ export async function handleTaskExecute(args) {
334
+ const taskId = args['task_id'];
335
+ const wait = args['wait'] ?? true;
336
+ const task = await getTask(taskId);
337
+ if (task === null) {
338
+ throw new Error(`Task not found: ${taskId}`);
339
+ }
340
+ if (task.status !== 'pending' && task.status !== 'queued') {
341
+ return {
342
+ task_id: taskId,
343
+ status: task.status,
344
+ error: `Task cannot be executed - current status is '${task.status}'`,
345
+ };
346
+ }
347
+ addBreadcrumb('Executing task', { task_id: taskId, provider: task.provider, failover_enabled: task.failover_enabled }, 'info', 'task');
348
+ // Update task status to running
349
+ task.status = 'running';
350
+ task.started_at = new Date();
351
+ tasks.set(taskId, task);
352
+ // Persist running status
353
+ safePersist(getPersistence().saveTask(task));
354
+ try {
355
+ // Use workspace root from environment or cwd
356
+ const workingDir = process.env['ANASTOPS_WORKSPACE'] ?? process.cwd();
357
+ // Build request with optional agent and skills
358
+ const taskInput = task.input;
359
+ const executeRequest = {
360
+ model: task.model,
361
+ prompt: taskInput?.prompt ?? task.description,
362
+ working_dir: workingDir,
363
+ };
364
+ // Execute with failover support
365
+ const failoverResult = await failoverService.executeWithFailover(task, executeRequest, registry, { workingDir });
366
+ const response = failoverResult.response;
367
+ // Update task with result
368
+ task.status = 'completed';
369
+ task.completed_at = new Date();
370
+ task.provider = failoverResult.successful_provider;
371
+ task.routing_tier = failoverResult.successful_tier;
372
+ task.failover_history = failoverResult.attempt_history;
373
+ task.token_usage = response.usage ?? task.token_usage;
374
+ task.output = {
375
+ content: response.content,
376
+ artifacts: [],
377
+ files_modified: [],
378
+ metadata: {
379
+ usage: response.usage,
380
+ failover_used: failoverResult.failover_used,
381
+ failover_attempts: failoverResult.failover_attempts,
382
+ },
383
+ };
384
+ tasks.set(taskId, task);
385
+ // Persist completed task
386
+ safePersist(getPersistence().saveTask(task));
387
+ addBreadcrumb('Task completed', {
388
+ task_id: taskId,
389
+ tokens_used: response.usage?.total_tokens ?? 0,
390
+ failover_used: failoverResult.failover_used,
391
+ final_provider: failoverResult.successful_provider,
392
+ }, 'info', 'task');
393
+ // Update session metadata via sessionManager
394
+ if (sessionManager.exists(task.session_id)) {
395
+ const session = sessionManager.getSession(task.session_id);
396
+ session.metadata = session.metadata ?? {
397
+ total_tokens: 0,
398
+ total_cost: 0,
399
+ agents_used: [],
400
+ files_affected: [],
401
+ tasks_completed: 0,
402
+ tasks_failed: 0,
403
+ };
404
+ session.metadata.tasks_completed = (session.metadata.tasks_completed ?? 0) + 1;
405
+ session.metadata.total_tokens =
406
+ (session.metadata.total_tokens ?? 0) + (response.usage?.total_tokens ?? 0);
407
+ session.metadata.total_cost =
408
+ (session.metadata.total_cost ?? 0) + (response.usage?.cost ?? 0);
409
+ session.updated_at = new Date();
410
+ safePersist(getPersistence().saveSession(session));
411
+ }
412
+ // Process queue to start next tasks after completion
413
+ void processTaskQueue(task.session_id);
414
+ return {
415
+ task_id: taskId,
416
+ status: 'completed',
417
+ result: response,
418
+ waited: wait,
419
+ failover: failoverResult.failover_used
420
+ ? {
421
+ used: true,
422
+ attempts: failoverResult.failover_attempts,
423
+ final_provider: failoverResult.successful_provider,
424
+ final_tier: failoverResult.successful_tier,
425
+ }
426
+ : undefined,
427
+ };
428
+ }
429
+ catch (error) {
430
+ // Get failover history from error if available
431
+ const failoverHistory = error instanceof AllProvidersFailedError ? error.failoverHistory : [];
432
+ task.status = 'failed';
433
+ task.completed_at = new Date();
434
+ task.error = error instanceof Error ? error.message : String(error);
435
+ task.failover_history = failoverHistory;
436
+ tasks.set(taskId, task);
437
+ // Persist failed task
438
+ safePersist(getPersistence().saveTask(task));
439
+ addBreadcrumb('Task failed', {
440
+ task_id: taskId,
441
+ error: task.error,
442
+ failover_attempts: failoverHistory.length,
443
+ }, 'error', 'task');
444
+ // Capture task execution error
445
+ captureError(error, {
446
+ task_id: taskId,
447
+ provider: task.provider,
448
+ type: task.type,
449
+ failover_attempts: failoverHistory.length,
450
+ });
451
+ // Update session metadata for failure via sessionManager
452
+ if (sessionManager.exists(task.session_id)) {
453
+ const session = sessionManager.getSession(task.session_id);
454
+ session.metadata = session.metadata ?? {
455
+ total_tokens: 0,
456
+ total_cost: 0,
457
+ agents_used: [],
458
+ files_affected: [],
459
+ tasks_completed: 0,
460
+ tasks_failed: 0,
461
+ };
462
+ session.metadata.tasks_failed = (session.metadata.tasks_failed ?? 0) + 1;
463
+ session.updated_at = new Date();
464
+ safePersist(getPersistence().saveSession(session));
465
+ }
466
+ // Process queue to start next tasks even after failure
467
+ void processTaskQueue(task.session_id);
468
+ throw error;
469
+ }
470
+ }
471
+ export async function handleTaskCancel(args) {
472
+ const taskId = args['task_id'];
473
+ const task = await getTask(taskId);
474
+ if (task === null) {
475
+ throw new Error(`Task not found: ${taskId}`);
476
+ }
477
+ // Check if task is cancelable (pending, queued, or running)
478
+ const cancelableStatuses = ['pending', 'queued', 'running'];
479
+ if (!cancelableStatuses.includes(task.status)) {
480
+ return {
481
+ task_id: taskId,
482
+ status: task.status,
483
+ cancelled: false,
484
+ error: `Task cannot be cancelled - current status is '${task.status}'`,
485
+ };
486
+ }
487
+ // Cancel the task
488
+ task.status = 'cancelled';
489
+ task.completed_at = new Date();
490
+ tasks.set(taskId, task);
491
+ // Persist to MongoDB
492
+ safePersist(getPersistence().saveTask(task));
493
+ return {
494
+ task_id: taskId,
495
+ status: task.status,
496
+ cancelled: true,
497
+ };
498
+ }
499
+ export async function handleTaskRetry(args) {
500
+ const taskId = args['task_id'];
501
+ const task = await getTask(taskId);
502
+ if (task === null) {
503
+ throw new Error(`Task not found: ${taskId}`);
504
+ }
505
+ // Check if task is in failed status
506
+ if (task.status !== 'failed') {
507
+ return {
508
+ task_id: taskId,
509
+ status: task.status,
510
+ retried: false,
511
+ error: `Task cannot be retried - current status is '${task.status}' (must be 'failed')`,
512
+ };
513
+ }
514
+ // Check if max retries exceeded
515
+ if (task.retry_count >= task.max_retries) {
516
+ return {
517
+ task_id: taskId,
518
+ status: task.status,
519
+ retried: false,
520
+ error: `Task has exceeded max retries (${task.max_retries})`,
521
+ retry_count: task.retry_count,
522
+ };
523
+ }
524
+ // Retry the task
525
+ task.status = 'pending';
526
+ task.retry_count += 1;
527
+ task.error = null;
528
+ task.started_at = null;
529
+ task.completed_at = null;
530
+ tasks.set(taskId, task);
531
+ // Persist to MongoDB
532
+ safePersist(getPersistence().saveTask(task));
533
+ return {
534
+ task_id: taskId,
535
+ status: task.status,
536
+ retried: true,
537
+ retry_count: task.retry_count,
538
+ max_retries: task.max_retries,
539
+ };
540
+ }
541
+ export function handleTaskBatchCreate(args) {
542
+ const sessionId = args['session_id'];
543
+ const taskDefs = args['tasks'];
544
+ const createdTasks = [];
545
+ const now = new Date();
546
+ for (const def of taskDefs) {
547
+ const taskId = nanoid(21);
548
+ const taskType = def.type ?? 'other';
549
+ const taskInput = {
550
+ prompt: def.prompt,
551
+ context_files: def.context_files ?? [],
552
+ };
553
+ if (def.agent !== undefined) {
554
+ taskInput.agent = def.agent;
555
+ }
556
+ if (def.skills !== undefined) {
557
+ taskInput.skills = def.skills;
558
+ }
559
+ const provider = def.force_provider ?? 'claude';
560
+ const model = 'claude-sonnet';
561
+ const tier = def.force_tier ?? 3;
562
+ const task = {
563
+ id: taskId,
564
+ session_id: sessionId,
565
+ agent_id: null,
566
+ type: taskType,
567
+ status: 'pending',
568
+ description: def.description,
569
+ input: taskInput,
570
+ output: null,
571
+ error: null,
572
+ complexity_score: 50,
573
+ routing_tier: tier,
574
+ provider: provider,
575
+ model: model,
576
+ token_usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0, cost: 0 },
577
+ created_at: now,
578
+ started_at: null,
579
+ completed_at: null,
580
+ dependencies: [],
581
+ priority: 5,
582
+ retry_count: 0,
583
+ max_retries: 3,
584
+ failover_history: [],
585
+ failover_enabled: true, // Failover enabled by default for batch tasks
586
+ };
587
+ tasks.set(taskId, task);
588
+ safePersist(getPersistence().saveTask(task));
589
+ const createdTask = {
590
+ task_id: taskId,
591
+ type: task.type,
592
+ routing: { tier, provider, model },
593
+ };
594
+ if (taskInput.agent !== undefined) {
595
+ createdTask.agent = taskInput.agent;
596
+ }
597
+ createdTasks.push(createdTask);
598
+ }
599
+ return Promise.resolve({
600
+ created: createdTasks.length,
601
+ task_ids: createdTasks.map((t) => t.task_id),
602
+ tasks: createdTasks,
603
+ });
604
+ }
605
+ export async function handleTaskBatchExecute(args) {
606
+ const taskIds = args['task_ids'];
607
+ const parallel = args['parallel'] ?? true;
608
+ const wait = args['wait'] ?? true;
609
+ const executeOne = async (taskId) => {
610
+ const task = await getTask(taskId);
611
+ if (task === null) {
612
+ return { task_id: taskId, status: 'not_found', error: `Task not found: ${taskId}` };
613
+ }
614
+ if (task.status !== 'pending' && task.status !== 'queued') {
615
+ return {
616
+ task_id: taskId,
617
+ status: task.status,
618
+ error: `Task cannot be executed - current status is '${task.status}'`,
619
+ };
620
+ }
621
+ task.status = 'running';
622
+ task.started_at = new Date();
623
+ tasks.set(taskId, task);
624
+ safePersist(getPersistence().saveTask(task));
625
+ try {
626
+ const workingDir = process.env['ANASTOPS_WORKSPACE'] ?? process.cwd();
627
+ const taskInput = task.input;
628
+ const executeRequest = {
629
+ model: task.model,
630
+ prompt: taskInput?.prompt ?? task.description,
631
+ working_dir: workingDir,
632
+ };
633
+ // Execute with failover support
634
+ const failoverResult = await failoverService.executeWithFailover(task, executeRequest, registry, { workingDir });
635
+ const response = failoverResult.response;
636
+ task.status = 'completed';
637
+ task.completed_at = new Date();
638
+ task.provider = failoverResult.successful_provider;
639
+ task.routing_tier = failoverResult.successful_tier;
640
+ task.failover_history = failoverResult.attempt_history;
641
+ task.token_usage = response.usage ?? task.token_usage;
642
+ task.output = {
643
+ content: response.content,
644
+ artifacts: [],
645
+ files_modified: [],
646
+ metadata: {
647
+ usage: response.usage,
648
+ failover_used: failoverResult.failover_used,
649
+ failover_attempts: failoverResult.failover_attempts,
650
+ },
651
+ };
652
+ tasks.set(taskId, task);
653
+ safePersist(getPersistence().saveTask(task));
654
+ if (sessionManager.exists(task.session_id)) {
655
+ const session = sessionManager.getSession(task.session_id);
656
+ session.metadata = session.metadata ?? {
657
+ total_tokens: 0,
658
+ total_cost: 0,
659
+ agents_used: [],
660
+ files_affected: [],
661
+ tasks_completed: 0,
662
+ tasks_failed: 0,
663
+ };
664
+ session.metadata.tasks_completed = (session.metadata.tasks_completed ?? 0) + 1;
665
+ session.metadata.total_tokens =
666
+ (session.metadata.total_tokens ?? 0) + (response.usage?.total_tokens ?? 0);
667
+ session.metadata.total_cost =
668
+ (session.metadata.total_cost ?? 0) + (response.usage?.cost ?? 0);
669
+ session.updated_at = new Date();
670
+ safePersist(getPersistence().saveSession(session));
671
+ }
672
+ const batchResult = {
673
+ task_id: taskId,
674
+ status: 'completed',
675
+ result: { content: response.content, usage: response.usage },
676
+ };
677
+ if (failoverResult.failover_used) {
678
+ batchResult.failover = {
679
+ used: true,
680
+ attempts: failoverResult.failover_attempts,
681
+ final_provider: failoverResult.successful_provider,
682
+ final_tier: failoverResult.successful_tier,
683
+ };
684
+ }
685
+ return batchResult;
686
+ }
687
+ catch (error) {
688
+ // Get failover history from error if available
689
+ const failoverHistory = error instanceof AllProvidersFailedError ? error.failoverHistory : [];
690
+ task.status = 'failed';
691
+ task.completed_at = new Date();
692
+ task.error = error instanceof Error ? error.message : String(error);
693
+ task.failover_history = failoverHistory;
694
+ tasks.set(taskId, task);
695
+ safePersist(getPersistence().saveTask(task));
696
+ if (sessionManager.exists(task.session_id)) {
697
+ const session = sessionManager.getSession(task.session_id);
698
+ session.metadata = session.metadata ?? {
699
+ total_tokens: 0,
700
+ total_cost: 0,
701
+ agents_used: [],
702
+ files_affected: [],
703
+ tasks_completed: 0,
704
+ tasks_failed: 0,
705
+ };
706
+ session.metadata.tasks_failed = (session.metadata.tasks_failed ?? 0) + 1;
707
+ session.updated_at = new Date();
708
+ safePersist(getPersistence().saveSession(session));
709
+ }
710
+ return {
711
+ task_id: taskId,
712
+ status: 'failed',
713
+ error: error instanceof Error ? error.message : String(error),
714
+ };
715
+ }
716
+ };
717
+ let results;
718
+ if (parallel) {
719
+ results = await Promise.all(taskIds.map(executeOne));
720
+ }
721
+ else {
722
+ results = [];
723
+ for (const taskId of taskIds) {
724
+ results.push(await executeOne(taskId));
725
+ }
726
+ }
727
+ const failoverUsedCount = results.filter((r) => r.failover?.used).length;
728
+ const summary = {
729
+ completed: results.filter((r) => r.status === 'completed').length,
730
+ failed: results.filter((r) => r.status === 'failed').length,
731
+ skipped: results.filter((r) => r.status !== 'completed' && r.status !== 'failed').length,
732
+ failover_used: failoverUsedCount,
733
+ };
734
+ // Process queues for all sessions involved
735
+ const sessionIds = new Set();
736
+ for (const taskId of taskIds) {
737
+ const task = await getTask(taskId);
738
+ if (task !== null) {
739
+ sessionIds.add(task.session_id);
740
+ }
741
+ }
742
+ for (const sessionId of sessionIds) {
743
+ void processTaskQueue(sessionId);
744
+ }
745
+ return {
746
+ executed: results.length,
747
+ parallel,
748
+ waited: wait,
749
+ results,
750
+ summary,
751
+ };
752
+ }
753
+ //# sourceMappingURL=handlers.task.js.map