@juspay/neurolink 9.39.0 → 9.41.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 (58) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/browser/neurolink.min.js +445 -431
  3. package/dist/cli/commands/task.d.ts +56 -0
  4. package/dist/cli/commands/task.js +835 -0
  5. package/dist/cli/parser.js +4 -1
  6. package/dist/lib/neurolink.d.ts +22 -1
  7. package/dist/lib/neurolink.js +195 -14
  8. package/dist/lib/tasks/backends/bullmqBackend.d.ts +32 -0
  9. package/dist/lib/tasks/backends/bullmqBackend.js +189 -0
  10. package/dist/lib/tasks/backends/nodeTimeoutBackend.d.ts +27 -0
  11. package/dist/lib/tasks/backends/nodeTimeoutBackend.js +141 -0
  12. package/dist/lib/tasks/backends/taskBackendRegistry.d.ts +31 -0
  13. package/dist/lib/tasks/backends/taskBackendRegistry.js +66 -0
  14. package/dist/lib/tasks/errors.d.ts +31 -0
  15. package/dist/lib/tasks/errors.js +18 -0
  16. package/dist/lib/tasks/store/fileTaskStore.d.ts +43 -0
  17. package/dist/lib/tasks/store/fileTaskStore.js +179 -0
  18. package/dist/lib/tasks/store/redisTaskStore.d.ts +42 -0
  19. package/dist/lib/tasks/store/redisTaskStore.js +189 -0
  20. package/dist/lib/tasks/taskExecutor.d.ts +21 -0
  21. package/dist/lib/tasks/taskExecutor.js +166 -0
  22. package/dist/lib/tasks/taskManager.d.ts +60 -0
  23. package/dist/lib/tasks/taskManager.js +393 -0
  24. package/dist/lib/tasks/tools/taskTools.d.ts +135 -0
  25. package/dist/lib/tasks/tools/taskTools.js +274 -0
  26. package/dist/lib/types/configTypes.d.ts +3 -0
  27. package/dist/lib/types/generateTypes.d.ts +42 -0
  28. package/dist/lib/types/index.d.ts +2 -1
  29. package/dist/lib/types/streamTypes.d.ts +7 -0
  30. package/dist/lib/types/taskTypes.d.ts +275 -0
  31. package/dist/lib/types/taskTypes.js +37 -0
  32. package/dist/neurolink.d.ts +22 -1
  33. package/dist/neurolink.js +195 -14
  34. package/dist/tasks/backends/bullmqBackend.d.ts +32 -0
  35. package/dist/tasks/backends/bullmqBackend.js +188 -0
  36. package/dist/tasks/backends/nodeTimeoutBackend.d.ts +27 -0
  37. package/dist/tasks/backends/nodeTimeoutBackend.js +140 -0
  38. package/dist/tasks/backends/taskBackendRegistry.d.ts +31 -0
  39. package/dist/tasks/backends/taskBackendRegistry.js +65 -0
  40. package/dist/tasks/errors.d.ts +31 -0
  41. package/dist/tasks/errors.js +17 -0
  42. package/dist/tasks/store/fileTaskStore.d.ts +43 -0
  43. package/dist/tasks/store/fileTaskStore.js +178 -0
  44. package/dist/tasks/store/redisTaskStore.d.ts +42 -0
  45. package/dist/tasks/store/redisTaskStore.js +188 -0
  46. package/dist/tasks/taskExecutor.d.ts +21 -0
  47. package/dist/tasks/taskExecutor.js +165 -0
  48. package/dist/tasks/taskManager.d.ts +60 -0
  49. package/dist/tasks/taskManager.js +392 -0
  50. package/dist/tasks/tools/taskTools.d.ts +135 -0
  51. package/dist/tasks/tools/taskTools.js +273 -0
  52. package/dist/types/configTypes.d.ts +3 -0
  53. package/dist/types/generateTypes.d.ts +42 -0
  54. package/dist/types/index.d.ts +2 -1
  55. package/dist/types/streamTypes.d.ts +7 -0
  56. package/dist/types/taskTypes.d.ts +275 -0
  57. package/dist/types/taskTypes.js +36 -0
  58. package/package.json +4 -2
