@juspay/neurolink 9.40.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.
- package/CHANGELOG.md +6 -0
- package/dist/browser/neurolink.min.js +440 -433
- package/dist/cli/commands/task.d.ts +56 -0
- package/dist/cli/commands/task.js +835 -0
- package/dist/cli/parser.js +4 -1
- package/dist/lib/neurolink.d.ts +16 -0
- package/dist/lib/neurolink.js +119 -2
- package/dist/lib/tasks/backends/bullmqBackend.d.ts +32 -0
- package/dist/lib/tasks/backends/bullmqBackend.js +189 -0
- package/dist/lib/tasks/backends/nodeTimeoutBackend.d.ts +27 -0
- package/dist/lib/tasks/backends/nodeTimeoutBackend.js +141 -0
- package/dist/lib/tasks/backends/taskBackendRegistry.d.ts +31 -0
- package/dist/lib/tasks/backends/taskBackendRegistry.js +66 -0
- package/dist/lib/tasks/errors.d.ts +31 -0
- package/dist/lib/tasks/errors.js +18 -0
- package/dist/lib/tasks/store/fileTaskStore.d.ts +43 -0
- package/dist/lib/tasks/store/fileTaskStore.js +179 -0
- package/dist/lib/tasks/store/redisTaskStore.d.ts +42 -0
- package/dist/lib/tasks/store/redisTaskStore.js +189 -0
- package/dist/lib/tasks/taskExecutor.d.ts +21 -0
- package/dist/lib/tasks/taskExecutor.js +166 -0
- package/dist/lib/tasks/taskManager.d.ts +60 -0
- package/dist/lib/tasks/taskManager.js +393 -0
- package/dist/lib/tasks/tools/taskTools.d.ts +135 -0
- package/dist/lib/tasks/tools/taskTools.js +274 -0
- package/dist/lib/types/configTypes.d.ts +3 -0
- package/dist/lib/types/generateTypes.d.ts +13 -0
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/lib/types/taskTypes.d.ts +275 -0
- package/dist/lib/types/taskTypes.js +37 -0
- package/dist/neurolink.d.ts +16 -0
- package/dist/neurolink.js +119 -2
- package/dist/tasks/backends/bullmqBackend.d.ts +32 -0
- package/dist/tasks/backends/bullmqBackend.js +188 -0
- package/dist/tasks/backends/nodeTimeoutBackend.d.ts +27 -0
- package/dist/tasks/backends/nodeTimeoutBackend.js +140 -0
- package/dist/tasks/backends/taskBackendRegistry.d.ts +31 -0
- package/dist/tasks/backends/taskBackendRegistry.js +65 -0
- package/dist/tasks/errors.d.ts +31 -0
- package/dist/tasks/errors.js +17 -0
- package/dist/tasks/store/fileTaskStore.d.ts +43 -0
- package/dist/tasks/store/fileTaskStore.js +178 -0
- package/dist/tasks/store/redisTaskStore.d.ts +42 -0
- package/dist/tasks/store/redisTaskStore.js +188 -0
- package/dist/tasks/taskExecutor.d.ts +21 -0
- package/dist/tasks/taskExecutor.js +165 -0
- package/dist/tasks/taskManager.d.ts +60 -0
- package/dist/tasks/taskManager.js +392 -0
- package/dist/tasks/tools/taskTools.d.ts +135 -0
- package/dist/tasks/tools/taskTools.js +273 -0
- package/dist/types/configTypes.d.ts +3 -0
- package/dist/types/generateTypes.d.ts +13 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/taskTypes.d.ts +275 -0
- package/dist/types/taskTypes.js +36 -0
- package/package.json +3 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TaskExecutor — Runs a single task execution against NeuroLink.generate().
|
|
3
|
+
*
|
|
4
|
+
* Handles:
|
|
5
|
+
* - Isolated mode: fresh generate() call with no history
|
|
6
|
+
* - Continuation mode: loads conversation history, appends new exchange
|
|
7
|
+
* - Retry with exponential backoff for transient errors
|
|
8
|
+
* - Run result construction and logging
|
|
9
|
+
*/
|
|
10
|
+
import { nanoid } from "nanoid";
|
|
11
|
+
import { logger } from "../utils/logger.js";
|
|
12
|
+
/** Errors that are transient and should be retried */
|
|
13
|
+
const TRANSIENT_PATTERNS = [
|
|
14
|
+
"rate limit",
|
|
15
|
+
"rate_limit",
|
|
16
|
+
"too many requests",
|
|
17
|
+
"429",
|
|
18
|
+
"503",
|
|
19
|
+
"502",
|
|
20
|
+
"504",
|
|
21
|
+
"timeout",
|
|
22
|
+
"econnreset",
|
|
23
|
+
"econnrefused",
|
|
24
|
+
"network",
|
|
25
|
+
"overloaded",
|
|
26
|
+
];
|
|
27
|
+
function isTransientError(error) {
|
|
28
|
+
const msg = String(error).toLowerCase();
|
|
29
|
+
return TRANSIENT_PATTERNS.some((p) => msg.includes(p));
|
|
30
|
+
}
|
|
31
|
+
export class TaskExecutor {
|
|
32
|
+
neurolink;
|
|
33
|
+
store;
|
|
34
|
+
constructor(neurolink, store) {
|
|
35
|
+
this.neurolink = neurolink;
|
|
36
|
+
this.store = store;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Execute a task once. Called by the backend on each scheduled tick.
|
|
40
|
+
* Returns the run result (success or error).
|
|
41
|
+
*/
|
|
42
|
+
async execute(task) {
|
|
43
|
+
const runId = `run_${nanoid(12)}`;
|
|
44
|
+
const startTime = Date.now();
|
|
45
|
+
let lastError;
|
|
46
|
+
for (let attempt = 1; attempt <= task.retry.maxAttempts; attempt++) {
|
|
47
|
+
try {
|
|
48
|
+
const result = await this.executeOnce(task, runId);
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
lastError = String(err);
|
|
53
|
+
const willRetry = attempt < task.retry.maxAttempts && isTransientError(err);
|
|
54
|
+
logger.warn("[TaskExecutor] Execution attempt failed", {
|
|
55
|
+
taskId: task.id,
|
|
56
|
+
runId,
|
|
57
|
+
attempt,
|
|
58
|
+
willRetry,
|
|
59
|
+
error: lastError,
|
|
60
|
+
});
|
|
61
|
+
if (!willRetry) {
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
// Backoff before retry
|
|
65
|
+
const backoffIndex = Math.min(attempt - 1, task.retry.backoffMs.length - 1);
|
|
66
|
+
const delay = task.retry.backoffMs[backoffIndex];
|
|
67
|
+
await sleep(delay);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// All retries exhausted or permanent error
|
|
71
|
+
const errorResult = {
|
|
72
|
+
taskId: task.id,
|
|
73
|
+
runId,
|
|
74
|
+
status: "error",
|
|
75
|
+
error: lastError,
|
|
76
|
+
durationMs: Date.now() - startTime,
|
|
77
|
+
timestamp: new Date().toISOString(),
|
|
78
|
+
};
|
|
79
|
+
return errorResult;
|
|
80
|
+
}
|
|
81
|
+
// ── Internal ──────────────────────────────────────────
|
|
82
|
+
async executeOnce(task, runId) {
|
|
83
|
+
const startTime = Date.now();
|
|
84
|
+
// Build generate options
|
|
85
|
+
const generateOptions = {
|
|
86
|
+
input: { text: task.prompt },
|
|
87
|
+
...(task.provider ? { provider: task.provider } : {}),
|
|
88
|
+
...(task.model ? { model: task.model } : {}),
|
|
89
|
+
...(task.systemPrompt ? { systemPrompt: task.systemPrompt } : {}),
|
|
90
|
+
...(task.maxTokens ? { maxTokens: task.maxTokens } : {}),
|
|
91
|
+
...(task.temperature !== undefined
|
|
92
|
+
? { temperature: task.temperature }
|
|
93
|
+
: {}),
|
|
94
|
+
...(task.timeout ? { timeout: task.timeout } : {}),
|
|
95
|
+
...(!task.tools ? { disableTools: true } : {}),
|
|
96
|
+
};
|
|
97
|
+
// Thinking level
|
|
98
|
+
if (task.thinkingLevel) {
|
|
99
|
+
generateOptions.thinkingConfig = { thinkingLevel: task.thinkingLevel };
|
|
100
|
+
}
|
|
101
|
+
// Continuation mode: pass conversation history as proper multi-turn messages
|
|
102
|
+
if (task.mode === "continuation" && task.sessionId) {
|
|
103
|
+
const history = await this.store.getHistory(task.id);
|
|
104
|
+
// Pass history as proper role-based conversation messages
|
|
105
|
+
if (history.length > 0) {
|
|
106
|
+
generateOptions.conversationMessages = history.map((entry, i) => ({
|
|
107
|
+
id: `${task.sessionId}_${i}`,
|
|
108
|
+
role: entry.role,
|
|
109
|
+
content: entry.content,
|
|
110
|
+
timestamp: entry.timestamp,
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
// Add continuation context to system prompt
|
|
114
|
+
const runCount = Math.floor(history.length / 2);
|
|
115
|
+
const continuationHint = runCount > 0
|
|
116
|
+
? `This is a continuation task (run ${runCount + 1}). Your previous ${runCount} exchange(s) are provided as conversation history.`
|
|
117
|
+
: "This is a continuation task. This is the first execution — no prior history exists yet.";
|
|
118
|
+
generateOptions.systemPrompt = task.systemPrompt
|
|
119
|
+
? `${task.systemPrompt}\n\n${continuationHint}`
|
|
120
|
+
: continuationHint;
|
|
121
|
+
}
|
|
122
|
+
// Execute
|
|
123
|
+
const result = await this.neurolink.generate(generateOptions);
|
|
124
|
+
// Build run result
|
|
125
|
+
const runResult = {
|
|
126
|
+
taskId: task.id,
|
|
127
|
+
runId,
|
|
128
|
+
status: "success",
|
|
129
|
+
output: result.content,
|
|
130
|
+
toolCalls: result.toolExecutions?.map((te) => ({
|
|
131
|
+
name: te.name,
|
|
132
|
+
input: te.input,
|
|
133
|
+
output: te.output,
|
|
134
|
+
})),
|
|
135
|
+
tokensUsed: result.usage
|
|
136
|
+
? {
|
|
137
|
+
input: result.usage.input ?? 0,
|
|
138
|
+
output: result.usage.output ?? 0,
|
|
139
|
+
}
|
|
140
|
+
: undefined,
|
|
141
|
+
durationMs: Date.now() - startTime,
|
|
142
|
+
timestamp: new Date().toISOString(),
|
|
143
|
+
};
|
|
144
|
+
// Continuation mode: append this exchange to history
|
|
145
|
+
if (task.mode === "continuation" && task.sessionId) {
|
|
146
|
+
const newEntries = [
|
|
147
|
+
{
|
|
148
|
+
role: "user",
|
|
149
|
+
content: task.prompt,
|
|
150
|
+
timestamp: new Date(startTime).toISOString(),
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
role: "assistant",
|
|
154
|
+
content: result.content,
|
|
155
|
+
timestamp: runResult.timestamp,
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
await this.store.appendHistory(task.id, newEntries);
|
|
159
|
+
}
|
|
160
|
+
return runResult;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function sleep(ms) {
|
|
164
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=taskExecutor.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
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 { type NeuroLinkExecutable, type Task, type TaskDefinition, type TaskManagerConfig, type TaskRunResult, type TaskStatus } from "../types/taskTypes.js";
|
|
12
|
+
export declare class TaskManager {
|
|
13
|
+
private neurolink;
|
|
14
|
+
private config;
|
|
15
|
+
private store;
|
|
16
|
+
private backend;
|
|
17
|
+
private executor;
|
|
18
|
+
private initialized;
|
|
19
|
+
private initPromise;
|
|
20
|
+
/** In-memory callback registry (not serializable to store) */
|
|
21
|
+
private callbacks;
|
|
22
|
+
/** Emitter reference — set by NeuroLink on integration */
|
|
23
|
+
private emitter?;
|
|
24
|
+
constructor(neurolink: NeuroLinkExecutable, config?: TaskManagerConfig);
|
|
25
|
+
/** Set the event emitter (called by NeuroLink during integration) */
|
|
26
|
+
setEmitter(emitter: {
|
|
27
|
+
emit(event: string, ...args: unknown[]): boolean;
|
|
28
|
+
}): void;
|
|
29
|
+
private ensureInitialized;
|
|
30
|
+
private doInitialize;
|
|
31
|
+
create(definition: TaskDefinition): Promise<Task>;
|
|
32
|
+
get(taskId: string): Promise<Task | null>;
|
|
33
|
+
list(filter?: {
|
|
34
|
+
status?: TaskStatus;
|
|
35
|
+
}): Promise<Task[]>;
|
|
36
|
+
update(taskId: string, updates: Partial<TaskDefinition>): Promise<Task>;
|
|
37
|
+
/** Run a task immediately (outside of its schedule) */
|
|
38
|
+
run(taskId: string): Promise<TaskRunResult>;
|
|
39
|
+
pause(taskId: string): Promise<Task>;
|
|
40
|
+
resume(taskId: string): Promise<Task>;
|
|
41
|
+
delete(taskId: string): Promise<void>;
|
|
42
|
+
runs(taskId: string, options?: {
|
|
43
|
+
limit?: number;
|
|
44
|
+
status?: string;
|
|
45
|
+
}): Promise<TaskRunResult[]>;
|
|
46
|
+
shutdown(): Promise<void>;
|
|
47
|
+
/** Check if the backend is healthy */
|
|
48
|
+
isHealthy(): Promise<boolean>;
|
|
49
|
+
/**
|
|
50
|
+
* Called by the backend on each scheduled tick.
|
|
51
|
+
* Executes the task, updates state, fires callbacks/events.
|
|
52
|
+
*/
|
|
53
|
+
private onTaskTick;
|
|
54
|
+
/**
|
|
55
|
+
* Re-schedule all active tasks from store.
|
|
56
|
+
* Called on initialization to handle process restarts.
|
|
57
|
+
*/
|
|
58
|
+
private rescheduleActiveTasks;
|
|
59
|
+
private emit;
|
|
60
|
+
}
|
|
@@ -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
|