@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.
- package/CHANGELOG.md +12 -0
- package/dist/browser/neurolink.min.js +445 -431
- 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 +22 -1
- package/dist/lib/neurolink.js +195 -14
- 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 +42 -0
- package/dist/lib/types/index.d.ts +2 -1
- package/dist/lib/types/streamTypes.d.ts +7 -0
- package/dist/lib/types/taskTypes.d.ts +275 -0
- package/dist/lib/types/taskTypes.js +37 -0
- package/dist/neurolink.d.ts +22 -1
- package/dist/neurolink.js +195 -14
- 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 +42 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/streamTypes.d.ts +7 -0
- package/dist/types/taskTypes.d.ts +275 -0
- package/dist/types/taskTypes.js +36 -0
- package/package.json +4 -2
package/dist/cli/parser.js
CHANGED
|
@@ -15,6 +15,7 @@ import { ObservabilityCommandFactory } from "./commands/observability.js";
|
|
|
15
15
|
import { TelemetryCommandFactory } from "./commands/telemetry.js";
|
|
16
16
|
import { proxyStartCommand, proxyStatusCommand, proxySetupCommand, proxyGuardCommand, proxyInstallCommand, proxyUninstallCommand, } from "./commands/proxy.js";
|
|
17
17
|
import { EvaluateCommandFactory } from "./commands/evaluate.js";
|
|
18
|
+
import { TaskCommandFactory } from "./commands/task.js";
|
|
18
19
|
// Enhanced CLI with Professional UX
|
|
19
20
|
export function initializeCliParser() {
|
|
20
21
|
return (yargs(hideBin(process.argv))
|
|
@@ -199,6 +200,8 @@ export function initializeCliParser() {
|
|
|
199
200
|
handler: () => { },
|
|
200
201
|
})
|
|
201
202
|
// Evaluate Command Group - Using EvaluateCommandFactory
|
|
202
|
-
.command(EvaluateCommandFactory.createEvaluateCommand())
|
|
203
|
+
.command(EvaluateCommandFactory.createEvaluateCommand())
|
|
204
|
+
// Task Command Group - Scheduled and self-running tasks
|
|
205
|
+
.command(TaskCommandFactory.createTaskCommands())); // Close the main return statement
|
|
203
206
|
}
|
|
204
207
|
//# sourceMappingURL=parser.js.map
|
package/dist/lib/neurolink.d.ts
CHANGED
|
@@ -25,11 +25,14 @@ import type { ObservabilityConfig } from "./types/observability.js";
|
|
|
25
25
|
import type { StreamOptions, StreamResult } from "./types/streamTypes.js";
|
|
26
26
|
import type { ToolExecutionContext, ToolExecutionSummary, ToolInfo, ToolRegistrationOptions } from "./types/tools.js";
|
|
27
27
|
import type { BatchOperationResult } from "./types/typeAliases.js";
|
|
28
|
+
import { TaskManager } from "./tasks/taskManager.js";
|
|
28
29
|
export declare class NeuroLink {
|
|
29
30
|
private mcpInitialized;
|
|
30
31
|
private mcpSkipped;
|
|
31
32
|
private mcpInitPromise;
|
|
32
33
|
private emitter;
|
|
34
|
+
private _taskManager?;
|
|
35
|
+
private _taskManagerConfig?;
|
|
33
36
|
private toolRegistry;
|
|
34
37
|
private autoDiscoveredServerInfos;
|
|
35
38
|
private externalServerManager;
|
|
@@ -148,6 +151,13 @@ export declare class NeuroLink {
|
|
|
148
151
|
*/
|
|
149
152
|
private get _metricsTraceContext();
|
|
150
153
|
constructor(config?: NeurolinkConstructorConfig);
|
|
154
|
+
/**
|
|
155
|
+
* TaskManager — scheduled and self-running tasks.
|
|
156
|
+
* Lazy-initialized on first access. Configurable via constructor `tasks` option.
|
|
157
|
+
* The actual async initialization (Redis connect, backend start) happens
|
|
158
|
+
* lazily inside TaskManager on first operation.
|
|
159
|
+
*/
|
|
160
|
+
get tasks(): TaskManager;
|
|
151
161
|
/**
|
|
152
162
|
* Initialize provider registry with security settings
|
|
153
163
|
*/
|
|
@@ -173,6 +183,12 @@ export declare class NeuroLink {
|
|
|
173
183
|
* and registers them as direct tools so they're available to LLMs.
|
|
174
184
|
*/
|
|
175
185
|
private registerFileTools;
|
|
186
|
+
/**
|
|
187
|
+
* Register task management tools bound to a TaskManager instance.
|
|
188
|
+
* Follows the same factory + registry pattern as registerFileTools().
|
|
189
|
+
* Called when TaskManager is created (eagerly or lazily via the `tasks` getter).
|
|
190
|
+
*/
|
|
191
|
+
private registerTaskTools;
|
|
176
192
|
/**
|
|
177
193
|
* Register memory retrieval tools that allow the AI to access
|
|
178
194
|
* conversation history, including full tool outputs.
|
|
@@ -181,6 +197,10 @@ export declare class NeuroLink {
|
|
|
181
197
|
private registerMemoryRetrievalTools;
|
|
182
198
|
/** Format memory context for prompt inclusion */
|
|
183
199
|
private formatMemoryContext;
|
|
200
|
+
/**
|
|
201
|
+
* Format memory context from multiple users into a labeled block.
|
|
202
|
+
*/
|
|
203
|
+
private formatMultiUserMemoryContext;
|
|
184
204
|
/**
|
|
185
205
|
* Determine whether memory should be read (retrieved) for this call.
|
|
186
206
|
* Respects both the global memory SDK config and per-call overrides.
|
|
@@ -192,13 +212,14 @@ export declare class NeuroLink {
|
|
|
192
212
|
*/
|
|
193
213
|
private shouldWriteMemory;
|
|
194
214
|
/**
|
|
195
|
-
* Retrieve condensed memory for a user.
|
|
215
|
+
* Retrieve condensed memory for a user (and optionally additional users).
|
|
196
216
|
* Returns the input text enhanced with memory context, or unchanged if no memory.
|
|
197
217
|
*/
|
|
198
218
|
private retrieveMemory;
|
|
199
219
|
/**
|
|
200
220
|
* Store a conversation turn in memory (non-blocking).
|
|
201
221
|
* Calls add(userId, content) which internally condenses old + new via LLM.
|
|
222
|
+
* Supports additional users with per-user prompt and maxWords overrides.
|
|
202
223
|
*/
|
|
203
224
|
private storeMemoryInBackground;
|
|
204
225
|
/**
|
package/dist/lib/neurolink.js
CHANGED
|
@@ -75,6 +75,8 @@ import { isNonNullObject } from "./utils/typeUtils.js";
|
|
|
75
75
|
import { resolveModel } from "./utils/modelAliasResolver.js";
|
|
76
76
|
import { getWorkflow } from "./workflow/core/workflowRegistry.js";
|
|
77
77
|
import { runWorkflow } from "./workflow/core/workflowRunner.js";
|
|
78
|
+
import { TaskManager } from "./tasks/taskManager.js";
|
|
79
|
+
import { createTaskTools } from "./tasks/tools/taskTools.js";
|
|
78
80
|
/**
|
|
79
81
|
* NL-002: Classify MCP error messages into categories for AI disambiguation.
|
|
80
82
|
* Returns a human-readable error category based on error message content.
|
|
@@ -187,6 +189,9 @@ export class NeuroLink {
|
|
|
187
189
|
mcpSkipped = false;
|
|
188
190
|
mcpInitPromise = null;
|
|
189
191
|
emitter = new EventEmitter();
|
|
192
|
+
// TaskManager — lazy-initialized on first access via `this.tasks`
|
|
193
|
+
_taskManager;
|
|
194
|
+
_taskManagerConfig;
|
|
190
195
|
toolRegistry;
|
|
191
196
|
autoDiscoveredServerInfos = [];
|
|
192
197
|
// External MCP server management
|
|
@@ -459,6 +464,28 @@ export class NeuroLink {
|
|
|
459
464
|
if (config?.auth) {
|
|
460
465
|
this.pendingAuthConfig = config.auth;
|
|
461
466
|
}
|
|
467
|
+
// Store task config for lazy initialization
|
|
468
|
+
this._taskManagerConfig = config?.tasks;
|
|
469
|
+
// Eagerly create TaskManager and register tools if config is provided
|
|
470
|
+
if (this._taskManagerConfig) {
|
|
471
|
+
this._taskManager = new TaskManager(this, this._taskManagerConfig);
|
|
472
|
+
this._taskManager.setEmitter(this.emitter);
|
|
473
|
+
this.registerTaskTools(this._taskManager);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* TaskManager — scheduled and self-running tasks.
|
|
478
|
+
* Lazy-initialized on first access. Configurable via constructor `tasks` option.
|
|
479
|
+
* The actual async initialization (Redis connect, backend start) happens
|
|
480
|
+
* lazily inside TaskManager on first operation.
|
|
481
|
+
*/
|
|
482
|
+
get tasks() {
|
|
483
|
+
if (!this._taskManager) {
|
|
484
|
+
this._taskManager = new TaskManager(this, this._taskManagerConfig);
|
|
485
|
+
this._taskManager.setEmitter(this.emitter);
|
|
486
|
+
this.registerTaskTools(this._taskManager);
|
|
487
|
+
}
|
|
488
|
+
return this._taskManager;
|
|
462
489
|
}
|
|
463
490
|
/**
|
|
464
491
|
* Initialize provider registry with security settings
|
|
@@ -716,6 +743,52 @@ export class NeuroLink {
|
|
|
716
743
|
logger.debug(`[NeuroLink] Registered ${Object.keys(fileTools).length} file reference tools`);
|
|
717
744
|
});
|
|
718
745
|
}
|
|
746
|
+
/**
|
|
747
|
+
* Register task management tools bound to a TaskManager instance.
|
|
748
|
+
* Follows the same factory + registry pattern as registerFileTools().
|
|
749
|
+
* Called when TaskManager is created (eagerly or lazily via the `tasks` getter).
|
|
750
|
+
*/
|
|
751
|
+
registerTaskTools(manager) {
|
|
752
|
+
const taskTools = createTaskTools(manager);
|
|
753
|
+
for (const [toolName, toolDef] of Object.entries(taskTools)) {
|
|
754
|
+
const toolId = `direct.${toolName}`;
|
|
755
|
+
const toolInfo = {
|
|
756
|
+
name: toolName,
|
|
757
|
+
description: toolDef.description || `Task tool: ${toolName}`,
|
|
758
|
+
inputSchema: {},
|
|
759
|
+
serverId: "direct",
|
|
760
|
+
category: "built-in",
|
|
761
|
+
};
|
|
762
|
+
// registerTool is async but its core logic is synchronous (Map.set).
|
|
763
|
+
// We fire-and-forget here but tools are available immediately after
|
|
764
|
+
// the synchronous validation + map insertion completes.
|
|
765
|
+
void this.toolRegistry.registerTool(toolId, toolInfo, {
|
|
766
|
+
execute: async (params) => {
|
|
767
|
+
try {
|
|
768
|
+
const result = await toolDef.execute(params, {
|
|
769
|
+
toolCallId: "task-tool",
|
|
770
|
+
messages: [],
|
|
771
|
+
});
|
|
772
|
+
return {
|
|
773
|
+
success: true,
|
|
774
|
+
data: result,
|
|
775
|
+
metadata: { toolName, serverId: "direct", executionTime: 0 },
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
return {
|
|
780
|
+
success: false,
|
|
781
|
+
error: error instanceof Error ? error.message : String(error),
|
|
782
|
+
metadata: { toolName, serverId: "direct", executionTime: 0 },
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
},
|
|
786
|
+
description: toolDef.description,
|
|
787
|
+
inputSchema: {},
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
logger.debug(`[NeuroLink] Registered ${Object.keys(taskTools).length} task tools`);
|
|
791
|
+
}
|
|
719
792
|
/**
|
|
720
793
|
* Register memory retrieval tools that allow the AI to access
|
|
721
794
|
* conversation history, including full tool outputs.
|
|
@@ -823,6 +896,20 @@ export class NeuroLink {
|
|
|
823
896
|
|
|
824
897
|
${memoryContext}
|
|
825
898
|
|
|
899
|
+
Current user's request: ${currentInput}`;
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Format memory context from multiple users into a labeled block.
|
|
903
|
+
*/
|
|
904
|
+
formatMultiUserMemoryContext(memories, currentInput) {
|
|
905
|
+
const memoryBlocks = [];
|
|
906
|
+
for (const [label, memory] of memories) {
|
|
907
|
+
memoryBlocks.push(`[${label}]\n${memory}`);
|
|
908
|
+
}
|
|
909
|
+
return `Context from previous conversations:
|
|
910
|
+
|
|
911
|
+
${memoryBlocks.join("\n\n")}
|
|
912
|
+
|
|
826
913
|
Current user's request: ${currentInput}`;
|
|
827
914
|
}
|
|
828
915
|
/**
|
|
@@ -863,32 +950,71 @@ Current user's request: ${currentInput}`;
|
|
|
863
950
|
return true;
|
|
864
951
|
}
|
|
865
952
|
/**
|
|
866
|
-
* Retrieve condensed memory for a user.
|
|
953
|
+
* Retrieve condensed memory for a user (and optionally additional users).
|
|
867
954
|
* Returns the input text enhanced with memory context, or unchanged if no memory.
|
|
868
955
|
*/
|
|
869
|
-
async retrieveMemory(inputText, userId) {
|
|
956
|
+
async retrieveMemory(inputText, userId, additionalUsers) {
|
|
870
957
|
const client = this.ensureMemoryReady();
|
|
871
958
|
if (!client) {
|
|
872
959
|
return inputText;
|
|
873
960
|
}
|
|
874
|
-
|
|
875
|
-
|
|
961
|
+
// Collect all user IDs to read (primary + additional users with read !== false)
|
|
962
|
+
const readableAdditional = (additionalUsers || []).filter((u) => u.read !== false);
|
|
963
|
+
if (readableAdditional.length === 0) {
|
|
964
|
+
// Single user — use original fast path
|
|
965
|
+
const memory = await client.get(userId);
|
|
966
|
+
if (!memory) {
|
|
967
|
+
return inputText;
|
|
968
|
+
}
|
|
969
|
+
return this.formatMemoryContext(memory, inputText);
|
|
970
|
+
}
|
|
971
|
+
// Multi-user: fetch all memories in parallel
|
|
972
|
+
// Build entries with labels for formatting
|
|
973
|
+
const entries = [
|
|
974
|
+
{ id: userId, label: "User" },
|
|
975
|
+
...readableAdditional.map((u) => ({
|
|
976
|
+
id: u.userId,
|
|
977
|
+
label: u.label || u.userId,
|
|
978
|
+
})),
|
|
979
|
+
];
|
|
980
|
+
const results = await Promise.all(entries.map(async (entry) => {
|
|
981
|
+
const memory = await client.get(entry.id);
|
|
982
|
+
return { ...entry, memory };
|
|
983
|
+
}));
|
|
984
|
+
const memories = new Map();
|
|
985
|
+
for (const { label, memory } of results) {
|
|
986
|
+
if (memory) {
|
|
987
|
+
memories.set(label, memory);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
if (memories.size === 0) {
|
|
876
991
|
return inputText;
|
|
877
992
|
}
|
|
878
|
-
return this.
|
|
993
|
+
return this.formatMultiUserMemoryContext(memories, inputText);
|
|
879
994
|
}
|
|
880
995
|
/**
|
|
881
996
|
* Store a conversation turn in memory (non-blocking).
|
|
882
997
|
* Calls add(userId, content) which internally condenses old + new via LLM.
|
|
998
|
+
* Supports additional users with per-user prompt and maxWords overrides.
|
|
883
999
|
*/
|
|
884
|
-
storeMemoryInBackground(originalPrompt, responseContent, userId) {
|
|
1000
|
+
storeMemoryInBackground(originalPrompt, responseContent, userId, additionalUsers) {
|
|
885
1001
|
setImmediate(async () => {
|
|
886
1002
|
try {
|
|
887
1003
|
const client = this.ensureMemoryReady();
|
|
888
|
-
if (client) {
|
|
889
|
-
|
|
890
|
-
await client.add(userId, content);
|
|
1004
|
+
if (!client) {
|
|
1005
|
+
return;
|
|
891
1006
|
}
|
|
1007
|
+
const content = `User: ${originalPrompt}\nAssistant: ${responseContent}`;
|
|
1008
|
+
// Collect all users to write: primary + additional users with write !== false
|
|
1009
|
+
const writeOps = [client.add(userId, content)];
|
|
1010
|
+
const writableAdditional = (additionalUsers || []).filter((u) => u.write !== false);
|
|
1011
|
+
for (const user of writableAdditional) {
|
|
1012
|
+
const addOptions = user.prompt || user.maxWords
|
|
1013
|
+
? { prompt: user.prompt, maxWords: user.maxWords }
|
|
1014
|
+
: undefined;
|
|
1015
|
+
writeOps.push(client.add(user.userId, content, addOptions));
|
|
1016
|
+
}
|
|
1017
|
+
await Promise.all(writeOps);
|
|
892
1018
|
}
|
|
893
1019
|
catch (error) {
|
|
894
1020
|
logger.warn("Memory storage failed:", error);
|
|
@@ -1853,6 +1979,18 @@ Current user's request: ${currentInput}`;
|
|
|
1853
1979
|
logger.warn("[NeuroLink] MCP servers shutdown failed:", error);
|
|
1854
1980
|
}
|
|
1855
1981
|
}
|
|
1982
|
+
// Shutdown TaskManager
|
|
1983
|
+
if (this._taskManager) {
|
|
1984
|
+
try {
|
|
1985
|
+
await withTimeout(this._taskManager.shutdown(), 5000, new Error("TaskManager shutdown timed out"));
|
|
1986
|
+
}
|
|
1987
|
+
catch (error) {
|
|
1988
|
+
logger.warn("[NeuroLink] TaskManager shutdown error:", error);
|
|
1989
|
+
}
|
|
1990
|
+
finally {
|
|
1991
|
+
this._taskManager = undefined;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1856
1994
|
// Close conversation memory manager (release Redis connections, etc.)
|
|
1857
1995
|
if (this.conversationMemory?.close) {
|
|
1858
1996
|
try {
|
|
@@ -2428,6 +2566,17 @@ Current user's request: ${currentInput}`;
|
|
|
2428
2566
|
});
|
|
2429
2567
|
}
|
|
2430
2568
|
}
|
|
2569
|
+
// Memory retrieval for generate path
|
|
2570
|
+
if (this.shouldReadMemory(options.memory, options.context?.userId) &&
|
|
2571
|
+
options.context?.userId) {
|
|
2572
|
+
try {
|
|
2573
|
+
options.input.text = await this.retrieveMemory(options.input.text, options.context.userId, options.memory?.additionalUsers);
|
|
2574
|
+
logger.debug("Memory retrieval successful (generate)");
|
|
2575
|
+
}
|
|
2576
|
+
catch (error) {
|
|
2577
|
+
logger.warn("Memory retrieval failed (generate):", error);
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2431
2580
|
// 🔧 CRITICAL FIX: Convert to TextGenerationOptions while preserving the input object for multimodal support
|
|
2432
2581
|
const baseOptions = {
|
|
2433
2582
|
prompt: options.input.text,
|
|
@@ -2457,6 +2606,8 @@ Current user's request: ${currentInput}`;
|
|
|
2457
2606
|
abortSignal: options.abortSignal,
|
|
2458
2607
|
skipToolPromptInjection: options.skipToolPromptInjection,
|
|
2459
2608
|
middleware: options.middleware,
|
|
2609
|
+
// Pass through conversation messages for task continuation and external callers
|
|
2610
|
+
conversationMessages: options.conversationMessages,
|
|
2460
2611
|
};
|
|
2461
2612
|
// Auto-map top-level sessionId/userId to context for convenience
|
|
2462
2613
|
// Tests and users may pass sessionId/userId as top-level options
|
|
@@ -2613,7 +2764,7 @@ Current user's request: ${currentInput}`;
|
|
|
2613
2764
|
// Memory storage
|
|
2614
2765
|
if (this.shouldWriteMemory(options.memory, options.context?.userId, generateResult.content) &&
|
|
2615
2766
|
options.context?.userId) {
|
|
2616
|
-
this.storeMemoryInBackground(originalPrompt ?? "", generateResult.content.trim(), options.context.userId);
|
|
2767
|
+
this.storeMemoryInBackground(originalPrompt ?? "", generateResult.content.trim(), options.context.userId, options.memory?.additionalUsers);
|
|
2617
2768
|
}
|
|
2618
2769
|
}
|
|
2619
2770
|
/**
|
|
@@ -2681,7 +2832,15 @@ Current user's request: ${currentInput}`;
|
|
|
2681
2832
|
// Execute workflow
|
|
2682
2833
|
const workflowResult = await runWorkflow(workflowConfig, {
|
|
2683
2834
|
prompt: options.input.text,
|
|
2684
|
-
conversationHistory: options.
|
|
2835
|
+
conversationHistory: options.conversationMessages
|
|
2836
|
+
?.filter((m) => m.role === "user" || m.role === "assistant")
|
|
2837
|
+
.map((m) => ({
|
|
2838
|
+
role: m.role,
|
|
2839
|
+
content: typeof m.content === "string"
|
|
2840
|
+
? m.content
|
|
2841
|
+
: JSON.stringify(m.content),
|
|
2842
|
+
})) ??
|
|
2843
|
+
options.conversationHistory,
|
|
2685
2844
|
timeout: options.timeout,
|
|
2686
2845
|
verbose: false,
|
|
2687
2846
|
metadata: options.context,
|
|
@@ -2772,7 +2931,15 @@ Current user's request: ${currentInput}`;
|
|
|
2772
2931
|
// Execute workflow with progressive streaming
|
|
2773
2932
|
const workflowStream = runWorkflowWithStreaming(workflowConfig, {
|
|
2774
2933
|
prompt: options.input.text,
|
|
2775
|
-
conversationHistory: options.
|
|
2934
|
+
conversationHistory: options.conversationMessages
|
|
2935
|
+
?.filter((m) => m.role === "user" || m.role === "assistant")
|
|
2936
|
+
.map((m) => ({
|
|
2937
|
+
role: m.role,
|
|
2938
|
+
content: typeof m.content === "string"
|
|
2939
|
+
? m.content
|
|
2940
|
+
: JSON.stringify(m.content),
|
|
2941
|
+
})) ??
|
|
2942
|
+
options.conversationHistory,
|
|
2776
2943
|
timeout: options.timeout,
|
|
2777
2944
|
verbose: false,
|
|
2778
2945
|
metadata: options.context,
|
|
@@ -4411,7 +4578,7 @@ Current user's request: ${currentInput}`;
|
|
|
4411
4578
|
if (this.shouldReadMemory(options.memory, options.context?.userId) &&
|
|
4412
4579
|
options.context?.userId) {
|
|
4413
4580
|
try {
|
|
4414
|
-
options.input.text = await this.retrieveMemory(options.input.text, options.context.userId);
|
|
4581
|
+
options.input.text = await this.retrieveMemory(options.input.text, options.context.userId, options.memory?.additionalUsers);
|
|
4415
4582
|
logger.debug("Memory retrieval successful");
|
|
4416
4583
|
}
|
|
4417
4584
|
catch (error) {
|
|
@@ -4724,7 +4891,7 @@ Current user's request: ${currentInput}`;
|
|
|
4724
4891
|
}
|
|
4725
4892
|
}
|
|
4726
4893
|
if (this.shouldWriteMemory(enhancedOptions.memory, enhancedOptions.context?.userId, accumulatedContent)) {
|
|
4727
|
-
this.storeMemoryInBackground(originalPrompt ?? "", accumulatedContent.trim(), enhancedOptions.context?.userId);
|
|
4894
|
+
this.storeMemoryInBackground(originalPrompt ?? "", accumulatedContent.trim(), enhancedOptions.context?.userId, enhancedOptions.memory?.additionalUsers);
|
|
4728
4895
|
}
|
|
4729
4896
|
}
|
|
4730
4897
|
/**
|
|
@@ -4982,6 +5149,7 @@ Current user's request: ${currentInput}`;
|
|
|
4982
5149
|
model: options.model,
|
|
4983
5150
|
temperature: options.temperature,
|
|
4984
5151
|
maxTokens: options.maxTokens,
|
|
5152
|
+
conversationMessages: options.conversationMessages,
|
|
4985
5153
|
});
|
|
4986
5154
|
// Create a wrapper around the fallback stream that accumulates content
|
|
4987
5155
|
let fallbackAccumulatedContent = "";
|
|
@@ -8332,6 +8500,19 @@ Current user's request: ${currentInput}`;
|
|
|
8332
8500
|
cleanupErrors.push(err);
|
|
8333
8501
|
logger.warn("[NeuroLink] Error clearing caches:", error);
|
|
8334
8502
|
}
|
|
8503
|
+
// 5b. Shutdown TaskManager
|
|
8504
|
+
if (this._taskManager) {
|
|
8505
|
+
try {
|
|
8506
|
+
logger.debug("[NeuroLink] Shutting down TaskManager...");
|
|
8507
|
+
await withTimeout(this._taskManager.shutdown(), 5000, new Error("TaskManager shutdown timed out"));
|
|
8508
|
+
}
|
|
8509
|
+
catch (error) {
|
|
8510
|
+
logger.warn("[NeuroLink] TaskManager shutdown error:", error);
|
|
8511
|
+
}
|
|
8512
|
+
finally {
|
|
8513
|
+
this._taskManager = undefined;
|
|
8514
|
+
}
|
|
8515
|
+
}
|
|
8335
8516
|
// 6. Reset initialization flags
|
|
8336
8517
|
try {
|
|
8337
8518
|
logger.debug("[NeuroLink] Resetting initialization state...");
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BullMQ Backend — Production-grade task scheduling via Redis.
|
|
3
|
+
*
|
|
4
|
+
* - Cron tasks → BullMQ repeatable jobs with cron pattern
|
|
5
|
+
* - Interval tasks → BullMQ repeatable jobs with `every` option
|
|
6
|
+
* - One-shot tasks → BullMQ delayed jobs
|
|
7
|
+
* - Survives process restarts (Redis-persisted)
|
|
8
|
+
*/
|
|
9
|
+
import { type Task, type TaskBackend, type TaskExecutorFn, type TaskManagerConfig } from "../../types/taskTypes.js";
|
|
10
|
+
export declare class BullMQBackend implements TaskBackend {
|
|
11
|
+
readonly name = "bullmq";
|
|
12
|
+
private queue;
|
|
13
|
+
private worker;
|
|
14
|
+
private executors;
|
|
15
|
+
private config;
|
|
16
|
+
constructor(config: TaskManagerConfig);
|
|
17
|
+
initialize(): Promise<void>;
|
|
18
|
+
shutdown(): Promise<void>;
|
|
19
|
+
schedule(task: Task, executor: TaskExecutorFn): Promise<void>;
|
|
20
|
+
cancel(taskId: string): Promise<void>;
|
|
21
|
+
pause(taskId: string): Promise<void>;
|
|
22
|
+
resume(taskId: string): Promise<void>;
|
|
23
|
+
isHealthy(): Promise<boolean>;
|
|
24
|
+
/**
|
|
25
|
+
* Returns a connection options object for BullMQ / ioredis.
|
|
26
|
+
* When a URL is provided we parse it fully, preserving TLS (`rediss://`),
|
|
27
|
+
* ACL username, password, db index, and any query-string parameters so
|
|
28
|
+
* nothing is silently dropped.
|
|
29
|
+
*/
|
|
30
|
+
private getConnectionConfig;
|
|
31
|
+
private ensureInitialized;
|
|
32
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BullMQ Backend — Production-grade task scheduling via Redis.
|
|
3
|
+
*
|
|
4
|
+
* - Cron tasks → BullMQ repeatable jobs with cron pattern
|
|
5
|
+
* - Interval tasks → BullMQ repeatable jobs with `every` option
|
|
6
|
+
* - One-shot tasks → BullMQ delayed jobs
|
|
7
|
+
* - Survives process restarts (Redis-persisted)
|
|
8
|
+
*/
|
|
9
|
+
import { Queue, Worker } from "bullmq";
|
|
10
|
+
import { logger } from "../../utils/logger.js";
|
|
11
|
+
import { TaskError } from "../errors.js";
|
|
12
|
+
import { TASK_DEFAULTS, } from "../../types/taskTypes.js";
|
|
13
|
+
const QUEUE_NAME = "neurolink-tasks";
|
|
14
|
+
export class BullMQBackend {
|
|
15
|
+
name = "bullmq";
|
|
16
|
+
queue = null;
|
|
17
|
+
worker = null;
|
|
18
|
+
executors = new Map();
|
|
19
|
+
config;
|
|
20
|
+
constructor(config) {
|
|
21
|
+
this.config = config;
|
|
22
|
+
}
|
|
23
|
+
async initialize() {
|
|
24
|
+
const connection = this.getConnectionConfig();
|
|
25
|
+
this.queue = new Queue(QUEUE_NAME, { connection });
|
|
26
|
+
this.worker = new Worker(QUEUE_NAME, async (job) => {
|
|
27
|
+
const taskId = job.data.taskId;
|
|
28
|
+
const task = job.data.task;
|
|
29
|
+
const executor = this.executors.get(taskId);
|
|
30
|
+
if (!executor) {
|
|
31
|
+
logger.warn("[BullMQ] No executor found for task", { taskId });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
logger.info("[BullMQ] Executing task", { taskId, name: task.name });
|
|
35
|
+
const result = await executor(task);
|
|
36
|
+
return result;
|
|
37
|
+
}, {
|
|
38
|
+
connection,
|
|
39
|
+
concurrency: this.config.maxConcurrentRuns ?? TASK_DEFAULTS.maxConcurrentRuns,
|
|
40
|
+
});
|
|
41
|
+
this.worker.on("failed", (job, err) => {
|
|
42
|
+
logger.error("[BullMQ] Job failed", {
|
|
43
|
+
taskId: job?.data?.taskId,
|
|
44
|
+
error: String(err),
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
this.worker.on("error", (err) => {
|
|
48
|
+
logger.error("[BullMQ] Worker error", { error: String(err) });
|
|
49
|
+
});
|
|
50
|
+
logger.info("[BullMQ] Backend initialized");
|
|
51
|
+
}
|
|
52
|
+
async shutdown() {
|
|
53
|
+
if (this.worker) {
|
|
54
|
+
await this.worker.close();
|
|
55
|
+
this.worker = null;
|
|
56
|
+
}
|
|
57
|
+
if (this.queue) {
|
|
58
|
+
await this.queue.close();
|
|
59
|
+
this.queue = null;
|
|
60
|
+
}
|
|
61
|
+
this.executors.clear();
|
|
62
|
+
logger.info("[BullMQ] Backend shut down");
|
|
63
|
+
}
|
|
64
|
+
async schedule(task, executor) {
|
|
65
|
+
this.ensureInitialized();
|
|
66
|
+
this.executors.set(task.id, executor);
|
|
67
|
+
const jobData = { taskId: task.id, task };
|
|
68
|
+
const schedule = task.schedule;
|
|
69
|
+
if (schedule.type === "cron") {
|
|
70
|
+
await this.queue.upsertJobScheduler(task.id, {
|
|
71
|
+
pattern: schedule.expression,
|
|
72
|
+
...(schedule.timezone ? { tz: schedule.timezone } : {}),
|
|
73
|
+
}, { name: task.name, data: jobData });
|
|
74
|
+
}
|
|
75
|
+
else if (schedule.type === "interval") {
|
|
76
|
+
await this.queue.upsertJobScheduler(task.id, { every: schedule.every }, { name: task.name, data: jobData });
|
|
77
|
+
}
|
|
78
|
+
else if (schedule.type === "once") {
|
|
79
|
+
const at = typeof schedule.at === "string" ? new Date(schedule.at) : schedule.at;
|
|
80
|
+
const delay = Math.max(0, at.getTime() - Date.now());
|
|
81
|
+
await this.queue.add(task.name, jobData, {
|
|
82
|
+
jobId: task.id,
|
|
83
|
+
delay,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
logger.info("[BullMQ] Task scheduled", {
|
|
87
|
+
taskId: task.id,
|
|
88
|
+
type: schedule.type,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
async cancel(taskId) {
|
|
92
|
+
this.ensureInitialized();
|
|
93
|
+
this.executors.delete(taskId);
|
|
94
|
+
// Remove repeatable job scheduler
|
|
95
|
+
try {
|
|
96
|
+
await this.queue.removeJobScheduler(taskId);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// May not be a repeatable job — try removing by job ID
|
|
100
|
+
}
|
|
101
|
+
// Remove delayed/waiting job
|
|
102
|
+
try {
|
|
103
|
+
const job = await this.queue.getJob(taskId);
|
|
104
|
+
if (job) {
|
|
105
|
+
await job.remove();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Job may already be processed/removed
|
|
110
|
+
}
|
|
111
|
+
logger.info("[BullMQ] Task cancelled", { taskId });
|
|
112
|
+
}
|
|
113
|
+
async pause(taskId) {
|
|
114
|
+
// BullMQ doesn't have per-job pause, so we fully cancel the job scheduler
|
|
115
|
+
// and executor. This is intentionally destructive — cancel() removes both
|
|
116
|
+
// the executor from the map and the job/scheduler from Redis.
|
|
117
|
+
//
|
|
118
|
+
// Resume flow (orchestrated by TaskManager):
|
|
119
|
+
// 1. TaskManager.resume() updates task status to "active" in the store
|
|
120
|
+
// 2. TaskManager.resume() calls backend.schedule(task, newExecutor)
|
|
121
|
+
// 3. schedule() re-registers the executor and creates a new job/scheduler
|
|
122
|
+
//
|
|
123
|
+
// Because TaskManager always supplies a fresh executor on schedule(),
|
|
124
|
+
// there is no need to preserve the old executor here.
|
|
125
|
+
await this.cancel(taskId);
|
|
126
|
+
logger.info("[BullMQ] Task paused (cancelled pending jobs; TaskManager will re-schedule on resume)", { taskId });
|
|
127
|
+
}
|
|
128
|
+
async resume(taskId) {
|
|
129
|
+
// No-op: BullMQ resume is handled by TaskManager calling schedule() after
|
|
130
|
+
// this method returns. See TaskManager.resume() which calls:
|
|
131
|
+
// backend.schedule(updatedTask, executor)
|
|
132
|
+
// That call re-registers the executor and creates the job/scheduler in Redis.
|
|
133
|
+
logger.info("[BullMQ] Task resume requested (awaiting re-schedule from TaskManager)", { taskId });
|
|
134
|
+
}
|
|
135
|
+
async isHealthy() {
|
|
136
|
+
if (!this.queue) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
// Check if the queue can reach Redis
|
|
141
|
+
await this.queue.getJobCounts();
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// ── Internal ──────────────────────────────────────────
|
|
149
|
+
/**
|
|
150
|
+
* Returns a connection options object for BullMQ / ioredis.
|
|
151
|
+
* When a URL is provided we parse it fully, preserving TLS (`rediss://`),
|
|
152
|
+
* ACL username, password, db index, and any query-string parameters so
|
|
153
|
+
* nothing is silently dropped.
|
|
154
|
+
*/
|
|
155
|
+
getConnectionConfig() {
|
|
156
|
+
const redis = this.config.redis ?? {};
|
|
157
|
+
if (redis.url) {
|
|
158
|
+
const parsed = new URL(redis.url);
|
|
159
|
+
const opts = {
|
|
160
|
+
host: parsed.hostname || "localhost",
|
|
161
|
+
port: Number(parsed.port) || 6379,
|
|
162
|
+
db: parsed.pathname ? Number(parsed.pathname.slice(1)) || 0 : 0,
|
|
163
|
+
};
|
|
164
|
+
if (parsed.password) {
|
|
165
|
+
opts.password = decodeURIComponent(parsed.password);
|
|
166
|
+
}
|
|
167
|
+
if (parsed.username) {
|
|
168
|
+
opts.username = decodeURIComponent(parsed.username);
|
|
169
|
+
}
|
|
170
|
+
// rediss:// scheme → enable TLS
|
|
171
|
+
if (parsed.protocol === "rediss:") {
|
|
172
|
+
opts.tls = {};
|
|
173
|
+
}
|
|
174
|
+
return opts;
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
host: redis.host ?? TASK_DEFAULTS.redis.host,
|
|
178
|
+
port: redis.port ?? TASK_DEFAULTS.redis.port,
|
|
179
|
+
...(redis.password ? { password: redis.password } : {}),
|
|
180
|
+
db: redis.db ?? 0,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
ensureInitialized() {
|
|
184
|
+
if (!this.queue) {
|
|
185
|
+
throw TaskError.create("BACKEND_NOT_INITIALIZED", "[BullMQ] Backend not initialized. Call initialize() first.");
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=bullmqBackend.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NodeTimeout Backend — Development/zero-dependency task scheduling.
|
|
3
|
+
*
|
|
4
|
+
* - Cron tasks → parsed with `croner`, scheduled via setTimeout chains
|
|
5
|
+
* - Interval tasks → setInterval
|
|
6
|
+
* - One-shot tasks → setTimeout
|
|
7
|
+
* - All timers are in-process — lost on restart
|
|
8
|
+
*/
|
|
9
|
+
import { type Task, type TaskBackend, type TaskExecutorFn, type TaskManagerConfig } from "../../types/taskTypes.js";
|
|
10
|
+
export declare class NodeTimeoutBackend implements TaskBackend {
|
|
11
|
+
readonly name = "node-timeout";
|
|
12
|
+
private scheduled;
|
|
13
|
+
private paused;
|
|
14
|
+
private disposed;
|
|
15
|
+
private activeRuns;
|
|
16
|
+
private maxConcurrentRuns;
|
|
17
|
+
constructor(config: TaskManagerConfig);
|
|
18
|
+
initialize(): Promise<void>;
|
|
19
|
+
shutdown(): Promise<void>;
|
|
20
|
+
schedule(task: Task, executor: TaskExecutorFn): Promise<void>;
|
|
21
|
+
cancel(taskId: string): Promise<void>;
|
|
22
|
+
pause(taskId: string): Promise<void>;
|
|
23
|
+
resume(taskId: string): Promise<void>;
|
|
24
|
+
isHealthy(): Promise<boolean>;
|
|
25
|
+
private executeTask;
|
|
26
|
+
private clearEntry;
|
|
27
|
+
}
|