@juspay/neurolink 12.0.5 → 12.2.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 +3 -3
- package/dist/agent/agentToolRegistrar.d.ts +30 -0
- package/dist/agent/agentToolRegistrar.js +72 -18
- package/dist/agent/backgroundCommands.d.ts +110 -0
- package/dist/agent/backgroundCommands.js +914 -0
- package/dist/agent/backgroundDelegation.d.ts +87 -0
- package/dist/agent/backgroundDelegation.js +753 -0
- package/dist/agent/gitTools.d.ts +43 -0
- package/dist/agent/gitTools.js +618 -0
- package/dist/agent/taskChecklist.d.ts +58 -0
- package/dist/agent/taskChecklist.js +322 -0
- package/dist/artifacts/artifactBanking.d.ts +57 -0
- package/dist/artifacts/artifactBanking.js +123 -0
- package/dist/artifacts/artifactStore.d.ts +36 -8
- package/dist/artifacts/artifactStore.js +164 -13
- package/dist/browser/neurolink.min.js +442 -414
- package/dist/cli/commands/setup.js +2 -1
- package/dist/constants/enums.d.ts +19 -0
- package/dist/constants/enums.js +20 -0
- package/dist/factories/providerDescriptors.js +16 -1
- package/dist/models/manifestRegistry.js +2 -0
- package/dist/models/manifests/cerebras.d.ts +9 -0
- package/dist/models/manifests/cerebras.js +19 -0
- package/dist/neurolink.d.ts +294 -3
- package/dist/neurolink.js +447 -4
- package/dist/providers/openaiCompatCatalog.d.ts +1 -1
- package/dist/providers/openaiCompatCatalog.js +34 -3
- package/dist/types/artifact.d.ts +54 -0
- package/dist/types/backgroundCommand.d.ts +174 -0
- package/dist/types/backgroundCommand.js +22 -0
- package/dist/types/delegation.d.ts +178 -0
- package/dist/types/delegation.js +18 -0
- package/dist/types/gitTools.d.ts +69 -0
- package/dist/types/gitTools.js +22 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.js +8 -0
- package/dist/types/pathSandbox.d.ts +23 -0
- package/dist/types/pathSandbox.js +12 -0
- package/dist/types/providers.d.ts +4 -0
- package/dist/types/tasks.d.ts +85 -0
- package/dist/types/tasks.js +14 -0
- package/dist/types/tools.d.ts +11 -0
- package/dist/utils/modelChoices.js +17 -1
- package/dist/utils/pathSandbox.d.ts +49 -0
- package/dist/utils/pathSandbox.js +127 -0
- package/dist/utils/providerConfig.d.ts +4 -0
- package/dist/utils/providerConfig.js +17 -0
- package/package.json +5 -1
package/dist/neurolink.js
CHANGED
|
@@ -48,6 +48,7 @@ import { EnhancedToolDiscovery } from "./mcp/enhancedToolDiscovery.js";
|
|
|
48
48
|
import { ExternalServerManager } from "./mcp/externalServerManager.js";
|
|
49
49
|
import { McpOutputNormalizer, DEFAULT_MAX_MCP_OUTPUT_BYTES, DEFAULT_WARN_MCP_OUTPUT_BYTES, } from "./mcp/mcpOutputNormalizer.js";
|
|
50
50
|
import { LocalTempArtifactStore } from "./artifacts/artifactStore.js";
|
|
51
|
+
import { bankArtifact as bankArtifactPayload, readArtifact as readBankedArtifact, } from "./artifacts/artifactBanking.js";
|
|
51
52
|
import { ToolRouter } from "./mcp/routing/index.js";
|
|
52
53
|
// Import direct tools server for automatic registration
|
|
53
54
|
import { directToolsServer } from "./mcp/servers/agent/directToolsServer.js";
|
|
@@ -66,6 +67,10 @@ import { AnalyticsService, calculateAdvancedCost, parseAnalyticsQualityScore, }
|
|
|
66
67
|
import { SpanStatus, SpanType, CircuitBreakerOpenError, ConversationMemoryError, ModelAccessDeniedError, } from "./types/index.js";
|
|
67
68
|
import { SpanSerializer } from "./observability/utils/spanSerializer.js";
|
|
68
69
|
import { flushOpenTelemetry, getLangfuseContext, getLangfuseHealthStatus, initializeOpenTelemetry, isOpenTelemetryInitialized, runWithCurrentLangfuseContext, setLangfuseContext, shutdownOpenTelemetry, stampGuestRescueIdentity, } from "./services/server/ai/observability/instrumentation.js";
|
|
70
|
+
import { clearChecklistState, createChecklistTools, getChecklistState, resolveChecklistSessionId, } from "./agent/taskChecklist.js";
|
|
71
|
+
import { cancelDelegates as cancelBackgroundDelegates, collectDelegates as collectBackgroundDelegates, configureDelegation, createDelegationTools, spawnDelegate as spawnBackgroundDelegate, } from "./agent/backgroundDelegation.js";
|
|
72
|
+
import { awaitBackgroundCommand as awaitCommand, createBackgroundCommandTools, getBackgroundCommandStatus as getCommandStatus, killAllBackgroundCommands as killAllCommands, killBackgroundCommand as killCommand, readBackgroundCommandOutput as readCommandOutput, setBackgroundCommandPolicy as setCommandPolicy, startBackgroundCommand as startCommand, } from "./agent/backgroundCommands.js";
|
|
73
|
+
import { configureGitTools, createGitTools, runGitCommand as runGitArgs, } from "./agent/gitTools.js";
|
|
69
74
|
import { TaskManager } from "./tasks/taskManager.js";
|
|
70
75
|
import { createTaskTools } from "./tasks/tools/taskTools.js";
|
|
71
76
|
import { ATTR, spanJsonAttribute } from "./telemetry/attributes.js";
|
|
@@ -792,6 +797,28 @@ export class NeuroLink {
|
|
|
792
797
|
* tool — gates the per-turn delegation scope in generate().
|
|
793
798
|
*/
|
|
794
799
|
hasAgentTools = false;
|
|
800
|
+
/**
|
|
801
|
+
* Tools registered with `cacheable: false` — their results are never served
|
|
802
|
+
* from the tool-result cache. A tool whose answer depends on live state (a
|
|
803
|
+
* checklist, a work queue) would otherwise replay its first answer for the
|
|
804
|
+
* whole TTL to every caller passing the same arguments.
|
|
805
|
+
*/
|
|
806
|
+
uncacheableTools = new Set();
|
|
807
|
+
/** Set once registerTaskTools() has registered the checklist toolset. */
|
|
808
|
+
hasTaskChecklistTools = false;
|
|
809
|
+
/** Set once registerDelegationTools() has registered the delegation toolset. */
|
|
810
|
+
hasBackgroundDelegationTools = false;
|
|
811
|
+
/** Set once registerBackgroundCommandTools() has registered the command toolset. */
|
|
812
|
+
hasBackgroundCommandTools = false;
|
|
813
|
+
/** Set once registerGitTools() has registered the read-only git toolset. */
|
|
814
|
+
hasGitTools = false;
|
|
815
|
+
/**
|
|
816
|
+
* Set once `retrieve_context` has been registered. The constructor skips
|
|
817
|
+
* registration when neither Redis nor an artifact store exists; banking can
|
|
818
|
+
* create a store later, and this keeps that second attempt from
|
|
819
|
+
* re-registering the tool on instances that already have it.
|
|
820
|
+
*/
|
|
821
|
+
retrieveContextRegistered = false;
|
|
795
822
|
/**
|
|
796
823
|
* Creates a new NeuroLink instance for AI text generation with MCP tool integration.
|
|
797
824
|
*
|
|
@@ -953,7 +980,7 @@ export class NeuroLink {
|
|
|
953
980
|
if (this._taskManagerConfig) {
|
|
954
981
|
this._taskManager = new TaskManager(this, this._taskManagerConfig);
|
|
955
982
|
this._taskManager.setEmitter(this.emitter);
|
|
956
|
-
this.
|
|
983
|
+
this.registerSchedulerTaskTools(this._taskManager);
|
|
957
984
|
}
|
|
958
985
|
}
|
|
959
986
|
/**
|
|
@@ -966,7 +993,7 @@ export class NeuroLink {
|
|
|
966
993
|
if (!this._taskManager) {
|
|
967
994
|
this._taskManager = new TaskManager(this, this._taskManagerConfig);
|
|
968
995
|
this._taskManager.setEmitter(this.emitter);
|
|
969
|
-
this.
|
|
996
|
+
this.registerSchedulerTaskTools(this._taskManager);
|
|
970
997
|
}
|
|
971
998
|
return this._taskManager;
|
|
972
999
|
}
|
|
@@ -1266,11 +1293,14 @@ export class NeuroLink {
|
|
|
1266
1293
|
});
|
|
1267
1294
|
}
|
|
1268
1295
|
/**
|
|
1269
|
-
* Register task
|
|
1296
|
+
* Register SCHEDULER task tools bound to a TaskManager instance (task_create,
|
|
1297
|
+
* task_list, … — scheduled/self-running jobs). Distinct from the agent task
|
|
1298
|
+
* CHECKLIST (registerTaskTools() / tasks_create): different tools, different
|
|
1299
|
+
* state, different purpose.
|
|
1270
1300
|
* Follows the same factory + registry pattern as registerFileTools().
|
|
1271
1301
|
* Called when TaskManager is created (eagerly or lazily via the `tasks` getter).
|
|
1272
1302
|
*/
|
|
1273
|
-
|
|
1303
|
+
registerSchedulerTaskTools(manager) {
|
|
1274
1304
|
const taskTools = createTaskTools(manager);
|
|
1275
1305
|
for (const [toolName, toolDef] of Object.entries(taskTools)) {
|
|
1276
1306
|
const toolId = `direct.${toolName}`;
|
|
@@ -1331,6 +1361,9 @@ export class NeuroLink {
|
|
|
1331
1361
|
* Only registered when Redis conversation memory is active.
|
|
1332
1362
|
*/
|
|
1333
1363
|
registerMemoryRetrievalTools() {
|
|
1364
|
+
if (this.retrieveContextRegistered) {
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1334
1367
|
// Check if conversation memory is configured
|
|
1335
1368
|
// Memory retrieval tool requires Redis (getSessionRaw) but registration
|
|
1336
1369
|
// is deferred — the execute handler checks at runtime whether the actual
|
|
@@ -1379,6 +1412,7 @@ export class NeuroLink {
|
|
|
1379
1412
|
return await withTimeout(tools.retrieve_context.execute(params, { toolCallId: "memory-retrieval", messages: [] }), TOOL_TIMEOUTS.EXECUTION_DEFAULT_MS, ErrorFactory.toolTimeout("retrieve_context", TOOL_TIMEOUTS.EXECUTION_DEFAULT_MS));
|
|
1380
1413
|
},
|
|
1381
1414
|
});
|
|
1415
|
+
this.retrieveContextRegistered = true;
|
|
1382
1416
|
logger.info("[NeuroLink] Memory retrieval tools registered");
|
|
1383
1417
|
}
|
|
1384
1418
|
/**
|
|
@@ -2730,6 +2764,17 @@ Current user's request: ${currentInput}`;
|
|
|
2730
2764
|
async shutdown() {
|
|
2731
2765
|
try {
|
|
2732
2766
|
logger.debug("[NeuroLink] Starting graceful shutdown");
|
|
2767
|
+
// Kill host-owned background work first: a shutdown must not leave
|
|
2768
|
+
// child processes or delegate workers running with nobody to collect
|
|
2769
|
+
// them.
|
|
2770
|
+
try {
|
|
2771
|
+
await killAllCommands(this);
|
|
2772
|
+
await cancelBackgroundDelegates(this);
|
|
2773
|
+
logger.debug("[NeuroLink] Background commands and delegates stopped");
|
|
2774
|
+
}
|
|
2775
|
+
catch (error) {
|
|
2776
|
+
logger.warn("[NeuroLink] Background work cleanup failed:", error);
|
|
2777
|
+
}
|
|
2733
2778
|
try {
|
|
2734
2779
|
await flushOpenTelemetry();
|
|
2735
2780
|
await shutdownOpenTelemetry();
|
|
@@ -9463,6 +9508,15 @@ Current user's request: ${currentInput}`;
|
|
|
9463
9508
|
const mcpServerInfo = createCustomToolServerInfo(name, convertedTool, options?.timeout, options?.maxRetries);
|
|
9464
9509
|
// Register with toolRegistry using MCPServerInfo directly
|
|
9465
9510
|
this.toolRegistry.registerServer(mcpServerInfo);
|
|
9511
|
+
// Re-registration replaces options wholesale: omitting `cacheable`
|
|
9512
|
+
// clears a previous `cacheable: false`. A caller replacing a tool whose
|
|
9513
|
+
// results are live state must repeat the flag.
|
|
9514
|
+
if (options?.cacheable === false) {
|
|
9515
|
+
this.uncacheableTools.add(name);
|
|
9516
|
+
}
|
|
9517
|
+
else {
|
|
9518
|
+
this.uncacheableTools.delete(name);
|
|
9519
|
+
}
|
|
9466
9520
|
// Emit tool registration success event
|
|
9467
9521
|
this.emitter.emit("tools-register:end", {
|
|
9468
9522
|
toolName: name,
|
|
@@ -9537,6 +9591,7 @@ Current user's request: ${currentInput}`;
|
|
|
9537
9591
|
const serverId = `custom-tool-${name}`;
|
|
9538
9592
|
const removed = this.toolRegistry.unregisterServer(serverId);
|
|
9539
9593
|
if (removed) {
|
|
9594
|
+
this.uncacheableTools.delete(name);
|
|
9540
9595
|
logger.info(`Unregistered custom tool: ${name}`);
|
|
9541
9596
|
}
|
|
9542
9597
|
return removed;
|
|
@@ -10185,6 +10240,7 @@ Current user's request: ${currentInput}`;
|
|
|
10185
10240
|
const isCacheEnabled = this.mcpToolResultCache &&
|
|
10186
10241
|
!options.disableToolCache &&
|
|
10187
10242
|
!this._disableToolCacheForCurrentRequest &&
|
|
10243
|
+
!this.uncacheableTools.has(toolName) &&
|
|
10188
10244
|
!toolAnnotations?.destructiveHint;
|
|
10189
10245
|
const toolResultCache = this.mcpToolResultCache;
|
|
10190
10246
|
// === MCP ENHANCEMENT: Cache check (before execution) ===
|
|
@@ -12576,6 +12632,15 @@ Current user's request: ${currentInput}`;
|
|
|
12576
12632
|
}),
|
|
12577
12633
|
};
|
|
12578
12634
|
const worker = new NeuroLink(workerConfig);
|
|
12635
|
+
// The cacheable:false flag lives BESIDE the registry, not in it, so a
|
|
12636
|
+
// shared registry alone would re-enable caching on the worker for exactly
|
|
12637
|
+
// the tools whose results are live state (tasks_list, command_status,
|
|
12638
|
+
// collect_results). The worker inherits the host's full uncacheable set.
|
|
12639
|
+
if (options?.shareToolRegistry !== false) {
|
|
12640
|
+
for (const uncacheableName of this.uncacheableTools) {
|
|
12641
|
+
worker.uncacheableTools.add(uncacheableName);
|
|
12642
|
+
}
|
|
12643
|
+
}
|
|
12579
12644
|
// Constructing an instance rebinds the process-global logger sink to the
|
|
12580
12645
|
// new instance's emitter — restore the host as the active sink so host
|
|
12581
12646
|
// log bridges keep flowing while workers come and go.
|
|
@@ -12678,6 +12743,371 @@ Current user's request: ${currentInput}`;
|
|
|
12678
12743
|
this.hasAgentTools = true;
|
|
12679
12744
|
return registered;
|
|
12680
12745
|
}
|
|
12746
|
+
/**
|
|
12747
|
+
* Register the task CHECKLIST toolset — `tasks_create`, `tasks_update`,
|
|
12748
|
+
* `tasks_list` — on this instance (TodoWrite-style planning for a
|
|
12749
|
+
* long-running run). Opt-in and idempotent: existing callers see no new
|
|
12750
|
+
* tools until they ask for them.
|
|
12751
|
+
*
|
|
12752
|
+
* The checklist is session state, not conversation state: it lives outside
|
|
12753
|
+
* the message list, so summarization/compaction cannot lose it, and every
|
|
12754
|
+
* tool result returns the whole list so the model re-anchors for free after
|
|
12755
|
+
* a compaction. Read the same state from host code with
|
|
12756
|
+
* {@link getTaskState} — that is all a completeness gate needs.
|
|
12757
|
+
*
|
|
12758
|
+
* Sessions come from the tool execution context. Call
|
|
12759
|
+
* `setToolContext({ sessionId })` (or run the agent through
|
|
12760
|
+
* `runIsolatedAgent`, which stamps one) so the checklist has a stable
|
|
12761
|
+
* identity; a model tool call with no session anywhere falls back to a
|
|
12762
|
+
* single default checklist per instance rather than one per call. A DIRECT
|
|
12763
|
+
* `executeTool("tasks_create", …)` call should pass
|
|
12764
|
+
* `authContext: { sessionId }` — the tool registry otherwise mints a fresh
|
|
12765
|
+
* id for that one call.
|
|
12766
|
+
*
|
|
12767
|
+
* @see {@link getTaskState} for the host-side read
|
|
12768
|
+
*/
|
|
12769
|
+
registerTaskTools() {
|
|
12770
|
+
if (this.hasTaskChecklistTools) {
|
|
12771
|
+
return;
|
|
12772
|
+
}
|
|
12773
|
+
const tools = createChecklistTools(this);
|
|
12774
|
+
for (const [toolName, toolDef] of Object.entries(tools)) {
|
|
12775
|
+
// Never cacheable: every one of these reads or mutates live checklist
|
|
12776
|
+
// state, so a cached `tasks_list` would report a list that has already
|
|
12777
|
+
// moved on — the exact silent staleness the checklist exists to prevent.
|
|
12778
|
+
this.registerTool(toolName, toolDef, { cacheable: false });
|
|
12779
|
+
}
|
|
12780
|
+
this.hasTaskChecklistTools = true;
|
|
12781
|
+
logger.info(`[NeuroLink] Registered ${Object.keys(tools).length} task checklist tools`);
|
|
12782
|
+
}
|
|
12783
|
+
/**
|
|
12784
|
+
* Read a session's task checklist — synchronous, so a completeness gate is
|
|
12785
|
+
* one line of host code:
|
|
12786
|
+
* `getTaskState(id).items.filter(i => i.status === "pending")`.
|
|
12787
|
+
*
|
|
12788
|
+
* Never throws: an unknown session simply has an empty checklist. Omit
|
|
12789
|
+
* `sessionId` to read the session the tools would currently write to (the
|
|
12790
|
+
* instance's tool-context session, or its default checklist).
|
|
12791
|
+
*/
|
|
12792
|
+
getTaskState(sessionId) {
|
|
12793
|
+
return getChecklistState(sessionId ?? resolveChecklistSessionId(this));
|
|
12794
|
+
}
|
|
12795
|
+
/**
|
|
12796
|
+
* Drop a session's checklist. Returns whether there was one to drop.
|
|
12797
|
+
* Omit `sessionId` to clear the session the tools currently write to.
|
|
12798
|
+
*/
|
|
12799
|
+
clearTaskState(sessionId) {
|
|
12800
|
+
return clearChecklistState(sessionId ?? resolveChecklistSessionId(this));
|
|
12801
|
+
}
|
|
12802
|
+
// ========================================
|
|
12803
|
+
// Async Delegation API (N2)
|
|
12804
|
+
// ========================================
|
|
12805
|
+
/**
|
|
12806
|
+
* Register the background-delegation toolset — `delegate_task` and
|
|
12807
|
+
* `collect_results` — on this instance. Opt-in and idempotent: existing
|
|
12808
|
+
* callers see no new tools until they ask for them.
|
|
12809
|
+
*
|
|
12810
|
+
* Delegation through {@link registerAgentTool} is synchronous — the loop
|
|
12811
|
+
* blocks on each worker. These tools make it asynchronous: `delegate_task`
|
|
12812
|
+
* returns a `workerId` at once and the agent keeps working, then
|
|
12813
|
+
* `collect_results` claims whichever worker finished FIRST. Concurrency is
|
|
12814
|
+
* bounded by the same process-wide pool `registerAgentTool` uses (raised,
|
|
12815
|
+
* never lowered, by `maxConcurrent`), each worker's FULL report is banked to
|
|
12816
|
+
* a file via {@link bankArtifact}, and the outstanding counts ride along in
|
|
12817
|
+
* every `tasks_list` result so the agent learns a worker landed without
|
|
12818
|
+
* polling.
|
|
12819
|
+
*
|
|
12820
|
+
* @param options - Depth ceiling, pool raise, and queue wait
|
|
12821
|
+
* @see {@link spawnDelegate} for the host-side spawn
|
|
12822
|
+
* @see {@link collectDelegates} for the host-side collect
|
|
12823
|
+
*/
|
|
12824
|
+
registerDelegationTools(options) {
|
|
12825
|
+
configureDelegation(this, options);
|
|
12826
|
+
if (this.hasBackgroundDelegationTools) {
|
|
12827
|
+
return;
|
|
12828
|
+
}
|
|
12829
|
+
const tools = createDelegationTools(this);
|
|
12830
|
+
for (const [toolName, toolDef] of Object.entries(tools)) {
|
|
12831
|
+
// Never cacheable: `delegate_task` starts a new worker every call and
|
|
12832
|
+
// `collect_results` claims each outcome exactly once. A cached result
|
|
12833
|
+
// would spawn nothing and hand the same worker back twice.
|
|
12834
|
+
this.registerTool(toolName, toolDef, { cacheable: false });
|
|
12835
|
+
}
|
|
12836
|
+
this.hasBackgroundDelegationTools = true;
|
|
12837
|
+
logger.info(`[NeuroLink] Registered ${Object.keys(tools).length} background delegation tools`);
|
|
12838
|
+
}
|
|
12839
|
+
/**
|
|
12840
|
+
* Start a background worker and get its handle immediately — before it has
|
|
12841
|
+
* run anything, and long before it finishes.
|
|
12842
|
+
*
|
|
12843
|
+
* The worker runs through {@link runIsolatedAgent}: a fresh session on a
|
|
12844
|
+
* worker instance sharing THIS instance's tool registry (so live MCP
|
|
12845
|
+
* connections are reused), waste detection, honest stop reasons. Its
|
|
12846
|
+
* complete report is banked when it settles; the outcome you collect carries
|
|
12847
|
+
* a bounded summary plus the read-back call for the rest.
|
|
12848
|
+
*
|
|
12849
|
+
* @example
|
|
12850
|
+
* ```typescript
|
|
12851
|
+
* const a = await neurolink.spawnDelegate({ task: "Audit the auth changes" });
|
|
12852
|
+
* const b = await neurolink.spawnDelegate({ task: "Review the migrations" });
|
|
12853
|
+
* // …keep working…
|
|
12854
|
+
* const first = await neurolink.collectDelegates({ mode: "any" });
|
|
12855
|
+
* ```
|
|
12856
|
+
*
|
|
12857
|
+
* @param options - Task, scope, context, tool allowlist, budgets
|
|
12858
|
+
* @returns The worker id, spawn time, and whether it is queued for a slot
|
|
12859
|
+
* @throws when the task is empty or the caller is at the depth ceiling
|
|
12860
|
+
*/
|
|
12861
|
+
async spawnDelegate(options) {
|
|
12862
|
+
return spawnBackgroundDelegate(this, options);
|
|
12863
|
+
}
|
|
12864
|
+
/**
|
|
12865
|
+
* Claim finished background workers — in COMPLETION order, which has nothing
|
|
12866
|
+
* to do with spawn order. Each outcome is handed out exactly once.
|
|
12867
|
+
*
|
|
12868
|
+
* @param request - `{ mode: "any" | "all" }` or `{ workerId }`, plus `waitMs`
|
|
12869
|
+
* @returns Claimed outcomes plus what is still pending/ready
|
|
12870
|
+
*/
|
|
12871
|
+
async collectDelegates(request) {
|
|
12872
|
+
return collectBackgroundDelegates(this, request);
|
|
12873
|
+
}
|
|
12874
|
+
/**
|
|
12875
|
+
* Cancel background workers: one by id, or every outstanding worker this
|
|
12876
|
+
* instance spawned. Cancelled workers still settle into a claimable outcome
|
|
12877
|
+
* saying so.
|
|
12878
|
+
*
|
|
12879
|
+
* @param workerId - Cancel just this worker; omit to cancel all
|
|
12880
|
+
* @returns How many workers were cancelled
|
|
12881
|
+
*/
|
|
12882
|
+
async cancelDelegates(workerId) {
|
|
12883
|
+
return cancelBackgroundDelegates(this, workerId);
|
|
12884
|
+
}
|
|
12885
|
+
// ========================================
|
|
12886
|
+
// Artifact Banking API (N3)
|
|
12887
|
+
// ========================================
|
|
12888
|
+
/**
|
|
12889
|
+
* This instance's artifact store, created on first use.
|
|
12890
|
+
*
|
|
12891
|
+
* Until now a store existed only when `mcp.outputLimits.strategy` was set to
|
|
12892
|
+
* `"externalize"`, so a caller that just wanted to bank a worker report had
|
|
12893
|
+
* to configure MCP output limits it did not use. This creates one on demand
|
|
12894
|
+
* and registers `retrieve_context` alongside it, so a banked payload is
|
|
12895
|
+
* readable by the model, not only by host code.
|
|
12896
|
+
*
|
|
12897
|
+
* Already-configured instances get the store they already had — the MCP
|
|
12898
|
+
* output normalizer and banking deliberately share one store, so an
|
|
12899
|
+
* externalized tool output and a banked report read back the same way.
|
|
12900
|
+
*
|
|
12901
|
+
* @returns The artifact store backing {@link bankArtifact} / {@link readArtifact}
|
|
12902
|
+
*/
|
|
12903
|
+
getArtifactStore() {
|
|
12904
|
+
if (!this.mcpArtifactStore) {
|
|
12905
|
+
this.mcpArtifactStore = new LocalTempArtifactStore();
|
|
12906
|
+
logger.debug("[NeuroLink] Artifact store created on demand (local-temp) for banking");
|
|
12907
|
+
}
|
|
12908
|
+
// A no-op when the constructor already registered it.
|
|
12909
|
+
this.registerMemoryRetrievalTools();
|
|
12910
|
+
return this.mcpArtifactStore;
|
|
12911
|
+
}
|
|
12912
|
+
/**
|
|
12913
|
+
* Bank a payload to a file and get back a pointer to it.
|
|
12914
|
+
*
|
|
12915
|
+
* The payload is stored WHOLE. What you put in the conversation is the
|
|
12916
|
+
* returned `preview` (a bounded head slice) and `readBackHint` (the literal
|
|
12917
|
+
* `retrieve_context` call that fetches the rest) — so a 4 MB worker report
|
|
12918
|
+
* costs a few hundred tokens of context and loses nothing, and compaction
|
|
12919
|
+
* can drop the preview without destroying evidence.
|
|
12920
|
+
*
|
|
12921
|
+
* @example
|
|
12922
|
+
* ```typescript
|
|
12923
|
+
* const ref = await neurolink.bankArtifact(fullReport, {
|
|
12924
|
+
* kind: "worker-report",
|
|
12925
|
+
* label: "delegate:auth-review",
|
|
12926
|
+
* sessionId: "review-1421",
|
|
12927
|
+
* });
|
|
12928
|
+
* // Hand the model ref.preview + ref.readBackHint, never fullReport.
|
|
12929
|
+
* ```
|
|
12930
|
+
*
|
|
12931
|
+
* @param payload Complete text or JSON. Never truncated.
|
|
12932
|
+
* @param options `kind` and `label` are required; see {@link BankArtifactOptions}
|
|
12933
|
+
* @returns Id, bounded preview, byte size, and the read-back call
|
|
12934
|
+
*/
|
|
12935
|
+
async bankArtifact(payload, options) {
|
|
12936
|
+
return bankArtifactPayload(this, payload, options);
|
|
12937
|
+
}
|
|
12938
|
+
/**
|
|
12939
|
+
* Read a banked payload back from host code — the programmatic twin of the
|
|
12940
|
+
* model's `retrieve_context({ artifactId })` call.
|
|
12941
|
+
*
|
|
12942
|
+
* Omit `page` for the complete payload; pass `{ offset, limit }` to walk a
|
|
12943
|
+
* large one in windows. Returns null when the id is unknown or expired.
|
|
12944
|
+
*
|
|
12945
|
+
* @param id `artifactId` from a {@link BankedArtifactRef}
|
|
12946
|
+
* @param page Optional character window
|
|
12947
|
+
*/
|
|
12948
|
+
async readArtifact(id, page) {
|
|
12949
|
+
return readBankedArtifact(this, id, page);
|
|
12950
|
+
}
|
|
12951
|
+
// ========================================
|
|
12952
|
+
// Background Command API (N4)
|
|
12953
|
+
// ========================================
|
|
12954
|
+
/**
|
|
12955
|
+
* Register the background-command toolset — `run_command_bg`,
|
|
12956
|
+
* `command_status`, `command_output`, `command_kill` — on this instance, and
|
|
12957
|
+
* declare what may be executed. Opt-in and idempotent: existing callers see
|
|
12958
|
+
* no new tools until they ask for them.
|
|
12959
|
+
*
|
|
12960
|
+
* A reviewing agent needs to run real commands — a build, a test suite, a
|
|
12961
|
+
* linter whose output is the evidence for a finding — without blocking its
|
|
12962
|
+
* own loop and without losing a byte of what they printed. These tools start
|
|
12963
|
+
* a command detached, write both streams to files as they arrive, and bank
|
|
12964
|
+
* the COMPLETE files as artifacts when the command settles; the conversation
|
|
12965
|
+
* gets a bounded tail plus the read-back call.
|
|
12966
|
+
*
|
|
12967
|
+
* The policy is not optional. `allowedExecutables` is matched exactly
|
|
12968
|
+
* against `argv[0]`, `cwdRoot` is a realpath-checked sandbox, there is never
|
|
12969
|
+
* a shell, and a command that outlives `defaultTimeoutMs` is killed.
|
|
12970
|
+
*
|
|
12971
|
+
* @param policy - What may run, where, for how long, and how loudly
|
|
12972
|
+
* @see {@link startBackgroundCommand} for the host-side start
|
|
12973
|
+
* @see {@link registerGitTools} for read-only git without a general policy
|
|
12974
|
+
*/
|
|
12975
|
+
registerBackgroundCommandTools(policy) {
|
|
12976
|
+
setCommandPolicy(this, policy);
|
|
12977
|
+
if (this.hasBackgroundCommandTools) {
|
|
12978
|
+
return;
|
|
12979
|
+
}
|
|
12980
|
+
const tools = createBackgroundCommandTools(this);
|
|
12981
|
+
for (const [toolName, toolDef] of Object.entries(tools)) {
|
|
12982
|
+
// Never cacheable: every one of these reads or changes live process
|
|
12983
|
+
// state. A cached `command_status` would report a build that has already
|
|
12984
|
+
// finished as still running, for the whole TTL.
|
|
12985
|
+
this.registerTool(toolName, toolDef, { cacheable: false });
|
|
12986
|
+
}
|
|
12987
|
+
this.hasBackgroundCommandTools = true;
|
|
12988
|
+
logger.info(`[NeuroLink] Registered ${Object.keys(tools).length} background command tools`);
|
|
12989
|
+
}
|
|
12990
|
+
/**
|
|
12991
|
+
* Declare (or replace) what this instance may execute, without registering
|
|
12992
|
+
* the model-facing tools. Host code that only drives
|
|
12993
|
+
* {@link startBackgroundCommand} itself needs nothing more than this.
|
|
12994
|
+
*
|
|
12995
|
+
* @param policy - What may run, where, for how long, and how loudly
|
|
12996
|
+
*/
|
|
12997
|
+
setBackgroundCommandPolicy(policy) {
|
|
12998
|
+
setCommandPolicy(this, policy);
|
|
12999
|
+
}
|
|
13000
|
+
/**
|
|
13001
|
+
* Start a command in the background and get its task id immediately.
|
|
13002
|
+
*
|
|
13003
|
+
* @example
|
|
13004
|
+
* ```typescript
|
|
13005
|
+
* const { taskId } = await neurolink.startBackgroundCommand(
|
|
13006
|
+
* ["pnpm", "run", "lint"],
|
|
13007
|
+
* { cwd: repoRoot },
|
|
13008
|
+
* );
|
|
13009
|
+
* // …keep working…
|
|
13010
|
+
* const status = await neurolink.awaitBackgroundCommand(taskId);
|
|
13011
|
+
* const full = await neurolink.readArtifact(status.stdout!.artifactId);
|
|
13012
|
+
* ```
|
|
13013
|
+
*
|
|
13014
|
+
* @param argv - Executable first, one entry per argument. Never a command string.
|
|
13015
|
+
* @param options - cwd (sandboxed), timeout, byte cap, env, label, session
|
|
13016
|
+
* @returns The task id, the argv that ran, and when it started
|
|
13017
|
+
* @throws when no policy is set, argv is malformed, the executable is not
|
|
13018
|
+
* allowlisted, the policy vetoes it, or the cwd escapes the sandbox
|
|
13019
|
+
*/
|
|
13020
|
+
async startBackgroundCommand(argv, options) {
|
|
13021
|
+
return startCommand(this, argv, options);
|
|
13022
|
+
}
|
|
13023
|
+
/**
|
|
13024
|
+
* Everything known about one command right now — synchronous, so a
|
|
13025
|
+
* mid-loop monitor costs nothing.
|
|
13026
|
+
*
|
|
13027
|
+
* @param taskId - Task id from {@link startBackgroundCommand}
|
|
13028
|
+
* @throws when the task id is unknown to this instance
|
|
13029
|
+
*/
|
|
13030
|
+
getBackgroundCommandStatus(taskId) {
|
|
13031
|
+
return getCommandStatus(this, taskId);
|
|
13032
|
+
}
|
|
13033
|
+
/**
|
|
13034
|
+
* Wait for a command to settle. `timeoutMs` bounds the WAIT, not the
|
|
13035
|
+
* command: when it elapses the current status is returned rather than
|
|
13036
|
+
* thrown, so a caller can poll in bounded steps and never lose the job.
|
|
13037
|
+
*
|
|
13038
|
+
* @param taskId - Task id from {@link startBackgroundCommand}
|
|
13039
|
+
* @param opts - `timeoutMs` to bound the wait
|
|
13040
|
+
*/
|
|
13041
|
+
async awaitBackgroundCommand(taskId, opts) {
|
|
13042
|
+
return awaitCommand(this, taskId, opts);
|
|
13043
|
+
}
|
|
13044
|
+
/**
|
|
13045
|
+
* Kill a running command — SIGTERM, then SIGKILL five seconds later — and
|
|
13046
|
+
* resolve with its settled status. Whatever it printed first is still
|
|
13047
|
+
* banked: killing a command discards the process, never its output.
|
|
13048
|
+
*
|
|
13049
|
+
* @param taskId - Task id from {@link startBackgroundCommand}
|
|
13050
|
+
* @param signal - Signal to send first. Default SIGTERM
|
|
13051
|
+
*/
|
|
13052
|
+
async killBackgroundCommand(taskId, signal) {
|
|
13053
|
+
return killCommand(this, taskId, signal);
|
|
13054
|
+
}
|
|
13055
|
+
/**
|
|
13056
|
+
* Read one character window of a command's output straight from its log
|
|
13057
|
+
* file — while it is still running, or long after it finished. Offsets,
|
|
13058
|
+
* `totalSize` and `hasMore` match `retrieve_context` exactly.
|
|
13059
|
+
*
|
|
13060
|
+
* @param taskId - Task id from {@link startBackgroundCommand}
|
|
13061
|
+
* @param page - Which stream, and which window of it
|
|
13062
|
+
*/
|
|
13063
|
+
async readBackgroundCommandOutput(taskId, page) {
|
|
13064
|
+
return readCommandOutput(this, taskId, page);
|
|
13065
|
+
}
|
|
13066
|
+
// ========================================
|
|
13067
|
+
// Read-only Git Toolset (N4.4)
|
|
13068
|
+
// ========================================
|
|
13069
|
+
/**
|
|
13070
|
+
* Register the read-only git toolset — `git_log`, `git_show`, `git_diff`,
|
|
13071
|
+
* `git_blame`, `git_merge_base`, `git_ls_files` — on this instance. Opt-in
|
|
13072
|
+
* and idempotent.
|
|
13073
|
+
*
|
|
13074
|
+
* These are BOUNDED tools, not a shell: the model supplies values (a ref, a
|
|
13075
|
+
* path, a line range), never flags, and each tool assembles a fixed argv
|
|
13076
|
+
* from them. That is what keeps them read-only — a free-form argument string
|
|
13077
|
+
* would carry `--output=<file>` and `diff.external` straight through.
|
|
13078
|
+
*
|
|
13079
|
+
* Registering them widens nothing else: they run under a private
|
|
13080
|
+
* one-executable policy rooted at `repoRoot`, so `run_command_bg` still
|
|
13081
|
+
* cannot execute git, and no general command policy is required.
|
|
13082
|
+
*
|
|
13083
|
+
* @param options - Repository root, plus timeout / byte-cap / preview bounds
|
|
13084
|
+
*/
|
|
13085
|
+
registerGitTools(options) {
|
|
13086
|
+
configureGitTools(this, options);
|
|
13087
|
+
if (this.hasGitTools) {
|
|
13088
|
+
return;
|
|
13089
|
+
}
|
|
13090
|
+
const tools = createGitTools(this);
|
|
13091
|
+
for (const [toolName, toolDef] of Object.entries(tools)) {
|
|
13092
|
+
// Never cacheable: the working tree moves under these answers.
|
|
13093
|
+
this.registerTool(toolName, toolDef, { cacheable: false });
|
|
13094
|
+
}
|
|
13095
|
+
this.hasGitTools = true;
|
|
13096
|
+
logger.info(`[NeuroLink] Registered ${Object.keys(tools).length} read-only git tools`);
|
|
13097
|
+
}
|
|
13098
|
+
/**
|
|
13099
|
+
* Run one read-only git command from host code, with the same bounding the
|
|
13100
|
+
* tools get: the complete stdout is banked, the result carries a preview and
|
|
13101
|
+
* the read-back call.
|
|
13102
|
+
*
|
|
13103
|
+
* @param args - Git arguments, e.g. `["log", "--oneline"]`. Assembled by the
|
|
13104
|
+
* caller, which is responsible for every value in them
|
|
13105
|
+
* @param sessionId - Session the command belongs to
|
|
13106
|
+
* @throws when {@link registerGitTools} has not been called
|
|
13107
|
+
*/
|
|
13108
|
+
async runGitCommand(args, sessionId) {
|
|
13109
|
+
return runGitArgs(this, args, sessionId);
|
|
13110
|
+
}
|
|
12681
13111
|
/**
|
|
12682
13112
|
* Execute an agent network with the given input.
|
|
12683
13113
|
*
|
|
@@ -12775,6 +13205,19 @@ Current user's request: ${currentInput}`;
|
|
|
12775
13205
|
this.lastCompactionMessageCount.clear();
|
|
12776
13206
|
const cleanupErrors = [];
|
|
12777
13207
|
try {
|
|
13208
|
+
// 0. Kill host-owned background work: a disposed instance must not
|
|
13209
|
+
// leave child processes or delegate workers running.
|
|
13210
|
+
try {
|
|
13211
|
+
await killAllCommands(this);
|
|
13212
|
+
await cancelBackgroundDelegates(this);
|
|
13213
|
+
}
|
|
13214
|
+
catch (error) {
|
|
13215
|
+
const err = error instanceof Error
|
|
13216
|
+
? error
|
|
13217
|
+
: new Error(`Background work cleanup error: ${String(error)}`);
|
|
13218
|
+
cleanupErrors.push(err);
|
|
13219
|
+
logger.warn("[NeuroLink] Error stopping background work:", error);
|
|
13220
|
+
}
|
|
12778
13221
|
// 1. Flush and shutdown OpenTelemetry
|
|
12779
13222
|
try {
|
|
12780
13223
|
logger.debug("[NeuroLink] Flushing and shutting down OpenTelemetry...");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { OpenAICompatCatalogEntry } from "../types/index.js";
|
|
2
2
|
/**
|
|
3
|
-
* Config-driven catalog of the
|
|
3
|
+
* Config-driven catalog of the 8 zero-quirk OpenAI-compatible providers.
|
|
4
4
|
* Each entry fully replaces what used to be a hand-written
|
|
5
5
|
* OpenAIChatCompletionsProvider subclass — see ConfiguredOpenAICompatProvider
|
|
6
6
|
* for the class that reads these entries, and providerRegistry.ts for the
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { AIProviderName } from "../constants/enums.js";
|
|
2
|
-
import { CloudflareModels, FireworksModels, GroqModels, MistralModels, PerplexityModels, TogetherAIModels, XaiModels, } from "../constants/enums.js";
|
|
2
|
+
import { CerebrasModels, CloudflareModels, FireworksModels, GroqModels, MistralModels, PerplexityModels, TogetherAIModels, XaiModels, } from "../constants/enums.js";
|
|
3
3
|
import { AuthenticationError, InvalidModelError, ProviderError, } from "../types/index.js";
|
|
4
4
|
import { DEFAULT_ERROR_RULES } from "../utils/errorClassifier.js";
|
|
5
|
-
import { createCloudflareConfig, createFireworksConfig, createGroqConfig, createMistralConfig, createPerplexityConfig, createTogetherAIConfig, createXaiConfig, } from "../utils/providerConfig.js";
|
|
5
|
+
import { createCerebrasConfig, createCloudflareConfig, createFireworksConfig, createGroqConfig, createMistralConfig, createPerplexityConfig, createTogetherAIConfig, createXaiConfig, } from "../utils/providerConfig.js";
|
|
6
6
|
function buildCloudflareBaseURL(accountId) {
|
|
7
7
|
return `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/v1`;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
|
-
* Config-driven catalog of the
|
|
10
|
+
* Config-driven catalog of the 8 zero-quirk OpenAI-compatible providers.
|
|
11
11
|
* Each entry fully replaces what used to be a hand-written
|
|
12
12
|
* OpenAIChatCompletionsProvider subclass — see ConfiguredOpenAICompatProvider
|
|
13
13
|
* for the class that reads these entries, and providerRegistry.ts for the
|
|
@@ -29,6 +29,37 @@ function buildCloudflareBaseURL(accountId) {
|
|
|
29
29
|
* Task 14's docs task for the deciding criteria).
|
|
30
30
|
*/
|
|
31
31
|
export const OPENAI_COMPAT_CATALOG = [
|
|
32
|
+
{
|
|
33
|
+
providerName: AIProviderName.CEREBRAS,
|
|
34
|
+
aliases: ["cerebras"],
|
|
35
|
+
apiKeyEnvVar: "CEREBRAS_API_KEY",
|
|
36
|
+
baseURLEnvVar: "CEREBRAS_BASE_URL",
|
|
37
|
+
defaultBaseURL: "https://api.cerebras.ai/v1",
|
|
38
|
+
configOptions: createCerebrasConfig(),
|
|
39
|
+
modelEnvVar: "CEREBRAS_MODEL",
|
|
40
|
+
defaultModel: CerebrasModels.LLAMA_3_3_70B,
|
|
41
|
+
registryDefaultModel: CerebrasModels.LLAMA_3_3_70B,
|
|
42
|
+
registryDefaultModelChecksEnvVar: true,
|
|
43
|
+
fallbackModelName: CerebrasModels.LLAMA_3_1_8B,
|
|
44
|
+
fallbackModels: [
|
|
45
|
+
CerebrasModels.LLAMA_3_3_70B,
|
|
46
|
+
CerebrasModels.LLAMA_3_1_8B,
|
|
47
|
+
CerebrasModels.QWEN_3_32B,
|
|
48
|
+
CerebrasModels.GPT_OSS_120B,
|
|
49
|
+
],
|
|
50
|
+
errorRules: [
|
|
51
|
+
{
|
|
52
|
+
// Probed live 2026-08-26: a bad key gets HTTP 401 with body
|
|
53
|
+
// {"message":"Wrong API Key","type":"invalid_request_error",
|
|
54
|
+
// "param":"api_key","code":"wrong_api_key"}.
|
|
55
|
+
match: (ctx) => ctx.statusCode === 401 ||
|
|
56
|
+
/wrong_api_key|Wrong API Key|invalid_api_key/i.test(ctx.message),
|
|
57
|
+
errorClass: AuthenticationError,
|
|
58
|
+
message: "Invalid Cerebras API key. Check CEREBRAS_API_KEY. Get one at https://cloud.cerebras.ai",
|
|
59
|
+
},
|
|
60
|
+
...DEFAULT_ERROR_RULES,
|
|
61
|
+
],
|
|
62
|
+
},
|
|
32
63
|
{
|
|
33
64
|
providerName: AIProviderName.GROQ,
|
|
34
65
|
aliases: ["groq"],
|