@@ -0,0 +1,393 @@
1
+ /**
2
+ * TaskManager — Main orchestrator for scheduled and self-running tasks.
3
+ *
4
+ * Manages the full task lifecycle: create, schedule, execute, pause, resume, delete.
5
+ * Auto-selects TaskStore and TaskBackend based on config.
6
+ *
7
+ * Usage:
8
+ * const neurolink = new NeuroLink({ tasks: { backend: "bullmq" } });
9
+ * await neurolink.tasks.create({ name: "monitor", prompt: "...", schedule: { type: "interval", every: 60000 } });
10
+ */
11
+ import { nanoid } from "nanoid";
12
+ import { logger } from "../utils/logger.js";
13
+ import { TaskBackendRegistry } from "./backends/taskBackendRegistry.js";
14
+ import { TaskError } from "./errors.js";
15
+ import { TaskExecutor } from "./taskExecutor.js";
16
+ import { TASK_DEFAULTS, } from "../types/taskTypes.js";
17
+ export class TaskManager {
18
+ neurolink;
19
+ config;
20
+ store = null;
21
+ backend = null;
22
+ executor = null;
23
+ initialized = false;
24
+ initPromise = null;
25
+ /** In-memory callback registry (not serializable to store) */
26
+ callbacks = new Map();
27
+ /** Emitter reference — set by NeuroLink on integration */
28
+ emitter;
29
+ constructor(neurolink, config) {
30
+ this.neurolink = neurolink;
31
+ this.config = { ...config };
32
+ }
33
+ /** Set the event emitter (called by NeuroLink during integration) */
34
+ setEmitter(emitter) {
35
+ this.emitter = emitter;
36
+ }
37
+ // ── Initialization ──────────────────────────────────────
38
+ async ensureInitialized() {
39
+ if (this.initialized) {
40
+ return;
41
+ }
42
+ if (this.initPromise) {
43
+ return this.initPromise;
44
+ }
45
+ this.initPromise = this.doInitialize();
46
+ await this.initPromise;
47
+ }
48
+ async doInitialize() {
49
+ const backendName = this.config.backend ?? TASK_DEFAULTS.backend;
50
+ // Create store based on backend
51
+ if (backendName === "bullmq") {
52
+ const { RedisTaskStore } = await import("./store/redisTaskStore.js");
53
+ this.store = new RedisTaskStore(this.config);
54
+ }
55
+ else {
56
+ const { FileTaskStore } = await import("./store/fileTaskStore.js");
57
+ this.store = new FileTaskStore(this.config);
58
+ }
59
+ await this.store.initialize();
60
+ // Create backend
61
+ this.backend = await TaskBackendRegistry.create(backendName, this.config);
62
+ await this.backend.initialize();
63
+ // Create executor
64
+ this.executor = new TaskExecutor(this.neurolink, this.store);
65
+ // Re-schedule active tasks from store (handles restarts)
66
+ await this.rescheduleActiveTasks();
67
+ this.initialized = true;
68
+ logger.info("[TaskManager] Initialized", {
69
+ backend: backendName,
70
+ store: this.store.type,
71
+ });
72
+ }
73
+ // ── Public API ────────────────────────────────────────
74
+ async create(definition) {
75
+ if (this.config.enabled === false) {
76
+ throw TaskError.create("TASK_DISABLED", "TaskManager is disabled. Set tasks.enabled to true in config.");
77
+ }
78
+ await this.ensureInitialized();
79
+ // Enforce maximum task limit to prevent unbounded task creation
80
+ const maxTasks = this.config.maxTasks ?? TASK_DEFAULTS.maxTasks;
81
+ const existingTasks = await this.store.list();
82
+ if (existingTasks.length >= maxTasks) {
83
+ throw TaskError.create("TASK_LIMIT_REACHED", `Task limit reached (${maxTasks}). Delete existing tasks or increase maxTasks config.`);
84
+ }
85
+ const now = new Date().toISOString();
86
+ const task = {
87
+ id: `task_${nanoid(12)}`,
88
+ name: definition.name,
89
+ prompt: definition.prompt,
90
+ schedule: definition.schedule,
91
+ mode: definition.mode ?? TASK_DEFAULTS.mode,
92
+ status: "active",
93
+ tools: definition.tools ?? TASK_DEFAULTS.tools,
94
+ timeout: definition.timeout ?? TASK_DEFAULTS.timeout,
95
+ retry: {
96
+ maxAttempts: definition.retry?.maxAttempts ?? TASK_DEFAULTS.retry.maxAttempts,
97
+ backoffMs: definition.retry?.backoffMs ?? [
98
+ ...TASK_DEFAULTS.retry.backoffMs,
99
+ ],
100
+ },
101
+ runCount: 0,
102
+ createdAt: now,
103
+ updatedAt: now,
104
+ // Optional overrides
105
+ ...(definition.provider ? { provider: definition.provider } : {}),
106
+ ...(definition.model ? { model: definition.model } : {}),
107
+ ...(definition.thinkingLevel
108
+ ? { thinkingLevel: definition.thinkingLevel }
109
+ : {}),
110
+ ...(definition.systemPrompt
111
+ ? { systemPrompt: definition.systemPrompt }
112
+ : {}),
113
+ ...(definition.maxTokens ? { maxTokens: definition.maxTokens } : {}),
114
+ ...(definition.temperature !== undefined
115
+ ? { temperature: definition.temperature }
116
+ : {}),
117
+ ...(definition.maxRuns !== undefined
118
+ ? { maxRuns: definition.maxRuns }
119
+ : {}),
120
+ ...(definition.metadata ? { metadata: definition.metadata } : {}),
121
+ };
122
+ // Generate session ID for continuation mode
123
+ if (task.mode === "continuation") {
124
+ task.sessionId = `session_${nanoid(12)}`;
125
+ }
126
+ // Save to store
127
+ await this.store.save(task);
128
+ // Register callbacks (in-memory only)
129
+ if (definition.onSuccess || definition.onError || definition.onComplete) {
130
+ this.callbacks.set(task.id, {
131
+ onSuccess: definition.onSuccess,
132
+ onError: definition.onError,
133
+ onComplete: definition.onComplete,
134
+ });
135
+ }
136
+ // Schedule
137
+ try {
138
+ await this.backend.schedule(task, (t) => this.onTaskTick(t));
139
+ }
140
+ catch (err) {
141
+ await this.store.delete(task.id);
142
+ throw err;
143
+ }
144
+ this.emit("task:created", task);
145
+ logger.info("[TaskManager] Task created", {
146
+ taskId: task.id,
147
+ name: task.name,
148
+ schedule: task.schedule.type,
149
+ mode: task.mode,
150
+ });
151
+ return task;
152
+ }
153
+ async get(taskId) {
154
+ await this.ensureInitialized();
155
+ return this.store.get(taskId);
156
+ }
157
+ async list(filter) {
158
+ await this.ensureInitialized();
159
+ return this.store.list(filter);
160
+ }
161
+ async update(taskId, updates) {
162
+ await this.ensureInitialized();
163
+ const existing = await this.store.get(taskId);
164
+ if (!existing) {
165
+ throw TaskError.create("TASK_NOT_FOUND", `Task not found: ${taskId}`);
166
+ }
167
+ // Apply allowed scalar updates via whitelist
168
+ const ALLOWED_UPDATE_FIELDS = [
169
+ "name",
170
+ "prompt",
171
+ "schedule",
172
+ "mode",
173
+ "provider",
174
+ "model",
175
+ "systemPrompt",
176
+ "maxTokens",
177
+ "temperature",
178
+ "timeout",
179
+ "tools",
180
+ "maxRuns",
181
+ "metadata",
182
+ "thinkingLevel",
183
+ ];
184
+ const taskUpdates = {};
185
+ for (const field of ALLOWED_UPDATE_FIELDS) {
186
+ if (updates[field] !== undefined) {
187
+ taskUpdates[field] = updates[field];
188
+ }
189
+ }
190
+ // Special-case: mode changes require sessionId handling
191
+ if (updates.mode !== undefined) {
192
+ if (updates.mode === "continuation" && !existing.sessionId) {
193
+ taskUpdates.sessionId = `session_${nanoid(12)}`;
194
+ }
195
+ else if (updates.mode !== "continuation") {
196
+ taskUpdates.sessionId = undefined;
197
+ await this.store.clearHistory(taskId);
198
+ }
199
+ }
200
+ const updated = await this.store.update(taskId, taskUpdates);
201
+ // Re-schedule if schedule changed and task is active
202
+ if (updates.schedule && updated.status === "active") {
203
+ await this.backend.cancel(taskId);
204
+ await this.backend.schedule(updated, (t) => this.onTaskTick(t));
205
+ }
206
+ return updated;
207
+ }
208
+ /** Run a task immediately (outside of its schedule) */
209
+ async run(taskId) {
210
+ await this.ensureInitialized();
211
+ const task = await this.store.get(taskId);
212
+ if (!task) {
213
+ throw TaskError.create("TASK_NOT_FOUND", `Task not found: ${taskId}`);
214
+ }
215
+ return this.onTaskTick(task);
216
+ }
217
+ async pause(taskId) {
218
+ await this.ensureInitialized();
219
+ const task = await this.store.get(taskId);
220
+ if (!task) {
221
+ throw TaskError.create("TASK_NOT_FOUND", `Task not found: ${taskId}`);
222
+ }
223
+ if (task.status !== "active") {
224
+ throw TaskError.create("INVALID_TASK_STATUS", `Cannot pause task with status: ${task.status}`);
225
+ }
226
+ await this.backend.pause(taskId);
227
+ const updated = await this.store.update(taskId, { status: "paused" });
228
+ this.emit("task:paused", updated);
229
+ return updated;
230
+ }
231
+ async resume(taskId) {
232
+ await this.ensureInitialized();
233
+ const task = await this.store.get(taskId);
234
+ if (!task) {
235
+ throw TaskError.create("TASK_NOT_FOUND", `Task not found: ${taskId}`);
236
+ }
237
+ if (task.status !== "paused") {
238
+ throw TaskError.create("INVALID_TASK_STATUS", `Cannot resume task with status: ${task.status}`);
239
+ }
240
+ const updated = await this.store.update(taskId, { status: "active" });
241
+ await this.backend.schedule(updated, (t) => this.onTaskTick(t));
242
+ this.emit("task:resumed", updated);
243
+ return updated;
244
+ }
245
+ async delete(taskId) {
246
+ await this.ensureInitialized();
247
+ await this.backend.cancel(taskId);
248
+ await this.store.delete(taskId);
249
+ this.callbacks.delete(taskId);
250
+ this.emit("task:deleted", taskId);
251
+ }
252
+ async runs(taskId, options) {
253
+ await this.ensureInitialized();
254
+ return this.store.getRuns(taskId, options);
255
+ }
256
+ async shutdown() {
257
+ if (this.backend) {
258
+ await this.backend.shutdown();
259
+ }
260
+ if (this.store) {
261
+ await this.store.shutdown();
262
+ }
263
+ this.callbacks.clear();
264
+ this.initialized = false;
265
+ this.initPromise = null;
266
+ logger.info("[TaskManager] Shut down");
267
+ }
268
+ /** Check if the backend is healthy */
269
+ async isHealthy() {
270
+ if (!this.backend) {
271
+ return false;
272
+ }
273
+ return this.backend.isHealthy();
274
+ }
275
+ // ── Internal ──────────────────────────────────────────
276
+ /**
277
+ * Called by the backend on each scheduled tick.
278
+ * Executes the task, updates state, fires callbacks/events.
279
+ */
280
+ async onTaskTick(task) {
281
+ this.emit("task:started", task);
282
+ // Re-read latest task state (may have been updated/paused since scheduling)
283
+ const current = await this.store.get(task.id);
284
+ if (!current || current.status !== "active") {
285
+ logger.debug("[TaskManager] Skipping tick for non-active task", {
286
+ taskId: task.id,
287
+ status: current?.status,
288
+ });
289
+ return {
290
+ taskId: task.id,
291
+ runId: "skipped",
292
+ status: "error",
293
+ error: "Task is not active",
294
+ durationMs: 0,
295
+ timestamp: new Date().toISOString(),
296
+ };
297
+ }
298
+ const result = await this.executor.execute(current);
299
+ // Log the run
300
+ await this.store.appendRun(task.id, result);
301
+ // Update task tracking
302
+ const updates = {
303
+ runCount: current.runCount + 1,
304
+ lastRunAt: result.timestamp,
305
+ };
306
+ // Check if task should complete
307
+ if (current.maxRuns && current.runCount + 1 >= current.maxRuns) {
308
+ updates.status = "completed";
309
+ await this.backend.cancel(task.id);
310
+ }
311
+ // Mark successful once tasks as completed
312
+ if (result.status === "success" && current.schedule.type === "once") {
313
+ updates.status = "completed";
314
+ await this.backend.cancel(task.id);
315
+ }
316
+ // Mark as failed on permanent error
317
+ if (result.status === "error" && current.schedule.type === "once") {
318
+ updates.status = "failed";
319
+ }
320
+ await this.store.update(task.id, updates);
321
+ // Fire callbacks
322
+ const cbs = this.callbacks.get(task.id);
323
+ if (cbs) {
324
+ try {
325
+ if (result.status === "success" && cbs.onSuccess) {
326
+ await cbs.onSuccess(result);
327
+ }
328
+ if (result.status === "error" && cbs.onError) {
329
+ await cbs.onError({
330
+ taskId: task.id,
331
+ runId: result.runId,
332
+ error: result.error ?? "Unknown error",
333
+ attempt: 1, // Executor handles retries internally and returns final result
334
+ maxAttempts: current.retry.maxAttempts,
335
+ willRetry: false,
336
+ timestamp: result.timestamp,
337
+ });
338
+ }
339
+ if (updates.status === "completed" || updates.status === "failed") {
340
+ const finalTask = await this.store.get(task.id);
341
+ if (finalTask && cbs.onComplete) {
342
+ await cbs.onComplete(finalTask);
343
+ }
344
+ }
345
+ }
346
+ catch (cbErr) {
347
+ logger.error("[TaskManager] Callback error", {
348
+ taskId: task.id,
349
+ error: String(cbErr),
350
+ });
351
+ }
352
+ }
353
+ // Emit events
354
+ if (result.status === "success") {
355
+ this.emit("task:completed", result);
356
+ }
357
+ else {
358
+ this.emit("task:failed", result);
359
+ }
360
+ return result;
361
+ }
362
+ /**
363
+ * Re-schedule all active tasks from store.
364
+ * Called on initialization to handle process restarts.
365
+ */
366
+ async rescheduleActiveTasks() {
367
+ const activeTasks = await this.store.list({ status: "active" });
368
+ for (const task of activeTasks) {
369
+ try {
370
+ await this.backend.schedule(task, (t) => this.onTaskTick(t));
371
+ logger.debug("[TaskManager] Re-scheduled task", {
372
+ taskId: task.id,
373
+ name: task.name,
374
+ });
375
+ }
376
+ catch (err) {
377
+ logger.error("[TaskManager] Failed to re-schedule task", {
378
+ taskId: task.id,
379
+ error: String(err),
380
+ });
381
+ }
382
+ }
383
+ if (activeTasks.length > 0) {
384
+ logger.info("[TaskManager] Re-scheduled active tasks", {
385
+ count: activeTasks.length,
386
+ });
387
+ }
388
+ }
389
+ emit(event, ...args) {
390
+ this.emitter?.emit(event, ...args);
391
+ }
392
+ }
393
+ //# sourceMappingURL=taskManager.js.map
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Built-in agent tools for TaskManager.
3
+ *
4
+ * These tools allow the AI to self-schedule, manage, and inspect tasks
5
+ * during conversations. Created per-instance via `createTaskTools()` factory,
6
+ * following the same pattern as `createFileTools()` in files/fileTools.ts.
7
+ *
8
+ * @module tasks/tools/taskTools
9
+ */
10
+ import type { TaskManager } from "../taskManager.js";
11
+ import type { TaskSchedule } from "../../types/taskTypes.js";
12
+ /**
13
+ * Create task management tools bound to a TaskManager instance.
14
+ *
15
+ * These tools follow the same factory pattern as `createFileTools()` in
16
+ * `src/lib/files/fileTools.ts`. The `manager` is captured via closure,
17
+ * eliminating the need for module-level singleton state.
18
+ *
19
+ * @param manager - The TaskManager instance to bind to
20
+ * @returns Record of tool name to tool definition
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * const manager = new TaskManager(neurolink, config);
25
+ * const tools = createTaskTools(manager);
26
+ * // tools.createTask, tools.listTasks, tools.getTaskRuns, etc.
27
+ * ```
28
+ */
29
+ export declare function createTaskTools(manager: TaskManager): {
30
+ createTask: import("ai").Tool<{
31
+ name: string;
32
+ prompt: string;
33
+ schedule: {
34
+ type: "cron" | "interval" | "once";
35
+ expression?: string | undefined;
36
+ timezone?: string | undefined;
37
+ every?: number | undefined;
38
+ at?: string | undefined;
39
+ };
40
+ mode?: "isolated" | "continuation" | undefined;
41
+ }, {
42
+ success: boolean;
43
+ taskId: string;
44
+ name: string;
45
+ status: import("../../types/taskTypes.js").TaskStatus;
46
+ mode: import("../../types/taskTypes.js").TaskExecutionMode;
47
+ nextRunAt: string | undefined;
48
+ schedule: TaskSchedule;
49
+ error?: undefined;
50
+ } | {
51
+ success: boolean;
52
+ error: string;
53
+ taskId?: undefined;
54
+ name?: undefined;
55
+ status?: undefined;
56
+ mode?: undefined;
57
+ nextRunAt?: undefined;
58
+ schedule?: undefined;
59
+ }>;
60
+ listTasks: import("ai").Tool<{
61
+ status?: "failed" | "pending" | "active" | "paused" | "completed" | "cancelled" | undefined;
62
+ }, {
63
+ success: boolean;
64
+ count: number;
65
+ tasks: {
66
+ taskId: string;
67
+ name: string;
68
+ status: import("../../types/taskTypes.js").TaskStatus;
69
+ mode: import("../../types/taskTypes.js").TaskExecutionMode;
70
+ schedule: TaskSchedule;
71
+ runCount: number;
72
+ lastRunAt: string | undefined;
73
+ nextRunAt: string | undefined;
74
+ }[];
75
+ error?: undefined;
76
+ } | {
77
+ success: boolean;
78
+ error: string;
79
+ count?: undefined;
80
+ tasks?: undefined;
81
+ }>;
82
+ getTaskRuns: import("ai").Tool<{
83
+ taskId: string;
84
+ limit?: number | undefined;
85
+ }, {
86
+ success: boolean;
87
+ taskId: string;
88
+ count: number;
89
+ runs: {
90
+ runId: string;
91
+ status: "error" | "success";
92
+ output: string | undefined;
93
+ durationMs: number;
94
+ timestamp: string;
95
+ error: string | undefined;
96
+ }[];
97
+ error?: undefined;
98
+ } | {
99
+ success: boolean;
100
+ error: string;
101
+ taskId?: undefined;
102
+ count?: undefined;
103
+ runs?: undefined;
104
+ }>;
105
+ deleteTask: import("ai").Tool<{
106
+ taskId: string;
107
+ }, {
108
+ success: boolean;
109
+ error: string;
110
+ deletedTask?: undefined;
111
+ taskId?: undefined;
112
+ } | {
113
+ success: boolean;
114
+ deletedTask: string;
115
+ taskId: string;
116
+ error?: undefined;
117
+ }>;
118
+ runTaskNow: import("ai").Tool<{
119
+ taskId: string;
120
+ }, {
121
+ success: boolean;
122
+ runId: string;
123
+ status: "error" | "success";
124
+ output: string | undefined;
125
+ durationMs: number;
126
+ error: string | undefined;
127
+ } | {
128
+ success: boolean;
129
+ error: string;
130
+ runId?: undefined;
131
+ status?: undefined;
132
+ output?: undefined;
133
+ durationMs?: undefined;
134
+ }>;
135
+ };